@@ -492,6 +492,15 @@ def _transform_attrs(
492492
493493 attrs = base_attrs + own_attrs
494494
495+ # Resolve default field alias before executing field_transformer, so that
496+ # the transformer receives fully populated Attribute objects with usable
497+ # alias values.
498+ for a in attrs :
499+ if not a .alias :
500+ # Evolve is very slow, so we hold our nose and do it dirty.
501+ _OBJ_SETATTR .__get__ (a )("alias" , _default_init_alias_for (a .name ))
502+ _OBJ_SETATTR .__get__ (a )("alias_is_default" , True )
503+
495504 if field_transformer is not None :
496505 attrs = tuple (field_transformer (cls , attrs ))
497506
@@ -509,13 +518,12 @@ def _transform_attrs(
509518 if had_default is False and a .default is not NOTHING :
510519 had_default = True
511520
512- # Resolve default field alias after executing field_transformer.
513- # This allows field_transformer to differentiate between explicit vs
514- # default aliases and supply their own defaults.
521+ # Resolve default field alias for any new attributes that the
522+ # field_transformer may have added without setting an alias.
515523 for a in attrs :
516524 if not a .alias :
517- # Evolve is very slow, so we hold our nose and do it dirty.
518525 _OBJ_SETATTR .__get__ (a )("alias" , _default_init_alias_for (a .name ))
526+ _OBJ_SETATTR .__get__ (a )("alias_is_default" , True )
519527
520528 # Create AttrsClass *after* applying the field_transformer since it may
521529 # add or remove attributes!
@@ -2464,6 +2472,8 @@ class Attribute:
24642472 - ``name`` (`str`): The name of the attribute.
24652473 - ``alias`` (`str`): The __init__ parameter name of the attribute, after
24662474 any explicit overrides and default private-attribute-name handling.
2475+ - ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically
2476+ generated (``True``) or explicitly provided by the user (``False``).
24672477 - ``inherited`` (`bool`): Whether or not that attribute has been inherited
24682478 from a base class.
24692479 - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The
@@ -2489,6 +2499,7 @@ class Attribute:
24892499 equality checks and hashing anymore.
24902500 .. versionadded:: 21.1.0 *eq_key* and *order_key*
24912501 .. versionadded:: 22.2.0 *alias*
2502+ .. versionadded:: 26.1.0 *alias_is_default*
24922503
24932504 For the full version history of the fields, see `attr.ib`.
24942505 """
@@ -2513,6 +2524,7 @@ class Attribute:
25132524 "inherited" ,
25142525 "on_setattr" ,
25152526 "alias" ,
2527+ "alias_is_default" ,
25162528 )
25172529
25182530 def __init__ (
@@ -2535,6 +2547,7 @@ def __init__(
25352547 order_key = None ,
25362548 on_setattr = None ,
25372549 alias = None ,
2550+ alias_is_default = None ,
25382551 ):
25392552 eq , eq_key , order , order_key = _determine_attrib_eq_order (
25402553 cmp , eq_key or eq , order_key or order , True
@@ -2569,6 +2582,10 @@ def __init__(
25692582 bound_setattr ("inherited" , inherited )
25702583 bound_setattr ("on_setattr" , on_setattr )
25712584 bound_setattr ("alias" , alias )
2585+ bound_setattr (
2586+ "alias_is_default" ,
2587+ alias is None if alias_is_default is None else alias_is_default ,
2588+ )
25722589
25732590 def __setattr__ (self , name , value ):
25742591 raise FrozenInstanceError
@@ -2604,6 +2621,7 @@ def from_counting_attr(
26042621 ca .order_key ,
26052622 ca .on_setattr ,
26062623 ca .alias ,
2624+ ca .alias is None ,
26072625 )
26082626
26092627 # Don't use attrs.evolve since fields(Attribute) doesn't work
@@ -2622,6 +2640,20 @@ def evolve(self, **changes):
26222640
26232641 new ._setattrs (changes .items ())
26242642
2643+ if "alias" in changes and "alias_is_default" not in changes :
2644+ # Explicit alias provided -- no longer the default.
2645+ _OBJ_SETATTR .__get__ (new )("alias_is_default" , False )
2646+ elif (
2647+ "name" in changes
2648+ and "alias" not in changes
2649+ # Don't auto-generate alias if the user picked picked the old one.
2650+ and self .alias_is_default
2651+ ):
2652+ # Name changed, alias was auto-generated -- update it.
2653+ _OBJ_SETATTR .__get__ (new )(
2654+ "alias" , _default_init_alias_for (new .name )
2655+ )
2656+
26252657 return new
26262658
26272659 # Don't use _add_pickle since fields(Attribute) doesn't work
@@ -2638,6 +2670,17 @@ def __setstate__(self, state):
26382670 """
26392671 Play nice with pickle.
26402672 """
2673+ if len (state ) < len (self .__slots__ ):
2674+ # Pre-26.1.0 pickle without alias_is_default -- infer it
2675+ # heuristically.
2676+ state_dict = dict (zip (self .__slots__ , state ))
2677+ alias_is_default = state_dict .get (
2678+ "alias"
2679+ ) is None or state_dict .get ("alias" ) == _default_init_alias_for (
2680+ state_dict ["name" ]
2681+ )
2682+ state = (* state , alias_is_default )
2683+
26412684 self ._setattrs (zip (self .__slots__ , state ))
26422685
26432686 def _setattrs (self , name_values_pairs ):
@@ -2661,7 +2704,7 @@ def _setattrs(self, name_values_pairs):
26612704 name = name ,
26622705 default = NOTHING ,
26632706 validator = None ,
2664- repr = True ,
2707+ repr = ( name != "alias_is_default" ) ,
26652708 cmp = None ,
26662709 eq = True ,
26672710 order = False ,
0 commit comments