Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ compound_stmt[stmt_ty]:
| &'try' try_stmt
| &'while' while_stmt
| match_stmt
| &(NAME NAME) class_def

# SIMPLE STATEMENTS
# =================
Expand Down Expand Up @@ -282,7 +283,12 @@ class_def_raw[stmt_ty]:
_PyAST_ClassDef(a->v.Name.id,
(b) ? ((expr_ty) b)->v.Call.args : NULL,
(b) ? ((expr_ty) b)->v.Call.keywords : NULL,
c, NULL, t, EXTRA) }
c, NULL, t, NULL, EXTRA) }
| "make" maker=NAME a=NAME t=[type_params] b=['(' z=[arguments] ')' { z }] ':' c=block {
_PyAST_ClassDef(a->v.Name.id,
(b) ? ((expr_ty) b)->v.Call.args : NULL,
(b) ? ((expr_ty) b)->v.Call.keywords : NULL,
c, NULL, t, maker, EXTRA) }

# Function definitions
# --------------------
Expand Down
5 changes: 3 additions & 2 deletions Include/internal/pycore_ast.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_ast_state.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__lshift__)
STRUCT_FOR_ID(__lt__)
STRUCT_FOR_ID(__main__)
STRUCT_FOR_ID(__make__)
STRUCT_FOR_ID(__match_args__)
STRUCT_FOR_ID(__matmul__)
STRUCT_FOR_ID(__missing__)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Lib/_ast_unparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,12 @@ def visit_ClassDef(self, node):
for deco in node.decorator_list:
self.fill("@", allow_semicolon=False)
self.traverse(deco)
self.fill("class " + node.name, allow_semicolon=False)
if node.builder is None:
self.fill("class " + node.name, allow_semicolon=False)
else:
self.fill(allow_semicolon=False)
self.traverse(node.builder)
self.write(" " + node.name)
if hasattr(node, "type_params"):
self._type_params_helper(node.type_params)
with self.delimit_if("(", ")", condition = node.bases or node.keywords):
Expand Down
153 changes: 150 additions & 3 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ def __repr__(self):
# @dataclass.
_PARAMS = '__dataclass_params__'

# The name of a temporary attribute on maker-created slotted classes.
_MAKER_DEFAULTS = '__dataclass_maker_defaults__'

# The name of the function, that if it exists, is called at the end of
# __init__.
_POST_INIT_NAME = '__post_init__'
Expand Down Expand Up @@ -821,7 +824,11 @@ def _get_field(cls, a_name, a_type, default_kw_only):

# If the default value isn't derived from Field, then it's only a
# normal default value. Convert it to a Field().
default = getattr(cls, a_name, MISSING)
maker_defaults = getattr(cls, _MAKER_DEFAULTS, None)
if maker_defaults is not None and a_name in maker_defaults:
default = maker_defaults[a_name]
else:
default = getattr(cls, a_name, MISSING)
if isinstance(default, Field):
f = default
else:
Expand Down Expand Up @@ -996,7 +1003,8 @@ def __get__(self, _obj, cls):


def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
match_args, kw_only, slots, weakref_slot):
match_args, kw_only, slots, weakref_slot,
slots_already_added=False):
# Now that dicts retain insertion order, there's no reason to use
# an ordered dict. I am leveraging that ordering here, because
# derived class fields overwrite base class fields, but the order
Expand Down Expand Up @@ -1238,9 +1246,21 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
# It's an error to specify weakref_slot if slots is False.
if weakref_slot and not slots:
raise TypeError('weakref_slot is True but slots is False')
if slots:
if slots and slots_already_added:
if frozen:
if '__getstate__' not in cls.__dict__:
cls.__getstate__ = _dataclass_getstate
if '__setstate__' not in cls.__dict__:
cls.__setstate__ = _dataclass_setstate
elif slots:
cls = _add_slots(cls, frozen, weakref_slot, fields)

if slots_already_added:
try:
delattr(cls, _MAKER_DEFAULTS)
except AttributeError:
pass

abc.update_abstractmethods(cls)

return cls
Expand Down Expand Up @@ -1457,6 +1477,133 @@ def wrap(cls):
return wrap(cls)


_DATACLASS_OPTION_NAMES = frozenset({
'init',
'repr',
'eq',
'order',
'unsafe_hash',
'frozen',
'match_args',
'kw_only',
'slots',
'weakref_slot',
})


