Skip to content
Merged
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
169 changes: 89 additions & 80 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import re
import sys
import copy
import types
import inspect
import keyword
import itertools
import annotationlib
import abc
from reprlib import recursive_repr
lazy import copy
lazy import re


__all__ = ['dataclass',
Expand Down Expand Up @@ -178,21 +177,16 @@ def __repr__(self):
return '<factory>'
_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS()

# A sentinel object to detect if a parameter is supplied or not. Use
# a class to give it a better repr.
class _MISSING_TYPE:
pass
MISSING = _MISSING_TYPE()
# A sentinel object to detect if a parameter is supplied or not.
MISSING = sentinel("MISSING")

# A sentinel object to indicate that following fields are keyword-only by
# default. Use a class to give it a better repr.
class _KW_ONLY_TYPE:
pass
KW_ONLY = _KW_ONLY_TYPE()
# default.
KW_ONLY = sentinel("KW_ONLY")

# Since most per-field metadata will be unused, create an empty
# read-only proxy that can be shared among all fields.
_EMPTY_METADATA = types.MappingProxyType({})
# read-only dictionary that can be shared among all fields.
_EMPTY_METADATA = frozendict()

# Markers for the various kinds of fields and pseudo-fields.
class _FIELD_BASE:
Expand All @@ -217,9 +211,8 @@ def __repr__(self):
_POST_INIT_NAME = '__post_init__'

# String regex that string annotations for ClassVar or InitVar must match.
# Allows "identifier.identifier[" or "identifier[".
# https://bugs.python.org/issue33453 for details.
_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)')
# This regular expression is compiled on demand so that 're' module can be imported lazily
_MODULE_IDENTIFIER_RE = None

# Atomic immutable types which don't require any recursive handling and for which deepcopy
# returns the same object. We can provide a fast-path for these types in asdict and astuple.
Expand Down Expand Up @@ -763,22 +756,16 @@ def _is_kw_only(a_type, dataclasses):
return a_type is dataclasses.KW_ONLY


def _is_type(annotation, cls, a_module, a_type, is_type_predicate):
# Given a type annotation string, does it refer to a_type in
# a_module? For example, when checking that annotation denotes a
# ClassVar, then a_module is typing, and a_type is
# typing.ClassVar.
def _get_type_from_annotation(annotation, cls):
# Loosely parse a string annotation and return its type.

# It's possible to look up a_module given a_type, but it involves
# looking in sys.modules (again!), and seems like a waste since
# the caller already knows a_module.
# We can't perform a full type hint evaluation at the point where @dataclass
# was invoked because class's module is not fully initialized yet. So we resort
# to parsing string annotation using regexp, and extracting a type before
# the first square bracket.

# - annotation is a string type annotation
# - cls is the class that this annotation was found in
# - a_module is the module we want to match
# - a_type is the type in that module we want to match
# - is_type_predicate is a function called with (obj, a_module)
# that determines if obj is of the desired type.

# Since this test does not do a local namespace lookup (and
# instead only a module (global) lookup), there are some things it
Expand All @@ -804,22 +791,26 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate):
# a eval() penalty for every single field of every dataclass
# that's defined. It was judged not worth it.

match = _MODULE_IDENTIFIER_RE.match(annotation)
if match:
ns = None
module_name = match.group(1)
if not module_name:
# No module name, assume the class's module did
# "from dataclasses import InitVar".
ns = sys.modules.get(cls.__module__).__dict__
else:
# Look up module_name in the class's module.
module = sys.modules.get(cls.__module__)
if module and module.__dict__.get(module_name) is a_module:
ns = sys.modules.get(a_type.__module__).__dict__
if ns and is_type_predicate(ns.get(match.group(2)), a_module):
return True
return False
# String regex that string annotations for ClassVar or InitVar must match.
# Allows "identifier.identifier[" or "identifier[".
# https://github.com/python/cpython/issues/77634 for details.
global _MODULE_IDENTIFIER_RE
if _MODULE_IDENTIFIER_RE is None:
_MODULE_IDENTIFIER_RE = re.compile(r'^\s*(\w+(?:\s*\.\s*\w+)*)')

