From 0f3cf768e7027520677a2237a18367260e408911 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Wed, 25 Dec 2024 16:12:24 +0100 Subject: [PATCH] Add "resolve_types" argument to define() Fixes: #1286 --- src/attr/__init__.pyi | 2 ++ src/attr/_make.py | 13 +++++++++++++ src/attr/_next_gen.py | 12 ++++++++++++ src/attrs/__init__.pyi | 2 ++ tests/test_next_gen.py | 21 +++++++++++++++++++++ 5 files changed, 50 insertions(+) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 133e50105..e6181c640 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -279,6 +279,7 @@ def attrs( field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., unsafe_hash: bool | None = ..., + resolve_types: bool = ..., ) -> _C: ... @overload @dataclass_transform(order_default=True, field_specifiers=(attrib, field)) @@ -307,6 +308,7 @@ def attrs( field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., unsafe_hash: bool | None = ..., + resolve_types: bool = ..., ) -> Callable[[_C], _C]: ... def fields(cls: type[AttrsInstance]) -> Any: ... def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ... diff --git a/src/attr/_make.py b/src/attr/_make.py index f00fec48c..0b3ec90d3 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -645,6 +645,7 @@ class _ClassBuilder: "_is_exc", "_on_setattr", "_pre_init_has_args", + "_resolve_types", "_slots", "_weakref_slot", "_wrote_own_setattr", @@ -666,6 +667,7 @@ def __init__( on_setattr, has_custom_setattr, field_transformer, + resolve_types, ): attrs, base_attrs, base_map = _transform_attrs( cls, @@ -683,6 +685,7 @@ def __init__( self._base_attr_map = base_map self._attr_names = tuple(a.name for a in attrs) self._slots = slots + self._resolve_types = resolve_types self._frozen = frozen self._weakref_slot = weakref_slot self._cache_hash = cache_hash @@ -766,6 +769,12 @@ def build_class(self): ): cls.__attrs_init_subclass__() + if self._resolve_types: + # Need to import here to avoid circular imports + from . import _funcs + + cls = _funcs.resolve_types(cls) + return cls def _patch_original_class(self): @@ -1267,6 +1276,7 @@ def attrs( field_transformer=None, match_args=True, unsafe_hash=None, + resolve_types=False, ): r""" A class decorator that adds :term:`dunder methods` according to the @@ -1333,6 +1343,8 @@ def attrs( If a class has an *inherited* classmethod called ``__attrs_init_subclass__``, it is executed after the class is created. .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + .. versionadded:: 25.1.0 + Added the *resolve_types* argument. """ if repr_ns is not None: import warnings @@ -1385,6 +1397,7 @@ def wrap(cls): on_setattr, has_own_setattr, field_transformer, + resolve_types, ) if _determine_whether_to_implement( cls, repr, auto_detect, ("__repr__",) diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 9290664b2..ca182223e 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -43,6 +43,7 @@ def define( on_setattr=None, field_transformer=None, match_args=True, + resolve_types=False, ): r""" A class decorator that adds :term:`dunder methods` according to @@ -235,6 +236,14 @@ def define( non-keyword-only ``__init__`` parameter names on Python 3.10 and later. Ignored on older Python versions. + resolve_types (bool): + If True, automatically call :func:`~attrs.resolve_types()` on the + class. + + If you need to explicitly pass a global or local namespace, you + should leave this at False and explicitly call + :func:`~attrs.resolve_types()` instead. + collect_by_mro (bool): If True, *attrs* collects attributes from base classes correctly according to the `method resolution order @@ -319,6 +328,8 @@ def define( .. versionadded:: 24.3.0 Unless already present, a ``__replace__`` method is automatically created for `copy.replace` (Python 3.13+ only). + .. versionadded:: 25.1.0 + Added the *resolve_types* argument. .. note:: @@ -366,6 +377,7 @@ def do_it(cls, auto_attribs): on_setattr=on_setattr, field_transformer=field_transformer, match_args=match_args, + resolve_types=resolve_types, ) def wrap(cls): diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi index 648fa7a34..03c0e115b 100644 --- a/src/attrs/__init__.pyi +++ b/src/attrs/__init__.pyi @@ -179,6 +179,7 @@ def define( on_setattr: _OnSetAttrArgType | None = ..., field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., + resolve_types: bool = ..., ) -> _C: ... @overload @dataclass_transform(field_specifiers=(attrib, field)) @@ -205,6 +206,7 @@ def define( on_setattr: _OnSetAttrArgType | None = ..., field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., + resolve_types: bool = ..., ) -> Callable[[_C], _C]: ... mutable = define diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index 41e534df0..e7afdd921 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -425,6 +425,27 @@ class D(B, C): assert d.x == d.xx() + def test_resolve_types(self): + """ + Types can optionally be resolve directly by the decorator. + """ + @attrs.define(resolve_types=True) + class A: + x: "int" = 10 + + assert attrs.fields(A).x.type is int + + def test_resolve_types_default_off(self): + """ + Types are not resolved by default. + """ + + @attrs.define(resolve_types=False) + class A: + x: "int" = 10 + + assert attrs.fields(A).x.type == "int" + class TestAsTuple: def test_smoke(self):