Skip to content

Commit ee3ecb1

Browse files
asfordpre-commit-ci[bot]hynek
authored
Add Attribute.alias (#950)
* Spike `alias` implementation. * Move default alias init to after field_transformer. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixup docs. * Update docs/extending.rst * Pre-commit fixes * Partially fix doctest * Add test docstrings. * Add typing_example tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tidy typing_example * Add note in init.rst on private aliases * Add alias example to examples.rst * Assert to comment * Add changelog entry * Fixup doc error * Tidy dataclass_transform docs * Lil' spice for the changelog. * Fix doctest * Update extending.rst * Make alias introspection more explicit * Update src/attr/_make.py Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hynek Schlawack <hs@ox.cx>
1 parent 69aca6c commit ee3ecb1

14 files changed

Lines changed: 254 additions & 12 deletions

changelog.d/950.change.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``attrs.field`` now supports an ``alias`` option for explicit ``__init__`` argument names.
2+
3+
Get ``__init__`` signatures matching any taste, peculiar or plain!
4+
The `PEP 681 compatible <https://peps.python.org/pep-0681/#field-specifier-parameters>`_ ``alias`` option can be use to override private attribute name mangling,
5+
or add other arbitrary field argument name overrides.

docs/api.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Core
7474
... class C:
7575
... x = attr.ib()
7676
>>> attr.fields(C).x
77-
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)
77+
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')
7878

7979

8080
.. autofunction:: attrs.make_class
@@ -246,9 +246,9 @@ Helpers
246246
... x = attr.ib()
247247
... y = attr.ib()
248248
>>> attrs.fields(C)
249-
(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))
249+
(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'))
250250
>>> attrs.fields(C)[1]
251-
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)
251+
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')
252252
>>> attrs.fields(C).y is attrs.fields(C)[1]
253253
True
254254

@@ -267,9 +267,9 @@ Helpers
267267
... x = attr.ib()
268268
... y = attr.ib()
269269
>>> attrs.fields_dict(C)
270-
{'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)}
270+
{'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')}
271271
>>> attr.fields_dict(C)['y']
272-
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)
272+
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')
273273
>>> attrs.fields_dict(C)['y'] is attrs.fields(C).y
274274
True
275275

docs/examples.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ If you want to initialize your private attributes yourself, you can do that too:
7070
...
7171
TypeError: __init__() takes exactly 1 argument (2 given)
7272

73+
If you prefer to expose your privates, you can use keyword argument aliases:
74+
75+
.. doctest::
76+
77+
>>> @define
78+
... class C:
79+
... _x: int = field(alias="_x")
80+
>>> C(_x=1)
81+
C(_x=1)
82+
7383
An additional way of defining attributes is supported too.
7484
This is useful in times when you want to enhance classes that are not yours (nice ``__repr__`` for Django models anyone?):
7585