match = _MODULE_IDENTIFIER_RE.prefixmatch(annotation)
if not match:
return None

# Note: _MODULE_IDENTIFIER_RE guarantees that path is non-empty
path = match[1].split(".")
root = sys.modules.get(cls.__module__)
for path_item in path:
root = getattr(root, path_item.strip(), None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bypass module getattr for annotation probing

When a dataclass lives in a module with PEP 562 __getattr__ and has an unresolved string annotation, this lookup invokes the module fallback; a non-AttributeError (or any side effect) now escapes and prevents class creation. The previous code used the module dictionary and treated the annotation as an ordinary field, so resolve the first path component directly from the module namespace before traversing nested proxy attributes.

Useful? React with 👍 / 👎.

if root is None:
return None

return root


def _get_field(cls, a_name, a_type, default_kw_only):
Expand Down Expand Up @@ -857,17 +848,18 @@ def _get_field(cls, a_name, a_type, default_kw_only):
# is actually of the correct type.

# For the complete discussion, see https://bugs.python.org/issue33453
if isinstance(a_type, str):
a_type_annotation = _get_type_from_annotation(a_type, cls)
else:
a_type_annotation = a_type

# If typing has not been imported, then it's impossible for any
# annotation to be a ClassVar. So, only look for ClassVar if
# typing has been imported by any module (not necessarily cls's
# module).
typing = sys.modules.get('typing')
if typing:
if (_is_classvar(a_type, typing)
or (isinstance(f.type, str)
and _is_type(f.type, cls, typing, typing.ClassVar,
_is_classvar))):
if _is_classvar(a_type_annotation, typing):
f._field_type = _FIELD_CLASSVAR

# If the type is InitVar, or if it's a matching string annotation,
Expand All @@ -876,10 +868,7 @@ def _get_field(cls, a_name, a_type, default_kw_only):
# The module we're checking against is the module we're
# currently in (dataclasses.py).
dataclasses = sys.modules[__name__]
if (_is_initvar(a_type, dataclasses)
or (isinstance(f.type, str)
and _is_type(f.type, cls, dataclasses, dataclasses.InitVar,
_is_initvar))):
if _is_initvar(a_type_annotation, dataclasses):
f._field_type = _FIELD_INITVAR

# Validations for individual fields. This is delayed until now,
Expand Down Expand Up @@ -982,6 +971,29 @@ def _hash_exception(cls, fields, func_builder):
# See https://bugs.python.org/issue32929#msg312829 for an if-statement
# version of this table.

# A non-data descriptor to autogenerate class docstring
# from the signature of its __init__ method on demand.
# The primary reason is to be able to lazy import `inspect` module.
class _AutoDocstring:

def __get__(self, _obj, cls):
try:
# In some cases fetching a signature is not possible.
# But, we surely should not fail in this case.
import inspect
text_sig = str(inspect.signature(
cls,
annotation_format=annotationlib.Format.FORWARDREF,
)).replace(' -> None', '')
except TypeError, ValueError:
text_sig = ''

doc = cls.__name__ + text_sig
setattr(cls, '__doc__', doc)
return doc

_auto_docstring = _AutoDocstring()


def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
match_args, kw_only, slots, weakref_slot):
Expand Down Expand Up @@ -1050,10 +1062,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
dataclasses = sys.modules[__name__]
for name, type in cls_annotations.items():
# See if this is a marker to change the value of kw_only.
if (_is_kw_only(type, dataclasses)
or (isinstance(type, str)
and _is_type(type, cls, dataclasses, dataclasses.KW_ONLY,
_is_kw_only))):
if isinstance(type, str):
a_type_annotation = _get_type_from_annotation(type, cls)
else:
a_type_annotation = type
if _is_kw_only(a_type_annotation, dataclasses):
# Switch the default to kw_only=True, and ignore this
# annotation: it's not a real field.
if KW_ONLY_seen:
Expand Down Expand Up @@ -1209,23 +1222,13 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
if hash_action:
cls.__hash__ = hash_action(cls, field_list, func_builder)

