Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ea8fc52

Browse files
authored
bpo-32513: Make it easier to override dunders in dataclasses. (GH-5366)
Class authors no longer need to specify repr=False if they want to provide a custom __repr__ for dataclasses. The same thing applies for the other dunder methods that the dataclass decorator adds. If dataclass finds that a dunder methods is defined in the class, it will not overwrite it.
1 parent 2a2247c commit ea8fc52

3 files changed

Lines changed: 678 additions & 294 deletions

File tree

Lib/dataclasses.py

Lines changed: 224 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,142 @@
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.
22158
class 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-
358468
def _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

450560
def _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

Comments
 (0)