docs/extending.rst

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``:
1616
... @define
1717
... class C:
1818
... a: int
19-
(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),)
19+
(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'),)
2020

2121

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

263+
Or, perhaps you would prefer to generate dataclass-compatible ``__init__`` signatures via a default field ``alias``.
264+
Note, ``field_transformer`` operates on `attrs.Attribute` instances before the default private-attribute handling is applied so explicit user-provided aliases can be detected.
265+
266+
.. doctest::
267+
268+
>>> def dataclass_names(cls, fields):
269+
... return [
270+
... field.evolve(alias=field.name)
271+
... if not field.alias
272+
... else field
273+
... for field in fields
274+
... ]
275+
...
276+
>>> @frozen(field_transformer=dataclass_names)
277+
... class Data:
278+
... public: int
279+
... _private: str
280+
... explicit: str = field(alias="aliased_name")
281+
...
282+
>>> Data(public=42, _private="spam", aliased_name="yes")
283+
Data(public=42, _private='spam', explicit='yes')
284+
263285

264286
Customize Value Serialization in ``asdict()``
265287
---------------------------------------------

docs/init.rst

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ Embrace functions and classmethods as a filter between reality and what's best f
4747

4848
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>`_.
4949

50+
.. _private_attributes:
5051

51-
Private Attributes
52-
------------------
52+
Private Attributes and Aliases
53+
------------------------------
5354

5455
One thing people tend to find confusing is the treatment of private attributes that start with an underscore.
5556
``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:
@@ -78,6 +79,20 @@ But it's important to be aware of it because it can lead to surprising syntax er
7879

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

82+
If your taste differs, you can use the ``alias`` argument to `attrs.field` to explicitly set the argument name.
83+
This can be used to override private attribute handling, or make other arbitrary changes to ``__init__`` argument names.
84+
85+
.. doctest::
86+
87+
>>> import inspect, attr, attrs
88+
>>> from attr import define
89+
>>> @define
90+
... class C:
91+
... _x: int = field(alias="_x")
92+
... y: int = field(alias="distasteful_y")
93+
>>> inspect.signature(C.__init__)
94+
<Signature (self, _x: int, distasteful_y: int) -> None>
95+
8196

8297
Defaults
8398
--------

src/attr/__init__.pyi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ class Attribute(Generic[_T]):
134134
type: Optional[Type[_T]]
135135
kw_only: bool
136136
on_setattr: _OnSetAttrType
137+
alias: Optional[str]
138+
137139
def evolve(self, **changes: Any) -> "Attribute[Any]": ...
138140

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

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

201205
# This form catches an explicit default argument.
@@ -215,6 +219,7 @@ def attrib(
215219
eq: Optional[_EqOrderType] = ...,
216220
order: Optional[_EqOrderType] = ...,
217221
on_setattr: Optional[_OnSetAttrArgType] = ...,
222+
alias: Optional[str] = ...,
218223
) -> _T: ...
219224

220225
# This form covers type=non-Type: e.g. forward references (str), Any
@@ -234,6 +239,7 @@ def attrib(
234239
eq: Optional[_EqOrderType] = ...,
235240
order: Optional[_EqOrderType] = ...,
236241
on_setattr: Optional[_OnSetAttrArgType] = ...,
242+
alias: Optional[str] = ...,
237243
) -> Any: ...
238244
@overload
239245
def field(
@@ -250,6 +256,7 @@ def field(
250256
eq: Optional[bool] = ...,
251257
order: Optional[bool] = ...,
252258
on_setattr: Optional[_OnSetAttrArgType] = ...,
259+
alias: Optional[str] = ...,
253260
) -> Any: ...
254261

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

274282
# This form catches an explicit default argument.
@@ -287,6 +295,7 @@ def field(
287295
eq: Optional[_EqOrderType] = ...,
288296
order: Optional[_EqOrderType] = ...,
289297
on_setattr: Optional[_OnSetAttrArgType] = ...,
298+
alias: Optional[str] = ...,
290299
) -> _T: ...
291300

292301
# This form covers type=non-Type: e.g. forward references (str), Any
@@ -305,6 +314,7 @@ def field(
305314
eq: Optional[_EqOrderType] = ...,
306315
order: Optional[_EqOrderType] = ...,
307316
on_setattr: Optional[_OnSetAttrArgType] = ...,
317+
alias: Optional[str] = ...,
308318
) -> Any: ...
309319
@overload
310320
@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))

src/attr/_funcs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ def evolve(inst, **changes):
359359
if not a.init:
360360
continue
361361
attr_name = a.name # To deal with private attributes.
362-
init_name = attr_name if attr_name[0] != "_" else attr_name[1:]
362+
init_name = a.alias
363363
if init_name not in changes:
364364
changes[init_name] = getattr(inst, attr_name)
365365

src/attr/_make.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def attrib(
101101
eq=None,
102102
order=None,
103103
on_setattr=None,
104+
alias=None,
104105
):
105106
"""
106107
Create a new attribute on a class.
@@ -208,6 +209,9 @@ def attrib(
208209
attribute -- regardless of the setting in `attr.s`.
209210
:type on_setattr: `callable`, or a list of callables, or `None`, or
210211
`attrs.setters.NO_OP`
212+
:param Optional[str] alias: Override this attribute's parameter name in the
213+
generated ``__init__`` method. If left `None`, default to ``name``
214+
stripped of leading underscores. See `private_attributes`.
211215
212216
.. versionadded:: 15.2.0 *convert*
213217
.. versionadded:: 16.3.0 *metadata*
@@ -230,6 +234,7 @@ def attrib(
230234
.. versionchanged:: 21.1.0
231235
*eq*, *order*, and *cmp* also accept a custom callable
232236
.. versionchanged:: 21.1.0 *cmp* undeprecated
237+
.. versionadded:: 22.2.0 *alias*
233238
"""
234239
eq, eq_key, order, order_key = _determine_attrib_eq_order(
235240
cmp, eq, order, True
@@ -279,6 +284,7 @@ def attrib(
279284
order=order,
280285
order_key=order_key,
281286
on_setattr=on_setattr,
287+
alias=alias,
282288
)
283289

284290

@@ -563,6 +569,14 @@ def _transform_attrs(
563569
if field_transformer is not None:
564570
attrs = field_transformer(cls, attrs)
565571

572+
# Resolve default field alias after executing field_transformer.
573+
# This allows field_transformer to differentiate between explicit vs
574+
# default aliases and supply their own defaults.
575+
attrs = [
576+
a.evolve(alias=_default_init_alias_for(a.name)) if not a.alias else a
577+
for a in attrs
578+
]
579+
566580
# Create AttrsClass *after* applying the field_transformer since it may
567581
# add or remove attributes!
568582
attr_names = [a.name for a in attrs]
@@ -2165,7 +2179,9 @@ def fmt_setter_with_converter(
21652179
has_on_setattr = a.on_setattr is not None or (
21662180
a.on_setattr is not setters.NO_OP and has_cls_on_setattr
21672181
)
2168-
arg_name = a.name.lstrip("_")
2182+
# a.alias is set to maybe-mangled attr_name in _ClassBuilder if not
2183+
# explicitly provided
2184+
arg_name = a.alias
21692185

21702186
has_factory = isinstance(a.default, Factory)
21712187
if has_factory and a.default.takes_self:
@@ -2358,6 +2374,17 @@ def fmt_setter_with_converter(
23582374
)
23592375

23602376

2377+
def _default_init_alias_for(name: str) -> str:
2378+
"""
2379+
The default __init__ parameter name for a field.
2380+
2381+
This performs private-name adjustment via leading-unscore stripping,
2382+
and is the default value of Attribute.alias if not provided.
2383+
"""
2384+
2385+
return name.lstrip("_")
2386+
2387+
23612388
class Attribute:
23622389
"""
23632390
*Read-only* representation of an attribute.
@@ -2367,6 +2394,8 @@ class Attribute:
23672394
following:
23682395
23692396
- ``name`` (`str`): The name of the attribute.
2397+
- ``alias`` (`str`): The __init__ parameter name of the attribute, after
2398+
any explicit overrides and default private-attribute-name handling.
23702399
- ``inherited`` (`bool`): Whether or not that attribute has been inherited
23712400
from a base class.
23722401
- ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables
@@ -2382,12 +2411,16 @@ class Attribute:
23822411
- Validators get them passed as the first argument.
23832412
- The :ref:`field transformer <transform-fields>` hook receives a list of
23842413
them.
2414+
- The ``alias`` property exposes the __init__ parameter name of the field,
2415+
with any overrides and default private-attribute handling applied.
2416+
23852417
23862418
.. versionadded:: 20.1.0 *inherited*
23872419
.. versionadded:: 20.1.0 *on_setattr*
23882420
.. versionchanged:: 20.2.0 *inherited* is not taken into account for
23892421
equality checks and hashing anymore.
23902422
.. versionadded:: 21.1.0 *eq_key* and *order_key*
2423+
.. versionadded:: 22.2.0 *alias*
23912424
23922425
For the full version history of the fields, see `attr.ib`.
23932426
"""
@@ -2409,6 +2442,7 @@ class Attribute:
24092442
"kw_only",
24102443
"inherited",
24112444
"on_setattr",
2445+
"alias",
24122446
)
24132447

24142448
def __init__(
@@ -2430,6 +2464,7 @@ def __init__(
24302464
order=None,
24312465
order_key=None,
24322466
on_setattr=None,
2467+
alias=None,
24332468
):
24342469
eq, eq_key, order, order_key = _determine_attrib_eq_order(
24352470
cmp, eq_key or eq, order_key or order, True
@@ -2463,6 +2498,7 @@ def __init__(
24632498
bound_setattr("kw_only", kw_only)
24642499
bound_setattr("inherited", inherited)
24652500
bound_setattr("on_setattr", on_setattr)
2501+
bound_setattr("alias", alias)
24662502

24672503
def __setattr__(self, name, value):
24682504
raise FrozenInstanceError()
@@ -2558,6 +2594,7 @@ def _setattrs(self, name_values_pairs):
25582594
hash=(name != "metadata"),
25592595
init=True,
25602596
inherited=False,
2597+
alias=_default_init_alias_for(name),
25612598
)
25622599
for name in Attribute.__slots__
25632600
]
@@ -2596,10 +2633,12 @@ class _CountingAttr:
25962633
"type",
25972634
"kw_only",
25982635
"on_setattr",
2636+
"alias",
25992637
)
26002638
__attrs_attrs__ = tuple(
26012639
Attribute(
26022640
name=name,
2641+
alias=_default_init_alias_for(name),
26032642
default=NOTHING,
26042643
validator=None,
26052644
repr=True,
@@ -2623,10 +2662,12 @@ class _CountingAttr:
26232662
"hash",
26242663
"init",
26252664
"on_setattr",
2665+
"alias",
26262666
)
26272667
) + (
26282668
Attribute(
26292669
name="metadata",
2670+
alias="metadata",
26302671
default=None,
26312672
validator=None,
26322673
repr=True,
@@ -2661,6 +2702,7 @@ def __init__(
26612702
order,
26622703
order_key,
26632704
on_setattr,
2705+
alias,
26642706
):
26652707
_CountingAttr.cls_counter += 1
26662708
self.counter = _CountingAttr.cls_counter
@@ -2678,6 +2720,7 @@ def __init__(
26782720
self.type = type
26792721
self.kw_only = kw_only
26802722
self.on_setattr = on_setattr
2723+
self.alias = alias
26812724

26822725
def validator(self, meth):
26832726
"""

0 commit comments

Comments
 (0)