# Generate the methods and add them to the class. This needs to be done
# before the __doc__ logic below, since inspect will look at the __init__
# signature.
# Generate the methods and add them to the class.
func_builder.add_fns_to_class(cls)

if not getattr(cls, '__doc__'):
# Create a class doc-string.
try:
# In some cases fetching a signature is not possible.
# But, we surely should not fail in this case.
text_sig = str(inspect.signature(
cls,
annotation_format=annotationlib.Format.FORWARDREF,
)).replace(' -> None', '')
except (TypeError, ValueError):
text_sig = ''
cls.__doc__ = (cls.__name__ + text_sig)
# Create a class doc-string lazily via descriptor protocol
# to avoid importing `inspect` module.
cls.__doc__ = _auto_docstring

if match_args:
# I could probably compute this once.
Expand Down Expand Up @@ -1344,10 +1347,6 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
if '__slots__' in cls.__dict__:
raise TypeError(f'{cls.__name__} already specifies __slots__')

# gh-102069: Remove existing __weakref__ descriptor.
# gh-135228: Make sure the original class can be garbage collected.
sys._clear_type_descriptors(cls)

# Create a new dict for our new class.
cls_dict = dict(cls.__dict__)
field_names = tuple(f.name for f in fields(cls))
Expand All @@ -1365,6 +1364,11 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
# available in _MARKER.
cls_dict.pop(field_name, None)

# Remove __dict__ and `__weakref__` descriptors.
# They'll be added back if applicable.
cls_dict.pop('__dict__', None)
cls_dict.pop('__weakref__', None) # gh-102069

# And finally create the class.
qualname = getattr(cls, '__qualname__', None)
newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
Expand All @@ -1385,8 +1389,11 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
# make an update, since all closures for a class will share a
# given cell.
for member in newcls.__dict__.values():

# If this is a wrapped function, unwrap it.
member = inspect.unwrap(member)
if not isinstance(member, type) and hasattr(member, '__wrapped__'):
import inspect
member = inspect.unwrap(member)

if isinstance(member, types.FunctionType):
if _update_func_cell_for__class__(member, cls, newcls):
Expand Down Expand Up @@ -1498,7 +1505,8 @@ class C:
If given, 'dict_factory' will be used instead of built-in dict.
The function applies recursively to field values that are
dataclass instances. This will also look into built-in containers:
tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'.
tuples, lists, dicts, and frozendicts. Other objects are copied
with 'copy.deepcopy()'.
"""
if not _is_dataclass_instance(obj):
raise TypeError("asdict() should be called on dataclass instances")
Expand Down Expand Up @@ -1554,7 +1562,7 @@ def _asdict_inner(obj, dict_factory):
return obj_type(*[_asdict_inner(v, dict_factory) for v in obj])
else:
return obj_type(_asdict_inner(v, dict_factory) for v in obj)
elif issubclass(obj_type, dict):
elif issubclass(obj_type, (dict, frozendict)):
if hasattr(obj_type, 'default_factory'):
# obj is a defaultdict, which has a different constructor from
# dict as it requires the default_factory as its first arg.
Expand Down Expand Up @@ -1589,7 +1597,8 @@ class C:
If given, 'tuple_factory' will be used instead of built-in tuple.
The function applies recursively to field values that are
dataclass instances. This will also look into built-in containers:
tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'.
tuples, lists, dicts, and frozendicts. Other objects are copied
with 'copy.deepcopy()'.
"""

if not _is_dataclass_instance(obj):
Expand Down Expand Up @@ -1618,7 +1627,7 @@ def _astuple_inner(obj, tuple_factory):
# generator (which is not true for namedtuples, handled
# above).
return type(obj)(_astuple_inner(v, tuple_factory) for v in obj)
elif isinstance(obj, dict):
elif isinstance(obj, (dict, frozendict)):
obj_type = type(obj)
if hasattr(obj_type, 'default_factory'):
# obj is a defaultdict, which has a different constructor from
Expand Down
Loading
Loading