Skip to content

Commit

Permalink
Flesh out resolve_types (#1099)
Browse files Browse the repository at this point in the history
* 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 <hs@ox.cx>
  • Loading branch information
3 people committed Mar 18, 2023
1 parent 5d9753a commit c7308a6
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 5 deletions.
1 change: 1 addition & 0 deletions changelog.d/1099.change.md
@@ -0,0 +1 @@
`attrs.resolve_types()` can now pass `include_extras` to `typing.get_type_hints()` on Python 3.9+, and does so by default.
6 changes: 4 additions & 2 deletions src/attr/__init__.pyi
Expand Up @@ -69,6 +69,7 @@ _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]
class AttrsInstance(AttrsInstance_, Protocol):
pass

_A = TypeVar("_A", bound=AttrsInstance)
# _make --

class _Nothing(enum.Enum):
Expand Down Expand Up @@ -488,11 +489,12 @@ def fields(cls: Type[AttrsInstance]) -> Any: ...
def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ...
def validate(inst: AttrsInstance) -> None: ...
def resolve_types(
cls: _C,
cls: _A,
globalns: Optional[Dict[str, Any]] = ...,
localns: Optional[Dict[str, Any]] = ...,
attribs: Optional[List[Attribute[Any]]] = ...,
) -> _C: ...
include_extras: bool = ...,
) -> _A: ...

# TODO: add support for returning a proper attrs class from the mypy plugin
# we use Any instead of _CountingAttr so that e.g. `make_class('Foo',
Expand Down
1 change: 1 addition & 0 deletions src/attr/_compat.py
Expand Up @@ -13,6 +13,7 @@


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

Expand Down
18 changes: 15 additions & 3 deletions src/attr/_funcs.py
Expand Up @@ -3,7 +3,7 @@

import copy

from ._compat import get_generic_base
from ._compat import PY_3_9_PLUS, get_generic_base
from ._make import NOTHING, _obj_setattr, fields
from .exceptions import AttrsAttributeNotFoundError

Expand Down Expand Up @@ -379,7 +379,9 @@ def evolve(inst, **changes):
return cls(**changes)


def resolve_types(cls, globalns=None, localns=None, attribs=None):
def resolve_types(
cls, globalns=None, localns=None, attribs=None, include_extras=True
):
"""
Resolve any strings and forward annotations in type annotations.
Expand All @@ -399,6 +401,10 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None):
:param Optional[list] attribs: List of attribs for the given class.
This is necessary when calling from inside a ``field_transformer``
since *cls* is not an *attrs* class yet.
:param bool include_extras: Resolve more accurately, if possible.
Pass ``include_extras`` to ``typing.get_hints``, if supported by the
typing module. On supported Python versions (3.9+), this resolves the
types more accurately.
:raise TypeError: If *cls* is not a class.
:raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
Expand All @@ -411,14 +417,20 @@ class and you didn't pass any attribs.
.. versionadded:: 20.1.0
.. versionadded:: 21.1.0 *attribs*
.. versionadded:: 23.1.0 *include_extras*
"""
# Since calling get_type_hints is expensive we cache whether we've
# done it already.
if getattr(cls, "__attrs_types_resolved__", None) != cls:
import typing

hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
kwargs = {"globalns": globalns, "localns": localns}

if PY_3_9_PLUS:
kwargs["include_extras"] = include_extras

hints = typing.get_type_hints(cls, **kwargs)
for field in fields(cls) if attribs is None else attribs:
if field.name in hints:
# Since fields have been frozen we must work around it.
Expand Down
28 changes: 28 additions & 0 deletions tests/test_annotations.py
Expand Up @@ -516,6 +516,34 @@ class C:
assert str is attr.fields(C).y.type
assert None is attr.fields(C).z.type

@pytest.mark.skipif(
sys.version_info[:2] < (3, 9),
reason="Incompatible behavior on older Pythons",
)
def test_extra_resolve(self):
"""
`get_type_hints` returns extra type hints.
"""
from typing import Annotated

globals = {"Annotated": Annotated}

@attr.define
class C:
x: 'Annotated[float, "test"]'

attr.resolve_types(C, globals)

assert attr.fields(C).x.type == Annotated[float, "test"]

@attr.define
class D:
x: 'Annotated[float, "test"]'

attr.resolve_types(D, globals, include_extras=False)

assert attr.fields(D).x.type == float

def test_resolve_types_auto_attrib(self, slots):
"""
Types can be resolved even when strings are involved.
Expand Down

0 comments on commit c7308a6

Please sign in to comment.