Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Attribute.alias #950

Merged
merged 24 commits into from Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.d/950.change.rst
@@ -0,0 +1,5 @@
``attrs.field`` now supports an ``alias`` option for explicit ``__init__`` argument names.

Get ``__init__`` signatures matching any taste, peculiar or plain!
The `PEP 681 compatible <https://peps.python.org/pep-0681/#field-specifier-parameters>`_ ``alias`` option can be use to override private attribute name mangling,
or add other arbitrary field argument name overrides.
10 changes: 5 additions & 5 deletions docs/api.rst
Expand Up @@ -74,7 +74,7 @@ Core
... class C:
... x = attr.ib()
>>> attr.fields(C).x
Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)
Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x')


.. autofunction:: attrs.make_class
Expand Down Expand Up @@ -246,9 +246,9 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attrs.fields(C)
(Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None))
(Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y'))
>>> attrs.fields(C)[1]
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')
>>> attrs.fields(C).y is attrs.fields(C)[1]
True

Expand All @@ -267,9 +267,9 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attrs.fields_dict(C)
{'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)}
{'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')}
>>> attr.fields_dict(C)['y']
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')
>>> attrs.fields_dict(C)['y'] is attrs.fields(C).y
True

Expand Down
10 changes: 10 additions & 0 deletions docs/examples.rst
Expand Up @@ -70,6 +70,16 @@ If you want to initialize your private attributes yourself, you can do that too:
...
TypeError: __init__() takes exactly 1 argument (2 given)

If you prefer to expose your privates, you can use keyword argument aliases:

.. doctest::

>>> @define
... class C:
... _x: int = field(alias="_x")
>>> C(_x=1)
C(_x=1)

An additional way of defining attributes is supported too.
This is useful in times when you want to enhance classes that are not yours (nice ``__repr__`` for Django models anyone?):

Expand Down
24 changes: 23 additions & 1 deletion docs/extending.rst
Expand Up @@ -16,7 +16,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``:
... @define
... class C:
... a: int
(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=<class 'int'>, converter=None, kw_only=False, inherited=False, on_setattr=None),)
(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=<class 'int'>, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='a'),)


.. warning::
Expand Down Expand Up @@ -260,6 +260,28 @@ A more realistic example would be to automatically convert data that you, e.g.,
>>> Data(**from_json) # ****
Data(a=3, b='spam', c=datetime.datetime(2020, 5, 4, 13, 37))

Or, perhaps you would prefer to generate dataclass-compatible ``__init__`` signatures via a default field ``alias``.
Note, ``field_transformer`` operates on `attrs.Attribute` instances before the default private-attribute handling is applied so explicit user-provided aliases can be detected.

.. doctest::

>>> def dataclass_names(cls, fields):
... return [
... field.evolve(alias=field.name)
... if not field.alias
... else field
... for field in fields
... ]
...
>>> @frozen(field_transformer=dataclass_names)
... class Data:
... public: int
... _private: str
... explicit: str = field(alias="aliased")
...
>>> Data(public=42, _private="spam", aliased="yes")
Data(public=42, _private='spam', aliased='yes')


Customize Value Serialization in ``asdict()``
---------------------------------------------
Expand Down
19 changes: 17 additions & 2 deletions docs/init.rst
Expand Up @@ -47,9 +47,10 @@ Embrace functions and classmethods as a filter between reality and what's best f

If you look for powerful-yet-unintrusive serialization and validation for your ``attrs`` classes, have a look at our sibling project `cattrs <https://cattrs.readthedocs.io/>`_ or our `third-party extensions <https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs>`_.

.. _private_attributes:
asford marked this conversation as resolved.
Show resolved Hide resolved

Private Attributes
------------------
Private Attributes and Aliases
------------------------------

One thing people tend to find confusing is the treatment of private attributes that start with an underscore.
``attrs`` follows the doctrine that `there is no such thing as a private argument`_ and strips the underscores from the name when writing the ``__init__`` method signature:
Expand Down Expand Up @@ -78,6 +79,20 @@ But it's important to be aware of it because it can lead to surprising syntax er

In this case a valid attribute name ``_1`` got transformed into an invalid argument name ``1``.