def _split_dataclass_build_class_kwargs(kwds):
dataclass_kwds = {}
class_kwds = {}
for name, value in kwds.items():
if name in _DATACLASS_OPTION_NAMES:
dataclass_kwds[name] = value
else:
class_kwds[name] = value
return dataclass_kwds, class_kwds


def _get_dataclass_maker_annotations(ns):
annotate = ns.get('__annotate_func__')
if annotate is None:
return ns.get('__annotations__', {})
return annotationlib.call_annotate_function(
annotate, annotationlib.Format.STRING)


class _DataclassBuilderNamespace:
pass


def _get_dataclass_maker_slots(ns, bases, weakref_slot):
if '__slots__' in ns:
name = ns.get('__qualname__', ns.get('__name__'))
raise TypeError(f'{name} already specifies __slots__')

annotations = _get_dataclass_maker_annotations(ns)
proxy = _DataclassBuilderNamespace()
proxy.__module__ = ns.get('__module__')
for name, value in ns.items():
setattr(proxy, name, value)

fields = {}
for base in reversed(bases):
for cls in getattr(base, '__mro__', (base,))[-1::-1]:
base_fields = getattr(cls, _FIELDS, None)
if base_fields is not None:
fields.update(base_fields)

dataclasses = sys.modules[__name__]
kw_only = False
defined_fields = {}
for name, type in annotations.items():
if isinstance(type, str):
a_type_annotation = _get_type_from_annotation(type, proxy)
else:
a_type_annotation = type
if _is_kw_only(a_type_annotation, dataclasses):
kw_only = True
continue
f = _get_field(proxy, name, type, kw_only)
fields[f.name] = f
if f._field_type is _FIELD:
defined_fields[f.name] = f

field_names = tuple(
f.name for f in fields.values() if f._field_type is _FIELD)
inherited_classes = itertools.chain.from_iterable(
getattr(base, '__mro__', (base,))[:-1] for base in bases)
inherited_slots = set(
itertools.chain.from_iterable(map(_get_slots, inherited_classes))
)
return _create_slots(defined_fields, inherited_slots, field_names,
weakref_slot)


def _add_dataclass_maker_slots(ns, bases, dataclass_kwds):
defaults = {}
weakref_slot = dataclass_kwds.get('weakref_slot', False)
ns['__slots__'] = _get_dataclass_maker_slots(ns, bases, weakref_slot)
for slot in ns['__slots__']:
if slot in ns:
defaults[slot] = ns.pop(slot)
if defaults:
ns[_MAKER_DEFAULTS] = defaults


def _dataclass_build_class(func, name, *bases, **kwds):
dataclass_kwds, class_kwds = _split_dataclass_build_class_kwargs(kwds)
slots = dataclass_kwds.get('slots', False)
resolved_bases = types.resolve_bases(bases)

def exec_body(ns):
types.exec_class_body(func, ns)
if slots:
_add_dataclass_maker_slots(ns, resolved_bases, dataclass_kwds)
if resolved_bases is not bases:
ns['__orig_bases__'] = bases

cls = types.new_class(name, resolved_bases, class_kwds, exec_body)
if slots:
return _process_class(
cls,
init=dataclass_kwds.get('init', True),
repr=dataclass_kwds.get('repr', True),
eq=dataclass_kwds.get('eq', True),
order=dataclass_kwds.get('order', False),
unsafe_hash=dataclass_kwds.get('unsafe_hash', False),
frozen=dataclass_kwds.get('frozen', False),
match_args=dataclass_kwds.get('match_args', True),
kw_only=dataclass_kwds.get('kw_only', False),
slots=True,
weakref_slot=dataclass_kwds.get('weakref_slot', False),
slots_already_added=True,
)
return dataclass(cls, **dataclass_kwds)


dataclass.__make__ = _dataclass_build_class


def fields(class_or_instance):
"""Return a tuple describing the fields of this dataclass.

Expand Down
12 changes: 11 additions & 1 deletion Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


__all__ = [
'EnumType', 'EnumMeta', 'EnumDict',
'enum', 'EnumType', 'EnumMeta', 'EnumDict',
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
Expand Down Expand Up @@ -1355,6 +1355,16 @@ def value(self):
return self._value_


class _EnumClassMaker:
def __make__(self, func, name, *bases, **kwds):
if any(isinstance(base, EnumType) for base in bases):
return bltns.__build_class__(func, name, *bases, **kwds)
return bltns.__build_class__(func, name, *bases, Enum, **kwds)


enum = _EnumClassMaker()


class ReprEnum(Enum):
"""
Only changes the repr(), leaving str() and format() to the mixed-in type.
Expand Down
Loading
Loading