From 210d93c1fd675a4993d28eb9a4c3aaf750cef0a1 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sat, 9 Apr 2022 12:26:41 -0700 Subject: [PATCH 01/22] Spike `alias` implementation. --- src/attr/__init__.pyi | 14 +++++++ src/attr/_funcs.py | 2 +- src/attr/_make.py | 48 +++++++++++++++++++++- src/attr/_next_gen.py | 2 + tests/test_make.py | 94 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 156 insertions(+), 4 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index dfa69578b..012cd540d 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -134,6 +134,8 @@ class Attribute(Generic[_T]): type: Optional[Type[_T]] kw_only: bool on_setattr: _OnSetAttrType + alias: str + def evolve(self, **changes: Any) -> "Attribute[Any]": ... # NOTE: We had several choices for the annotation to use for type arg: @@ -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 @@ -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. @@ -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 @@ -234,6 +239,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> Any: ... @overload def field( @@ -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 @@ -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. @@ -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 @@ -305,7 +314,9 @@ 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)) def attrs( @@ -333,6 +344,7 @@ def attrs( field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., ) -> _C: ... + @overload @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) def attrs( @@ -360,6 +372,7 @@ def attrs( field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., ) -> Callable[[_C], _C]: ... + @overload @__dataclass_transform__(field_descriptors=(attrib, field)) def define( @@ -385,6 +398,7 @@ def define( field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., ) -> _C: ... + @overload @__dataclass_transform__(field_descriptors=(attrib, field)) def define( diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index 49f241d02..1f573c110 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -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 if init_name not in changes: changes[init_name] = getattr(inst, attr_name) diff --git a/src/attr/_make.py b/src/attr/_make.py index 680738151..9ee40377d 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -101,6 +101,7 @@ def attrib( eq=None, order=None, on_setattr=None, + alias=None, ): """ Create a new attribute on a class. @@ -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 unspecified, default to `name` stripped + of leading underscores. See `private-atributes` for details. .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* @@ -230,6 +234,8 @@ def attrib( .. versionchanged:: 21.1.0 *eq*, *order*, and *cmp* also accept a custom callable .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionchanged:: 21.5.0 *alias* """ eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq, order, True @@ -279,6 +285,7 @@ def attrib( order=order, order_key=order_key, on_setattr=on_setattr, + alias=alias, ) @@ -523,6 +530,14 @@ def _transform_attrs( key=lambda e: e[1].counter, ) + # TODO + # This is the location where we could resolve the default attribute alias, + # rather than the Attribute constructor. + # + # This would be required if we want to adjust the default alias behavior + # in the future with an `attr.s` option. + # + # However we could leave in Attribute constructor and just override here. own_attrs = [ Attribute.from_counting_attr( name=attr_name, ca=ca, type=anns.get(attr_name) @@ -563,6 +578,11 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) + # TODO ...or resolve default init aliases here. + # This would have the advantage of leaving alias=None for field_transformer + # implementations, so that transformers can differentiate between explicit + # vs default aliases and supply their own defaults. + # Create AttrsClass *after* applying the field_transformer since it may # add or remove attributes! attr_names = [a.name for a in attrs] @@ -586,6 +606,7 @@ def _frozen_setattrs(self, name, value): raise FrozenInstanceError() + else: def _frozen_setattrs(self, name, value): @@ -2165,7 +2186,7 @@ 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("_") + arg_name = a.alias has_factory = isinstance(a.default, Factory) if has_factory and a.default.takes_self: @@ -2358,6 +2379,16 @@ 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. @@ -2388,6 +2419,7 @@ class Attribute: .. 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`. """ @@ -2409,6 +2441,7 @@ class Attribute: "kw_only", "inherited", "on_setattr", + "alias", ) def __init__( @@ -2430,6 +2463,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 @@ -2463,6 +2497,14 @@ def __init__( bound_setattr("kw_only", kw_only) bound_setattr("inherited", inherited) bound_setattr("on_setattr", on_setattr) + # TODO + # This location for default init aliasing feels "correct" if external + # users would expect to get the "default" aliasing behavior, while + # preserving backward compatability with any callers who ignored the + # big red warning and directly instantiated Attribute. + bound_setattr( + "alias", alias if alias else _default_init_alias_for(name) + ) def __setattr__(self, name, value): raise FrozenInstanceError() @@ -2596,6 +2638,7 @@ class _CountingAttr: "type", "kw_only", "on_setattr", + "alias", ) __attrs_attrs__ = tuple( Attribute( @@ -2623,6 +2666,7 @@ class _CountingAttr: "hash", "init", "on_setattr", + "alias", ) ) + ( Attribute( @@ -2661,6 +2705,7 @@ def __init__( order, order_key, on_setattr, + alias, ): _CountingAttr.cls_counter += 1 self.counter = _CountingAttr.cls_counter @@ -2678,6 +2723,7 @@ def __init__( self.type = type self.kw_only = kw_only self.on_setattr = on_setattr + self.alias = alias def validator(self, meth): """ diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 260519f1c..79e8a44dc 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -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 @@ -189,6 +190,7 @@ def field( eq=eq, order=order, on_setattr=on_setattr, + alias=alias, ) diff --git a/tests/test_make.py b/tests/test_make.py index 119bbe8a8..6adf42214 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -223,7 +223,7 @@ class C: "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)", + "kw_only=False, inherited=False, on_setattr=None, alias='y')", ) == e.value.args def test_kw_only(self): @@ -1729,6 +1729,97 @@ def __setstate__(self, state): assert actual == expected +class TestInitAlias: + """ + Tests for Attribute alias handling. + """ + + def test_default_and_specify(self): + + # alias is None by default on _CountingAttr + default_counting = attr.ib() + assert default_counting.alias is None + + override_counting = attr.ib(alias="specified") + assert override_counting.alias == "specified" + + @attr.s + class Cases: + public_default = attr.ib() + _private_default = attr.ib() + __dunder_default__= attr.ib() + + public_override = attr.ib(alias="public") + _private_override = attr.ib(alias="_private") + __dunder_override__ = attr.ib(alias="__dunder__") + + + cases = attr.fields_dict(Cases) + + # Default applies private-name mangling logic + assert cases["public_default"].name == "public_default" + assert cases["public_default"].alias == "public_default" + + assert cases["_private_default"].name == "_private_default" + assert cases["_private_default"].alias == "private_default" + + assert cases["__dunder_default__"].name == "__dunder_default__" + assert cases["__dunder_default__"].alias == "dunder_default__" + + # Override is passed through + assert cases["public_override"].name == "public_override" + assert cases["public_override"].alias == "public" + + assert cases["_private_override"].name == "_private_override" + assert cases["_private_override"].alias == "_private" + + assert cases["__dunder_override__"].name == "__dunder_override__" + assert cases["__dunder_override__"].alias == "__dunder__" + + # And aliases are applied to the __init__ signature + example = Cases( + public_default=1, + private_default=2, + dunder_default__=3, + public=4, + _private=5, + __dunder__=6, + ) + + assert example.public_default == 1 + assert example._private_default == 2 + assert example.__dunder_default__ == 3 + assert example.public_override == 4 + assert example._private_override == 5 + assert example.__dunder_override__ == 6 + + def test_evolve(self): + + @attr.s + class EvolveCase: + _override = attr.ib(alias="_override") + __mangled = attr.ib() + __dunder__ = attr.ib() + + org = EvolveCase(1, 2, 3) + + # Previous behavior of evolve as broken for double-underscore passthrough, + # and would raise here due to mis-mapping the __dunder__ alias + assert attr.evolve(org) == org + + # evolve uses the alias to match __init__ signature + assert attr.evolve( + org, + _override = 0, + ) == EvolveCase(0, 2, 3) + + # and properly passes through dunders and mangles + assert attr.evolve( + org, + EvolveCase__mangled = 4, + dunder__ = 5, + ) == EvolveCase(1, 4, 5) + class TestMakeOrder: """ @@ -1778,7 +1869,6 @@ class B(A): with pytest.raises(TypeError): a > b - class TestDetermineAttrsEqOrder: def test_default(self): """ From fe28e2286e8e91f31dc4737bf9f45532f0d93dc9 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sat, 9 Apr 2022 13:21:09 -0700 Subject: [PATCH 02/22] Move default alias init to after field_transformer. --- docs/extending.rst | 21 +++++++++++++++++++++ src/attr/__init__.pyi | 2 +- src/attr/_make.py | 38 +++++++++++++++----------------------- tests/test_functional.py | 4 ++++ tests/test_hooks.py | 18 ++++++++++++++++++ tests/test_make.py | 2 +- tests/utils.py | 3 ++- 7 files changed, 62 insertions(+), 26 deletions(-) diff --git a/docs/extending.rst b/docs/extending.rst index 13d66edf3..154ce22d5 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -260,6 +260,27 @@ 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 alternative default field names. +Noting, that ``field_transformer`` receives `attr.Attribute` instances before private-attribute handling. + +.. 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: float + ... + >>> Data(public=42, _private="spam") + Data(public=42, _privatge='spam') + Customize Value Serialization in ``asdict()`` --------------------------------------------- diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 012cd540d..69fca81bc 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -134,7 +134,7 @@ class Attribute(Generic[_T]): type: Optional[Type[_T]] kw_only: bool on_setattr: _OnSetAttrType - alias: str + alias: Optional[str] def evolve(self, **changes: Any) -> "Attribute[Any]": ... diff --git a/src/attr/_make.py b/src/attr/_make.py index 9ee40377d..b1b6a0f54 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -211,7 +211,7 @@ def attrib( `attrs.setters.NO_OP` :param Optional[str] alias: Override this attribute's parameter name in the generated ``__init__`` method. If unspecified, default to `name` stripped - of leading underscores. See `private-atributes` for details. + of leading underscores. See `private-attributes` for details. .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* @@ -234,7 +234,6 @@ def attrib( .. versionchanged:: 21.1.0 *eq*, *order*, and *cmp* also accept a custom callable .. versionchanged:: 21.1.0 *cmp* undeprecated - .. versionchanged:: 21.1.0 *cmp* undeprecated .. versionchanged:: 21.5.0 *alias* """ eq, eq_key, order, order_key = _determine_attrib_eq_order( @@ -530,14 +529,6 @@ def _transform_attrs( key=lambda e: e[1].counter, ) - # TODO - # This is the location where we could resolve the default attribute alias, - # rather than the Attribute constructor. - # - # This would be required if we want to adjust the default alias behavior - # in the future with an `attr.s` option. - # - # However we could leave in Attribute constructor and just override here. own_attrs = [ Attribute.from_counting_attr( name=attr_name, ca=ca, type=anns.get(attr_name) @@ -578,10 +569,13 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) - # TODO ...or resolve default init aliases here. - # This would have the advantage of leaving alias=None for field_transformer - # implementations, so that transformers can differentiate between explicit - # vs default aliases and supply their own defaults. + # 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! @@ -606,7 +600,6 @@ def _frozen_setattrs(self, name, value): raise FrozenInstanceError() - else: def _frozen_setattrs(self, name, value): @@ -2186,6 +2179,7 @@ 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 ) + assert a.alias, "requires known field alias, should have been set via _ClassBuilder" arg_name = a.alias has_factory = isinstance(a.default, Factory) @@ -2413,6 +2407,8 @@ class Attribute: - Validators get them passed as the first argument. - The :ref:`field transformer ` 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* @@ -2497,14 +2493,7 @@ def __init__( bound_setattr("kw_only", kw_only) bound_setattr("inherited", inherited) bound_setattr("on_setattr", on_setattr) - # TODO - # This location for default init aliasing feels "correct" if external - # users would expect to get the "default" aliasing behavior, while - # preserving backward compatability with any callers who ignored the - # big red warning and directly instantiated Attribute. - bound_setattr( - "alias", alias if alias else _default_init_alias_for(name) - ) + bound_setattr("alias", alias) def __setattr__(self, name, value): raise FrozenInstanceError() @@ -2600,6 +2589,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__ ] @@ -2643,6 +2633,7 @@ class _CountingAttr: __attrs_attrs__ = tuple( Attribute( name=name, + alias=_default_init_alias_for(name), default=NOTHING, validator=None, repr=True, @@ -2671,6 +2662,7 @@ class _CountingAttr: ) + ( Attribute( name="metadata", + alias="metadata", default=None, validator=None, repr=True, diff --git a/tests/test_functional.py b/tests/test_functional.py index dbb834777..617266c1e 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -119,6 +119,7 @@ def test_fields(self, cls): assert ( Attribute( name="x", + alias="x", default=foo, validator=None, repr=True, @@ -131,6 +132,7 @@ def test_fields(self, cls): ), Attribute( name="y", + alias="y", default=attr.Factory(list), validator=None, repr=True, @@ -188,6 +190,7 @@ def test_programmatic(self, slots, frozen): assert ( Attribute( name="a", + alias="a", default=NOTHING, validator=None, repr=True, @@ -200,6 +203,7 @@ def test_programmatic(self, slots, frozen): ), Attribute( name="b", + alias="b", default=NOTHING, validator=None, repr=True, diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 92fc2dcaa..dba4c7646 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -99,6 +99,24 @@ class C: assert attr.asdict(C(1, 2)) == {"x": 1, "new": 2} + def test_hook_override_alias(self): + """ + It is possible to set field alias via hook + """ + + def use_dataclass_names(cls, attribs): + return [ + a.evolve(alias=a.name) for a in attribs + ] + + @attr.s(auto_attribs=True, field_transformer=use_dataclass_names) + class NameCase: + public: int + _private: int + __dunder__: int + + assert NameCase(public=1, _private=2, __dunder__=3) == NameCase(1, 2, 3) + def test_hook_with_inheritance(self): """ The hook receives all fields from base classes. diff --git a/tests/test_make.py b/tests/test_make.py index 6adf42214..ff9573f6c 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -223,7 +223,7 @@ class C: "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')", + "kw_only=False, inherited=False, on_setattr=None, alias=None)", ) == e.value.args def test_kw_only(self): diff --git a/tests/utils.py b/tests/utils.py index 261d15b65..866d2fa53 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,7 @@ from attr import Attribute -from attr._make import NOTHING, make_class +from attr._make import NOTHING, make_class, _default_init_alias_for def simple_class( @@ -64,4 +64,5 @@ def simple_attr( converter=converter, kw_only=kw_only, inherited=inherited, + alias=_default_init_alias_for(name), ) From 740c3384bd52be029429cd294e2176fff7c9456c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 9 Apr 2022 20:23:44 +0000 Subject: [PATCH 03/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/attr/__init__.pyi | 4 ---- src/attr/_make.py | 4 +++- tests/test_hooks.py | 8 ++++---- tests/test_make.py | 12 ++++++------ tests/utils.py | 2 +- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 69fca81bc..7aff65c3c 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -316,7 +316,6 @@ def field( on_setattr: Optional[_OnSetAttrArgType] = ..., alias: Optional[str] = ..., ) -> Any: ... - @overload @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) def attrs( @@ -344,7 +343,6 @@ def attrs( field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., ) -> _C: ... - @overload @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) def attrs( @@ -372,7 +370,6 @@ def attrs( field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., ) -> Callable[[_C], _C]: ... - @overload @__dataclass_transform__(field_descriptors=(attrib, field)) def define( @@ -398,7 +395,6 @@ def define( field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., ) -> _C: ... - @overload @__dataclass_transform__(field_descriptors=(attrib, field)) def define( diff --git a/src/attr/_make.py b/src/attr/_make.py index b1b6a0f54..5b1b309b3 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2179,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 ) - assert a.alias, "requires known field alias, should have been set via _ClassBuilder" + assert ( + a.alias + ), "requires known field alias, should have been set via _ClassBuilder" arg_name = a.alias has_factory = isinstance(a.default, Factory) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index dba4c7646..54a27fb1a 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -105,9 +105,7 @@ def test_hook_override_alias(self): """ def use_dataclass_names(cls, attribs): - return [ - a.evolve(alias=a.name) for a in attribs - ] + return [a.evolve(alias=a.name) for a in attribs] @attr.s(auto_attribs=True, field_transformer=use_dataclass_names) class NameCase: @@ -115,7 +113,9 @@ class NameCase: _private: int __dunder__: int - assert NameCase(public=1, _private=2, __dunder__=3) == NameCase(1, 2, 3) + assert NameCase(public=1, _private=2, __dunder__=3) == NameCase( + 1, 2, 3 + ) def test_hook_with_inheritance(self): """ diff --git a/tests/test_make.py b/tests/test_make.py index ff9573f6c..87f004ccd 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -1729,6 +1729,7 @@ def __setstate__(self, state): assert actual == expected + class TestInitAlias: """ Tests for Attribute alias handling. @@ -1747,13 +1748,12 @@ def test_default_and_specify(self): class Cases: public_default = attr.ib() _private_default = attr.ib() - __dunder_default__= attr.ib() + __dunder_default__ = attr.ib() public_override = attr.ib(alias="public") _private_override = attr.ib(alias="_private") __dunder_override__ = attr.ib(alias="__dunder__") - cases = attr.fields_dict(Cases) # Default applies private-name mangling logic @@ -1794,7 +1794,6 @@ class Cases: assert example.__dunder_override__ == 6 def test_evolve(self): - @attr.s class EvolveCase: _override = attr.ib(alias="_override") @@ -1810,14 +1809,14 @@ class EvolveCase: # evolve uses the alias to match __init__ signature assert attr.evolve( org, - _override = 0, + _override=0, ) == EvolveCase(0, 2, 3) # and properly passes through dunders and mangles assert attr.evolve( org, - EvolveCase__mangled = 4, - dunder__ = 5, + EvolveCase__mangled=4, + dunder__=5, ) == EvolveCase(1, 4, 5) @@ -1869,6 +1868,7 @@ class B(A): with pytest.raises(TypeError): a > b + class TestDetermineAttrsEqOrder: def test_default(self): """ diff --git a/tests/utils.py b/tests/utils.py index 866d2fa53..9e678f05f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,7 @@ from attr import Attribute -from attr._make import NOTHING, make_class, _default_init_alias_for +from attr._make import NOTHING, _default_init_alias_for, make_class def simple_class( From 8361211f36d0189f11f84ebafabf44bae2342ff4 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sat, 9 Apr 2022 13:34:32 -0700 Subject: [PATCH 04/22] Fixup docs. --- docs/init.rst | 1 + src/attr/_make.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/init.rst b/docs/init.rst index 33cfde3d9..e60dc9d56 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -47,6 +47,7 @@ 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 `_ or our `third-party extensions `_. +.. _private_attributes: Private Attributes ------------------ diff --git a/src/attr/_make.py b/src/attr/_make.py index 5b1b309b3..ec568f0a5 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -210,8 +210,8 @@ def attrib( :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 unspecified, default to `name` stripped - of leading underscores. See `private-attributes` for details. + generated ``__init__`` method. If unspecified, default to ``name`` stripped + of leading underscores. See `private_attributes`. .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* From 0226f7274e4814461a16bb8a88a08a83ad9bb30c Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sat, 9 Apr 2022 13:35:07 -0700 Subject: [PATCH 05/22] Update docs/extending.rst --- docs/extending.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extending.rst b/docs/extending.rst index 154ce22d5..c162a6e99 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -279,7 +279,7 @@ Noting, that ``field_transformer`` receives `attr.Attribute` instances before pr ... _private: float ... >>> Data(public=42, _private="spam") - Data(public=42, _privatge='spam') + Data(public=42, _private='spam') Customize Value Serialization in ``asdict()`` From 507c8c0322ec8fcf3f0dbe4aff73024780e2bb56 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sat, 9 Apr 2022 13:36:24 -0700 Subject: [PATCH 06/22] Pre-commit fixes --- src/attr/_make.py | 4 ++-- tests/test_make.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index ec568f0a5..d6e6a2aa4 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -210,8 +210,8 @@ def attrib( :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 unspecified, default to ``name`` stripped - of leading underscores. See `private_attributes`. + 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* diff --git a/tests/test_make.py b/tests/test_make.py index 87f004ccd..0f818d5b5 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -1802,8 +1802,9 @@ class EvolveCase: org = EvolveCase(1, 2, 3) - # Previous behavior of evolve as broken for double-underscore passthrough, - # and would raise here due to mis-mapping the __dunder__ alias + # Previous behavior of evolve as broken for double-underscore + # passthrough, and would raise here due to mis-mapping the __dunder__ + # alias assert attr.evolve(org) == org # evolve uses the alias to match __init__ signature From 470e9956843294a5c5e357c0024ea8ea639f92fd Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sat, 9 Apr 2022 13:47:48 -0700 Subject: [PATCH 07/22] Partially fix doctest --- docs/api.rst | 10 +++++----- docs/extending.rst | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 9dff7a54a..59d006f33 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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 @@ -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 @@ -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 diff --git a/docs/extending.rst b/docs/extending.rst index c162a6e99..38b8fa048 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -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=, 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=, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='a'),) .. warning:: @@ -261,7 +261,7 @@ A more realistic example would be to automatically convert data that you, e.g., Data(a=3, b='spam', c=datetime.datetime(2020, 5, 4, 13, 37)) Or, perhaps you would prefer alternative default field names. -Noting, that ``field_transformer`` receives `attr.Attribute` instances before private-attribute handling. +Noting, that ``field_transformer`` receives `attrs.Attribute` instances before private-attribute handling. .. doctest:: From aa0c11feee96a8468525f9283224eff731df494d Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sat, 9 Apr 2022 13:57:33 -0700 Subject: [PATCH 08/22] Add test docstrings. --- tests/test_make.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_make.py b/tests/test_make.py index 0f818d5b5..0fae62d7d 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -1736,6 +1736,12 @@ class TestInitAlias: """ def test_default_and_specify(self): + """ + alias is present on the Attributes returned from attr.fields. + + If left unspecified, it defaults to standard private-attribute + handling. If specified, it passes through the explicit alias. + """ # alias is None by default on _CountingAttr default_counting = attr.ib() @@ -1794,6 +1800,10 @@ class Cases: assert example.__dunder_override__ == 6 def test_evolve(self): + """ + attr.evolve uses Attribute.alias to determine parameter names. + """ + @attr.s class EvolveCase: _override = attr.ib(alias="_override") From 95b95d0e9ebe2bd8e74140fce639b7af547bd16c Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sat, 9 Apr 2022 14:03:18 -0700 Subject: [PATCH 09/22] Add typing_example tests --- tests/typing_example.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/typing_example.py b/tests/typing_example.py index 012a190cc..f32cda7af 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -119,6 +119,15 @@ class Error2(Exception): e.args str(e) +# Field aliases + +@attr.s +class AliasExample: + x = attr.ib() + _private = attr.ib(alias="_private") + +attr.fields(AliasExample).x.alias +attr.fields(AliasExample)._private.alias # Converters # XXX: Currently converters can only be functions so none of this works From ec4c7c0537befcdaa7fdbcfe61f1f782b2249ce6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 9 Apr 2022 21:03:43 +0000 Subject: [PATCH 10/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/typing_example.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/typing_example.py b/tests/typing_example.py index f32cda7af..4feb7cae1 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -121,11 +121,13 @@ class Error2(Exception): # Field aliases + @attr.s class AliasExample: x = attr.ib() _private = attr.ib(alias="_private") + attr.fields(AliasExample).x.alias attr.fields(AliasExample)._private.alias From e2ed777845b7bdb25ecc8b99145ad2202918a018 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 10 Apr 2022 17:26:50 -0700 Subject: [PATCH 11/22] Tidy typing_example --- tests/typing_example.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/typing_example.py b/tests/typing_example.py index 4feb7cae1..b4b617e0e 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -122,14 +122,14 @@ class Error2(Exception): # Field aliases -@attr.s +@attrs.define class AliasExample: - x = attr.ib() - _private = attr.ib(alias="_private") + without_alias: int + _with_alias: int = attr.ib(alias="_with_alias") -attr.fields(AliasExample).x.alias -attr.fields(AliasExample)._private.alias +attr.fields(AliasExample).without_alias.alias +attr.fields(AliasExample)._with_alias.alias # Converters # XXX: Currently converters can only be functions so none of this works From 746f9ca94134394a0b319e8399ce60e47c09b55d Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 08:26:02 -0800 Subject: [PATCH 12/22] Add note in init.rst on private aliases --- docs/init.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/init.rst b/docs/init.rst index e60dc9d56..525396839 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -49,7 +49,7 @@ Embrace functions and classmethods as a filter between reality and what's best f .. _private_attributes: -Private Attributes +Private Attributes and Aliases ------------------ One thing people tend to find confusing is the treatment of private attributes that start with an underscore. @@ -79,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__) + None> + Defaults -------- From a80dfba9b44ab4d3ea697a93594cbef74b21fd07 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 08:33:09 -0800 Subject: [PATCH 13/22] Add alias example to examples.rst --- docs/examples.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/examples.rst b/docs/examples.rst index ab9fe2d03..558a9fa19 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -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?): From 859886f1131fbab4f43aefd9e8681b9f1cde3326 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 08:53:14 -0800 Subject: [PATCH 14/22] Assert to comment --- src/attr/_make.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index d6e6a2aa4..f6437d771 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2179,9 +2179,8 @@ 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 ) - assert ( - a.alias - ), "requires known field alias, should have been set via _ClassBuilder" + # 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) @@ -2376,7 +2375,8 @@ def fmt_setter_with_converter( def _default_init_alias_for(name: str) -> str: - """The default __init__ parameter name for a field. + """ + 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. From 5de8c67e29b4c4d1ac61e2ef0c71ef19ccbd1b4b Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 08:53:23 -0800 Subject: [PATCH 15/22] Add changelog entry --- changelog.d/950.change.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/950.change.rst diff --git a/changelog.d/950.change.rst b/changelog.d/950.change.rst new file mode 100644 index 000000000..cc6480156 --- /dev/null +++ b/changelog.d/950.change.rst @@ -0,0 +1,4 @@ +``attrs.field`` now supports an ``alias`` option for explicit ``__init__`` argument names. + +The `PEP 681 compatible `_ ``alias`` option can be use to override private attribute name mangling, +or add other arbitrary field argument name overrides. From f4879a7a8e326705f885e1240e80d72a94a7a1aa Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 09:06:49 -0800 Subject: [PATCH 16/22] Fixup doc error --- docs/init.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/init.rst b/docs/init.rst index 525396839..aa4cdc0c2 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -50,7 +50,7 @@ Embrace functions and classmethods as a filter between reality and what's best f .. _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: From 12bdbc5432b8d3b1a6b53cd66146f5e6f82b8178 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 09:18:26 -0800 Subject: [PATCH 17/22] Tidy dataclass_transform docs --- docs/extending.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/extending.rst b/docs/extending.rst index 38b8fa048..e6d8cc682 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -260,8 +260,8 @@ 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 alternative default field names. -Noting, that ``field_transformer`` receives `attrs.Attribute` instances before private-attribute handling. +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:: @@ -276,10 +276,11 @@ Noting, that ``field_transformer`` receives `attrs.Attribute` instances before p >>> @frozen(field_transformer=dataclass_names) ... class Data: ... public: int - ... _private: float + ... _private: str + ... explicit: str = field(alias="aliased") ... - >>> Data(public=42, _private="spam") - Data(public=42, _private='spam') + >>> Data(public=42, _private="spam", aliased="yes") + Data(public=42, _private='spam', aliased='yes') Customize Value Serialization in ``asdict()`` From ce4746eb69568e5ddf1dafc294b4b8796de90d7e Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 09:27:20 -0800 Subject: [PATCH 18/22] Lil' spice for the changelog. --- changelog.d/950.change.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.d/950.change.rst b/changelog.d/950.change.rst index cc6480156..714623fc4 100644 --- a/changelog.d/950.change.rst +++ b/changelog.d/950.change.rst @@ -1,4 +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 `_ ``alias`` option can be use to override private attribute name mangling, or add other arbitrary field argument name overrides. From 0f2879d203a8d0031b599d5733c094d0379e272c Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 09:33:09 -0800 Subject: [PATCH 19/22] Fix doctest --- docs/init.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/init.rst b/docs/init.rst index aa4cdc0c2..3ba49f0ba 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -91,7 +91,7 @@ This can be used to override private attribute handling, or make other arbitrary ... _x: int = field(alias="_x") ... y: int = field(alias="distasteful_y") >>> inspect.signature(C.__init__) - None> + None> Defaults From 395b04c8ba8eab6142ee2088ec3908782b7a845d Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 09:37:44 -0800 Subject: [PATCH 20/22] Update extending.rst --- docs/extending.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/extending.rst b/docs/extending.rst index e6d8cc682..7fcebec24 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -277,10 +277,10 @@ Note, ``field_transformer`` operates on `attrs.Attribute` instances before the d ... class Data: ... public: int ... _private: str - ... explicit: str = field(alias="aliased") + ... explicit: str = field(alias="aliased_name") ... - >>> Data(public=42, _private="spam", aliased="yes") - Data(public=42, _private='spam', aliased='yes') + >>> Data(public=42, _private="spam", aliased_name="yes") + Data(public=42, _private='spam', explicit='yes') Customize Value Serialization in ``asdict()`` From cfa4eb778e5e426a5cccd69b3cab5ed4f85c5d1a Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Nov 2022 09:52:20 -0800 Subject: [PATCH 21/22] Make alias introspection more explicit --- src/attr/_make.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index f6437d771..69ad64cc7 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2394,6 +2394,8 @@ class Attribute: following: - ``name`` (`str`): The name of the attribute. + - ``alias`` (`str`): The __init__ parameter name of the attribute, after + any explicit overrides and default private-attribute-name handling. - ``inherited`` (`bool`): Whether or not that attribute has been inherited from a base class. - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables @@ -2409,8 +2411,9 @@ class Attribute: - Validators get them passed as the first argument. - The :ref:`field transformer ` hook receives a list of them. - - ``alias`` exposes the __init__ parameter name of the field, including - with overrides or default private-attribute handling. + - The ``alias`` property exposes the __init__ parameter name of the field, + with any overrides and default private-attribute handling applied. + .. versionadded:: 20.1.0 *inherited* .. versionadded:: 20.1.0 *on_setattr* From 4c63ec147323888f1e8901845bd6c11dc2c34246 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 30 Nov 2022 15:35:38 +0100 Subject: [PATCH 22/22] Update src/attr/_make.py --- src/attr/_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 69ad64cc7..4e6846aaa 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -234,7 +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* + .. versionadded:: 22.2.0 *alias* """ eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq, order, True