If your taste differs, you can use the ``alias`` argument to `attrs.field` to explicitly set the argument name.
This can be used to override private attribute handling, or make other arbitrary changes to ``__init__`` argument names.

.. doctest::

>>> import inspect, attr, attrs
>>> from attr import define
>>> @define
... class C:
... _x: int = field(alias="_x")
... y: int = field(alias="distasteful_y")
>>> inspect.signature(C.__init__)
<Signature (self, x: int, distasteful_y: int) -> None>


Defaults
--------
Expand Down
10 changes: 10 additions & 0 deletions src/attr/__init__.pyi
Expand Up @@ -134,6 +134,8 @@ class Attribute(Generic[_T]):
type: Optional[Type[_T]]
kw_only: bool
on_setattr: _OnSetAttrType
alias: Optional[str]

def evolve(self, **changes: Any) -> "Attribute[Any]": ...

# NOTE: We had several choices for the annotation to use for type arg:
Expand Down Expand Up @@ -176,6 +178,7 @@ def attrib(
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> Any: ...

# This form catches an explicit None or no default and infers the type from the
Expand All @@ -196,6 +199,7 @@ def attrib(
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> _T: ...

# This form catches an explicit default argument.
Expand All @@ -215,6 +219,7 @@ def attrib(
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> _T: ...

# This form covers type=non-Type: e.g. forward references (str), Any
Expand All @@ -234,6 +239,7 @@ def attrib(
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> Any: ...
@overload
def field(
Expand All @@ -250,6 +256,7 @@ def field(
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> Any: ...

# This form catches an explicit None or no default and infers the type from the
Expand All @@ -269,6 +276,7 @@ def field(
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> _T: ...

# This form catches an explicit default argument.
Expand All @@ -287,6 +295,7 @@ def field(
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> _T: ...

# This form covers type=non-Type: e.g. forward references (str), Any
Expand All @@ -305,6 +314,7 @@ def field(
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> Any: ...
@overload
@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
Expand Down
2 changes: 1 addition & 1 deletion src/attr/_funcs.py
Expand Up @@ -359,7 +359,7 @@ def evolve(inst, **changes):
if not a.init:
continue
attr_name = a.name # To deal with private attributes.
init_name = attr_name if attr_name[0] != "_" else attr_name[1:]
init_name = a.alias
asford marked this conversation as resolved.
Show resolved Hide resolved
if init_name not in changes:
changes[init_name] = getattr(inst, attr_name)

Expand Down
42 changes: 41 additions & 1 deletion src/attr/_make.py
Expand Up @@ -101,6 +101,7 @@ def attrib(
eq=None,
order=None,
on_setattr=None,
alias=None,
):
"""
Create a new attribute on a class.
Expand Down Expand Up @@ -208,6 +209,9 @@ def attrib(
attribute -- regardless of the setting in `attr.s`.
:type on_setattr: `callable`, or a list of callables, or `None`, or
`attrs.setters.NO_OP`
:param Optional[str] alias: Override this attribute's parameter name in the
generated ``__init__`` method. If left `None`, default to ``name``
stripped of leading underscores. See `private_attributes`.

.. versionadded:: 15.2.0 *convert*
.. versionadded:: 16.3.0 *metadata*
Expand All @@ -230,6 +234,7 @@ def attrib(
.. versionchanged:: 21.1.0
*eq*, *order*, and *cmp* also accept a custom callable
.. versionchanged:: 21.1.0 *cmp* undeprecated
.. versionchanged:: 21.5.0 *alias*
asford marked this conversation as resolved.
Show resolved Hide resolved
hynek marked this conversation as resolved.
Show resolved Hide resolved
"""
eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq, order, True
Expand Down Expand Up @@ -279,6 +284,7 @@ def attrib(
order=order,
order_key=order_key,
on_setattr=on_setattr,
alias=alias,
)


Expand Down Expand Up @@ -563,6 +569,14 @@ def _transform_attrs(
if field_transformer is not None:
attrs = field_transformer(cls, attrs)

# Resolve default field alias after executing field_transformer.
# This allows field_transformer to differentiate between explicit vs
# default aliases and supply their own defaults.
attrs = [
a.evolve(alias=_default_init_alias_for(a.name)) if not a.alias else a
for a in attrs
]

# Create AttrsClass *after* applying the field_transformer since it may
# add or remove attributes!
attr_names = [a.name for a in attrs]
Expand Down Expand Up @@ -2165,7 +2179,9 @@ def fmt_setter_with_converter(
has_on_setattr = a.on_setattr is not None or (
a.on_setattr is not setters.NO_OP and has_cls_on_setattr
)
arg_name = a.name.lstrip("_")
# a.alias is set to maybe-mangled attr_name in _ClassBuilder if not
# explicitly provided
arg_name = a.alias

has_factory = isinstance(a.default, Factory)
if has_factory and a.default.takes_self:
Expand Down Expand Up @@ -2358,6 +2374,17 @@ def fmt_setter_with_converter(
)


def _default_init_alias_for(name: str) -> str:
"""
The default __init__ parameter name for a field.

This performs private-name adjustment via leading-unscore stripping,
and is the default value of Attribute.alias if not provided.
"""

return name.lstrip("_")


class Attribute:
"""
*Read-only* representation of an attribute.
Expand All @@ -2382,12 +2409,15 @@ class Attribute:
- Validators get them passed as the first argument.
- The :ref:`field transformer <transform-fields>` hook receives a list of
them.
- ``alias`` exposes the __init__ parameter name of the field, including
with overrides or default private-attribute handling.

.. versionadded:: 20.1.0 *inherited*
.. versionadded:: 20.1.0 *on_setattr*
.. versionchanged:: 20.2.0 *inherited* is not taken into account for
equality checks and hashing anymore.
.. versionadded:: 21.1.0 *eq_key* and *order_key*
.. versionadded:: 22.2.0 *alias*

For the full version history of the fields, see `attr.ib`.
"""
Expand All @@ -2409,6 +2439,7 @@ class Attribute:
"kw_only",
"inherited",
"on_setattr",
"alias",
)

def __init__(
Expand All @@ -2430,6 +2461,7 @@ def __init__(
order=None,
order_key=None,
on_setattr=None,
alias=None,
):
eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq_key or eq, order_key or order, True
Expand Down Expand Up @@ -2463,6 +2495,7 @@ def __init__(
bound_setattr("kw_only", kw_only)
bound_setattr("inherited", inherited)
bound_setattr("on_setattr", on_setattr)
bound_setattr("alias", alias)

def __setattr__(self, name, value):
raise FrozenInstanceError()
Expand Down Expand Up @@ -2558,6 +2591,7 @@ def _setattrs(self, name_values_pairs):
hash=(name != "metadata"),
init=True,
inherited=False,
alias=_default_init_alias_for(name),
)
for name in Attribute.__slots__
]
Expand Down Expand Up @@ -2596,10 +2630,12 @@ class _CountingAttr:
"type",
"kw_only",
"on_setattr",
"alias",
)
__attrs_attrs__ = tuple(
Attribute(
name=name,
alias=_default_init_alias_for(name),
default=NOTHING,
validator=None,
repr=True,
Expand All @@ -2623,10 +2659,12 @@ class _CountingAttr:
"hash",
"init",
"on_setattr",
"alias",
)
) + (
Attribute(
name="metadata",
alias="metadata",
default=None,
validator=None,
repr=True,
Expand Down Expand Up @@ -2661,6 +2699,7 @@ def __init__(
order,
order_key,
on_setattr,
alias,
):
_CountingAttr.cls_counter += 1
self.counter = _CountingAttr.cls_counter
Expand All @@ -2678,6 +2717,7 @@ def __init__(
self.type = type
self.kw_only = kw_only
self.on_setattr = on_setattr
self.alias = alias

def validator(self, meth):
"""
Expand Down
2 changes: 2 additions & 0 deletions src/attr/_next_gen.py
Expand Up @@ -169,6 +169,7 @@ def field(
eq=None,
order=None,
on_setattr=None,
alias=None,
):
"""
Identical to `attr.ib`, except keyword-only and with some arguments
Expand All @@ -189,6 +190,7 @@ def field(
eq=eq,
order=order,
on_setattr=on_setattr,
alias=alias,
)


Expand Down