1818 'is_dataclass' ,
1919 ]
2020
21+ # Conditions for adding methods. The boxes indicate what action the
22+ # dataclass decorator takes. For all of these tables, when I talk
23+ # about init=, repr=, eq=, order=, hash=, or frozen=, I'm referring
24+ # to the arguments to the @dataclass decorator. When checking if a
25+ # dunder method already exists, I mean check for an entry in the
26+ # class's __dict__. I never check to see if an attribute is defined
27+ # in a base class.
28+
29+ # Key:
30+ # +=========+=========================================+
31+ # + Value | Meaning |
32+ # +=========+=========================================+
33+ # | <blank> | No action: no method is added. |
34+ # +---------+-----------------------------------------+
35+ # | add | Generated method is added. |
36+ # +---------+-----------------------------------------+
37+ # | add* | Generated method is added only if the |
38+ # | | existing attribute is None and if the |
39+ # | | user supplied a __eq__ method in the |
40+ # | | class definition. |
41+ # +---------+-----------------------------------------+
42+ # | raise | TypeError is raised. |
43+ # +---------+-----------------------------------------+
44+ # | None | Attribute is set to None. |
45+ # +=========+=========================================+
46+
47+ # __init__
48+ #
49+ # +--- init= parameter
50+ # |
51+ # v | | |
52+ # | no | yes | <--- class has __init__ in __dict__?
53+ # +=======+=======+=======+
54+ # | False | | |
55+ # +-------+-------+-------+
56+ # | True | add | | <- the default
57+ # +=======+=======+=======+
58+
59+ # __repr__
60+ #
61+ # +--- repr= parameter
62+ # |
63+ # v | | |
64+ # | no | yes | <--- class has __repr__ in __dict__?
65+ # +=======+=======+=======+
66+ # | False | | |
67+ # +-------+-------+-------+
68+ # | True | add | | <- the default
69+ # +=======+=======+=======+
70+
71+
72+ # __setattr__
73+ # __delattr__
74+ #
75+ # +--- frozen= parameter
76+ # |
77+ # v | | |
78+ # | no | yes | <--- class has __setattr__ or __delattr__ in __dict__?
79+ # +=======+=======+=======+
80+ # | False | | | <- the default
81+ # +-------+-------+-------+
82+ # | True | add | raise |
83+ # +=======+=======+=======+
84+ # Raise because not adding these methods would break the "frozen-ness"
85+ # of the class.
86+
87+ # __eq__
88+ #
89+ # +--- eq= parameter
90+ # |
91+ # v | | |
92+ # | no | yes | <--- class has __eq__ in __dict__?
93+ # +=======+=======+=======+
94+ # | False | | |
95+ # +-------+-------+-------+
96+ # | True | add | | <- the default
97+ # +=======+=======+=======+
98+
99+ # __lt__
100+ # __le__
101+ # __gt__
102+ # __ge__
103+ #
104+ # +--- order= parameter
105+ # |
106+ # v | | |
107+ # | no | yes | <--- class has any comparison method in __dict__?
108+ # +=======+=======+=======+
109+ # | False | | | <- the default
110+ # +-------+-------+-------+
111+ # | True | add | raise |
112+ # +=======+=======+=======+
113+ # Raise because to allow this case would interfere with using
114+ # functools.total_ordering.
115+
116+ # __hash__
117+
118+ # +------------------- hash= parameter
119+ # | +----------- eq= parameter
120+ # | | +--- frozen= parameter
121+ # | | |
122+ # v v v | | |
123+ # | no | yes | <--- class has __hash__ in __dict__?
124+ # +=========+=======+=======+========+========+
125+ # | 1 None | False | False | | | No __eq__, use the base class __hash__
126+ # +---------+-------+-------+--------+--------+
127+ # | 2 None | False | True | | | No __eq__, use the base class __hash__
128+ # +---------+-------+-------+--------+--------+
129+ # | 3 None | True | False | None | | <-- the default, not hashable
130+ # +---------+-------+-------+--------+--------+
131+ # | 4 None | True | True | add | add* | Frozen, so hashable
132+ # +---------+-------+-------+--------+--------+
133+ # | 5 False | False | False | | |
134+ # +---------+-------+-------+--------+--------+
135+ # | 6 False | False | True | | |
136+ # +---------+-------+-------+--------+--------+
137+ # | 7 False | True | False | | |
138+ # +---------+-------+-------+--------+--------+
139+ # | 8 False | True | True | | |
140+ # +---------+-------+-------+--------+--------+
141+ # | 9 True | False | False | add | add* | Has no __eq__, but hashable
142+ # +---------+-------+-------+--------+--------+
143+ # |10 True | False | True | add | add* | Has no __eq__, but hashable
144+ # +---------+-------+-------+--------+--------+
145+ # |11 True | True | False | add | add* | Not frozen, but hashable
146+ # +---------+-------+-------+--------+--------+
147+ # |12 True | True | True | add | add* | Frozen, so hashable
148+ # +=========+=======+=======+========+========+
149+ # For boxes that are blank, __hash__ is untouched and therefore
150+ # inherited from the base class. If the base is object, then
151+ # id-based hashing is used.
152+ # Note that a class may have already __hash__=None if it specified an
153+ # __eq__ method in the class body (not one that was created by
154+ # @dataclass).
155+
156+
21157# Raised when an attempt is made to modify a frozen class.
22158class FrozenInstanceError (AttributeError ): pass
23159
@@ -143,13 +279,13 @@ def _tuple_str(obj_name, fields):
143279 # return "(self.x,self.y)".
144280
145281 # Special case for the 0-tuple.
146- if len ( fields ) == 0 :
282+ if not fields :
147283 return '()'
148284 # Note the trailing comma, needed if this turns out to be a 1-tuple.
149285 return f'({ "," .join ([f"{ obj_name } .{ f .name } " for f in fields ])} ,)'
150286
151287
152- def _create_fn (name , args , body , globals = None , locals = None ,
288+ def _create_fn (name , args , body , * , globals = None , locals = None ,
153289 return_type = MISSING ):
154290 # Note that we mutate locals when exec() is called. Caller beware!
155291 if locals is None :
@@ -287,7 +423,7 @@ def _init_fn(fields, frozen, has_post_init, self_name):
287423 body_lines += [f'{ self_name } .{ _POST_INIT_NAME } ({ params_str } )' ]
288424
289425 # If no body lines, use 'pass'.
290- if len ( body_lines ) == 0 :
426+ if not body_lines :
291427 body_lines = ['pass' ]
292428
293429 locals = {f'_type_{ f .name } ' : f .type for f in fields }
@@ -329,32 +465,6 @@ def _cmp_fn(name, op, self_tuple, other_tuple):
329465 'return NotImplemented' ])
330466
331467
332- def _set_eq_fns (cls , fields ):
333- # Create and set the equality comparison methods on cls.
334- # Pre-compute self_tuple and other_tuple, then re-use them for
335- # each function.
336- self_tuple = _tuple_str ('self' , fields )
337- other_tuple = _tuple_str ('other' , fields )
338- for name , op in [('__eq__' , '==' ),
339- ('__ne__' , '!=' ),
340- ]:
341- _set_attribute (cls , name , _cmp_fn (name , op , self_tuple , other_tuple ))
342-
343-
344- def _set_order_fns (cls , fields ):
345- # Create and set the ordering methods on cls.
346- # Pre-compute self_tuple and other_tuple, then re-use them for
347- # each function.
348- self_tuple = _tuple_str ('self' , fields )
349- other_tuple = _tuple_str ('other' , fields )
350- for name , op in [('__lt__' , '<' ),
351- ('__le__' , '<=' ),
352- ('__gt__' , '>' ),
353- ('__ge__' , '>=' ),
354- ]:
355- _set_attribute (cls , name , _cmp_fn (name , op , self_tuple , other_tuple ))
356-
357-
358468def _hash_fn (fields ):
359469 self_tuple = _tuple_str ('self' , fields )
360470 return _create_fn ('__hash__' ,
@@ -431,20 +541,20 @@ def _find_fields(cls):
431541 # a Field(), then it contains additional info beyond (and
432542 # possibly including) the actual default value. Pseudo-fields
433543 # ClassVars and InitVars are included, despite the fact that
434- # they're not real fields. That's deal with later.
544+ # they're not real fields. That's dealt with later.
435545
436546 annotations = getattr (cls , '__annotations__' , {})
437-
438547 return [_get_field (cls , a_name , a_type )
439548 for a_name , a_type in annotations .items ()]
440549
441550
442- def _set_attribute (cls , name , value ):
443- # Raise TypeError if an attribute by this name already exists.
551+ def _set_new_attribute (cls , name , value ):
552+ # Never overwrites an existing attribute. Returns True if the
553+ # attribute already exists.
444554 if name in cls .__dict__ :
445- raise TypeError (f'Cannot overwrite attribute { name } '
446- f'in { cls .__name__ } ' )
555+ return True
447556 setattr (cls , name , value )
557+ return False
448558
449559
450560def _process_class (cls , repr , eq , order , hash , init , frozen ):
@@ -495,6 +605,9 @@ def _process_class(cls, repr, eq, order, hash, init, frozen):
495605 # be inherited down.
496606 is_frozen = frozen or cls .__setattr__ is _frozen_setattr
497607
608+ # Was this class defined with an __eq__? Used in __hash__ logic.
609+ auto_hash_test = '__eq__' in cls .__dict__ and getattr (cls .__dict__ , '__hash__' , MISSING ) is None
610+
498611 # If we're generating ordering methods, we must be generating
499612 # the eq methods.
500613 if order and not eq :
@@ -505,62 +618,91 @@ def _process_class(cls, repr, eq, order, hash, init, frozen):
505618 has_post_init = hasattr (cls , _POST_INIT_NAME )
506619
507620 # Include InitVars and regular fields (so, not ClassVars).
508- _set_attribute ( cls , '__init__' ,
509- _init_fn ( list ( filter ( lambda f : f . _field_type
510- in ( _FIELD , _FIELD_INITVAR ) ,
511- fields . values ())) ,
512- is_frozen ,
513- has_post_init ,
514- # The name to use for the "self" param
515- # in __init__. Use "self" if possible.
516- '__dataclass_self__' if 'self' in fields
517- else 'self' ,
518- ))
621+ flds = [ f for f in fields . values ()
622+ if f . _field_type in ( _FIELD , _FIELD_INITVAR )]
623+ _set_new_attribute ( cls , '__init__' ,
624+ _init_fn ( flds ,
625+ is_frozen ,
626+ has_post_init ,
627+ # The name to use for the "self" param
628+ # in __init__. Use "self" if possible.
629+ '__dataclass_self__' if 'self' in fields
630+ else 'self' ,
631+ ))
519632
520633 # Get the fields as a list, and include only real fields. This is
521634 # used in all of the following methods.
522- field_list = list (filter (lambda f : f ._field_type is _FIELD ,
523- fields .values ()))
635+ field_list = [f for f in fields .values () if f ._field_type is _FIELD ]
524636
525637 if repr :
526- _set_attribute (cls , '__repr__' ,
527- _repr_fn (list (filter (lambda f : f .repr , field_list ))))
528-
529- if is_frozen :
530- _set_attribute (cls , '__setattr__' , _frozen_setattr )
531- _set_attribute (cls , '__delattr__' , _frozen_delattr )
532-
533- generate_hash = False
534- if hash is None :
535- if eq and frozen :
536- # Generate a hash function.
537- generate_hash = True
538- elif eq and not frozen :
539- # Not hashable.
540- _set_attribute (cls , '__hash__' , None )
541- elif not eq :
542- # Otherwise, use the base class definition of hash(). That is,
543- # don't set anything on this class.
544- pass
545- else :
546- assert "can't get here"
547- else :
548- generate_hash = hash
549- if generate_hash :
550- _set_attribute (cls , '__hash__' ,
551- _hash_fn (list (filter (lambda f : f .compare
552- if f .hash is None
553- else f .hash ,
554- field_list ))))
638+ flds = [f for f in field_list if f .repr ]
639+ _set_new_attribute (cls , '__repr__' , _repr_fn (flds ))
555640
556641 if eq :
557- # Create and __eq__ and __ne__ methods.
558- _set_eq_fns (cls , list (filter (lambda f : f .compare , field_list )))
642+ # Create _eq__ method. There's no need for a __ne__ method,
643+ # since python will call __eq__ and negate it.
644+ flds = [f for f in field_list if f .compare ]
645+ self_tuple = _tuple_str ('self' , flds )
646+ other_tuple = _tuple_str ('other' , flds )
647+ _set_new_attribute (cls , '__eq__' ,
648+ _cmp_fn ('__eq__' , '==' ,
649+ self_tuple , other_tuple ))
559650
560651 if order :
561- # Create and __lt__, __le__, __gt__, and __ge__ methods.
562- # Create and set the comparison functions.
563- _set_order_fns (cls , list (filter (lambda f : f .compare , field_list )))
652+ # Create and set the ordering methods.
653+ flds = [f for f in field_list if f .compare ]
654+ self_tuple = _tuple_str ('self' , flds )
655+ other_tuple = _tuple_str ('other' , flds )
656+ for name , op in [('__lt__' , '<' ),
657+ ('__le__' , '<=' ),
658+ ('__gt__' , '>' ),
659+ ('__ge__' , '>=' ),
660+ ]:
661+ if _set_new_attribute (cls , name ,
662+ _cmp_fn (name , op , self_tuple , other_tuple )):
663+ raise TypeError (f'Cannot overwrite attribute { name } '
664+ f'in { cls .__name__ } . Consider using '
665+ 'functools.total_ordering' )
666+
667+ if is_frozen :
668+ for name , fn in [('__setattr__' , _frozen_setattr ),
669+ ('__delattr__' , _frozen_delattr )]:
670+ if _set_new_attribute (cls , name , fn ):
671+ raise TypeError (f'Cannot overwrite attribute { name } '
672+ f'in { cls .__name__ } ' )
673+
674+ # Decide if/how we're going to create a hash function.
675+ # TODO: Move this table to module scope, so it's not recreated
676+ # all the time.
677+ generate_hash = {(None , False , False ): ('' , '' ),
678+ (None , False , True ): ('' , '' ),
679+ (None , True , False ): ('none' , '' ),
680+ (None , True , True ): ('fn' , 'fn-x' ),
681+ (False , False , False ): ('' , '' ),
682+ (False , False , True ): ('' , '' ),
683+ (False , True , False ): ('' , '' ),
684+ (False , True , True ): ('' , '' ),
685+ (True , False , False ): ('fn' , 'fn-x' ),
686+ (True , False , True ): ('fn' , 'fn-x' ),
687+ (True , True , False ): ('fn' , 'fn-x' ),
688+ (True , True , True ): ('fn' , 'fn-x' ),
689+ }[None if hash is None else bool (hash ), # Force bool() if not None.
690+ bool (eq ),
691+ bool (frozen )]['__hash__' in cls .__dict__ ]
692+ # No need to call _set_new_attribute here, since we already know if
693+ # we're overwriting a __hash__ or not.
694+ if generate_hash == '' :
695+ # Do nothing.
696+ pass
697+ elif generate_hash == 'none' :
698+ cls .__hash__ = None
699+ elif generate_hash in ('fn' , 'fn-x' ):
700+ if generate_hash == 'fn' or auto_hash_test :
701+ flds = [f for f in field_list
702+ if (f .compare if f .hash is None else f .hash )]
703+ cls .__hash__ = _hash_fn (flds )
704+ else :
705+ assert False , f"can't get here: { generate_hash } "
564706
565707 if not getattr (cls , '__doc__' ):
566708 # Create a class doc-string.
0 commit comments