Skip to content

Commit c7308a6

Browse files
Tinchepre-commit-ci[bot]hynek
authored
Flesh out resolve_types (#1099)
* Flesh out resolve_types * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add changelog entry * Fix flake? * Update 1099.change.md --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hynek Schlawack <[email protected]>
1 parent 5d9753a commit c7308a6

File tree

5 files changed

+49
-5
lines changed

5 files changed

+49
-5
lines changed

changelog.d/1099.change.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`attrs.resolve_types()` can now pass `include_extras` to `typing.get_type_hints()` on Python 3.9+, and does so by default.

src/attr/__init__.pyi

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]
6969
class AttrsInstance(AttrsInstance_, Protocol):
7070
pass
7171

72+
_A = TypeVar("_A", bound=AttrsInstance)
7273
# _make --
7374

7475
class _Nothing(enum.Enum):
@@ -488,11 +489,12 @@ def fields(cls: Type[AttrsInstance]) -> Any: ...
488489
def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ...
489490
def validate(inst: AttrsInstance) -> None: ...
490491
def resolve_types(
491-
cls: _C,
492+
cls: _A,
492493
globalns: Optional[Dict[str, Any]] = ...,
493494
localns: Optional[Dict[str, Any]] = ...,
494495
attribs: Optional[List[Attribute[Any]]] = ...,
495-
) -> _C: ...
496+
include_extras: bool = ...,
497+
) -> _A: ...
496498

497499
# TODO: add support for returning a proper attrs class from the mypy plugin
498500
# we use Any instead of _CountingAttr so that e.g. `make_class('Foo',

src/attr/_compat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414

1515
PYPY = platform.python_implementation() == "PyPy"
16+
PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
1617
PY310 = sys.version_info[:2] >= (3, 10)
1718
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
1819

src/attr/_funcs.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import copy
55

6-
from ._compat import get_generic_base
6+
from ._compat import PY_3_9_PLUS, get_generic_base
77
from ._make import NOTHING, _obj_setattr, fields
88
from .exceptions import AttrsAttributeNotFoundError
99

@@ -379,7 +379,9 @@ def evolve(inst, **changes):
379379
return cls(**changes)
380380

381381

382-
def resolve_types(cls, globalns=None, localns=None, attribs=None):
382+
def resolve_types(
383+
cls, globalns=None, localns=None, attribs=None, include_extras=True
384+
):
383385
"""
384386
Resolve any strings and forward annotations in type annotations.
385387
@@ -399,6 +401,10 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None):
399401
:param Optional[list] attribs: List of attribs for the given class.
400402
This is necessary when calling from inside a ``field_transformer``
401403
since *cls* is not an *attrs* class yet.
404+
:param bool include_extras: Resolve more accurately, if possible.
405+
Pass ``include_extras`` to ``typing.get_hints``, if supported by the
406+
typing module. On supported Python versions (3.9+), this resolves the
407+
types more accurately.
402408
403409
:raise TypeError: If *cls* is not a class.
404410
:raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
@@ -411,14 +417,20 @@ class and you didn't pass any attribs.
411417
412418
.. versionadded:: 20.1.0
413419
.. versionadded:: 21.1.0 *attribs*
420+
.. versionadded:: 23.1.0 *include_extras*
414421
415422
"""
416423
# Since calling get_type_hints is expensive we cache whether we've
417424
# done it already.
418425
if getattr(cls, "__attrs_types_resolved__", None) != cls:
419426
import typing
420427

421-
hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
428+
kwargs = {"globalns": globalns, "localns": localns}
429+
430+
if PY_3_9_PLUS:
431+
kwargs["include_extras"] = include_extras
432+
433+
hints = typing.get_type_hints(cls, **kwargs)
422434
for field in fields(cls) if attribs is None else attribs:
423435
if field.name in hints:
424436
# Since fields have been frozen we must work around it.

tests/test_annotations.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,34 @@ class C:
516516
assert str is attr.fields(C).y.type
517517
assert None is attr.fields(C).z.type
518518

519+
@pytest.mark.skipif(
520+
sys.version_info[:2] < (3, 9),
521+
reason="Incompatible behavior on older Pythons",
522+
)
523+
def test_extra_resolve(self):
524+
"""
525+
`get_type_hints` returns extra type hints.
526+
"""
527+
from typing import Annotated
528+
529+
globals = {"Annotated": Annotated}
530+
531+
@attr.define
532+
class C:
533+
x: 'Annotated[float, "test"]'
534+
535+
attr.resolve_types(C, globals)
536+
537+
assert attr.fields(C).x.type == Annotated[float, "test"]
538+
539+
@attr.define
540+
class D:
541+
x: 'Annotated[float, "test"]'
542+
543+
attr.resolve_types(D, globals, include_extras=False)
544+
545+
assert attr.fields(D).x.type == float
546+
519547
def test_resolve_types_auto_attrib(self, slots):
520548
"""
521549
Types can be resolved even when strings are involved.

0 commit comments

Comments
 (0)