Skip to content

Commit

Permalink
Add attr.resolve_types
Browse files Browse the repository at this point in the history
This adds `attr.resolve_types` which can be used to resolve forward declarations in classes created using `__annotations__`

Fixes #265
  • Loading branch information
David Euresti authored and euresti committed Jul 14, 2020
1 parent 3e3d842 commit f4f4a6f
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 0 deletions.
2 changes: 2 additions & 0 deletions changelog.d/302.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added ``attr.resolve_types()``.
This function lets you resolve forward references that might exist in your annotations.
26 changes: 26 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,32 @@ Helpers
>>> attr.astuple(C(1,2))
(1, 2)

.. autofunction:: attr.resolve_types

For example:

.. doctest::

>>> import typing
>>> @attr.s(auto_attribs=True)
... class A:
... a: typing.List['A']
... b: 'B'
...
>>> @attr.s(auto_attribs=True)
... class B:
... a: A
...
>>> attr.fields(A).a.type
typing.List[_ForwardRef('A')]
>>> attr.fields(A).b.type
'B'
>>> attr.resolve_types(A, globals(), locals())
>>> attr.fields(A).a.type
typing.List[A]
>>> attr.fields(A).b.type
<class 'B'>

``attrs`` includes some handy helpers for filtering:

.. autofunction:: attr.filters.include
Expand Down
26 changes: 26 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,32 @@ If you don't mind annotating *all* attributes, you can even drop the `attr.ib` a

The generated ``__init__`` method will have an attribute called ``__annotations__`` that contains this type information.

If your annotations contain strings (e.g. forward references),
you can resolve these after all references have been defined by using :func:`attr.resolve_types`.
This will replace the *type* attribute in the respective fields.

.. doctest::

>>> import typing
>>> @attr.s(auto_attribs=True)
... class A:
... a: typing.List['A']
... b: 'B'
...
>>> @attr.s(auto_attribs=True)
... class B:
... a: A
...
>>> attr.fields(A).a.type
typing.List[_ForwardRef('A')]
>>> attr.fields(A).b.type
'B'
>>> attr.resolve_types(A, globals(), locals())
>>> attr.fields(A).a.type
typing.List[A]
>>> attr.fields(A).b.type
<class 'B'>

.. warning::

``attrs`` itself doesn't have any features that work on top of type metadata *yet*.
Expand Down
2 changes: 2 additions & 0 deletions src/attr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
fields,
fields_dict,
make_class,
resolve_types,
validate,
)
from ._version_info import VersionInfo
Expand Down Expand Up @@ -61,6 +62,7 @@
"has",
"ib",
"make_class",
"resolve_types",
"s",
"set_run_validators",
"validate",
Expand Down
30 changes: 30 additions & 0 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,36 @@ def fields_dict(cls):
return ordered_dict(((a.name, a) for a in attrs))


def resolve_types(cls, globalns=None, localns=None):
"""
Resolve any strings and forward annotations in type annotations.
:param type cls: Class to resolve.
:param globalns: Dictionary containing global variables, if needed.
:param localns: Dictionary containing local variables, if needed.
:raise TypeError: If *cls* is not a class.
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
:raise NameError: If types cannot be resolved because of missing variables.
.. versionadded:: 17.4.0
"""
try:
# Since calling get_type_hints is expensive we cache whether we've
# done it already.
cls.__attrs_types_resolved__
except AttributeError:
import typing

hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
for field in fields(cls):
if field.name in hints:
# Since fields have been frozen we must work around it.
_obj_setattr(field, "type", hints[field.name])
cls.__attrs_types_resolved__ = True


def validate(inst):
"""
Validate all attributes on *inst* that have a validator.
Expand Down
86 changes: 86 additions & 0 deletions tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,89 @@ def __eq__(self, other):
@attr.s(auto_attribs=True)
class C:
x: typing.Any = NonComparable()

def test_basic_resolve(self):
"""
Resolve the `Attribute.type` attr from basic type annotations.
Unannotated types are ignored.
"""

@attr.s
class C:
x: "int" = attr.ib()
y = attr.ib(type=str)
z = attr.ib()

assert "int" == attr.fields(C).x.type
assert str is attr.fields(C).y.type
assert None is attr.fields(C).z.type

attr.resolve_types(C)

assert int is attr.fields(C).x.type
assert str is attr.fields(C).y.type
assert None is attr.fields(C).z.type

@pytest.mark.parametrize("slots", [True, False])
def test_resolve_types_auto_attrib(self, slots):
"""
Types can be resolved even when strings are involved.
"""

@attr.s(slots=slots, auto_attribs=True)
class A:
a: typing.List[int]
b: typing.List["int"]
c: "typing.List[int]"

assert typing.List[int] == attr.fields(A).a.type
assert typing.List["int"] == attr.fields(A).b.type
assert "typing.List[int]" == attr.fields(A).c.type

# Note: I don't have to pass globals and locals here because
attr.resolve_types(A)

assert typing.List[int] == attr.fields(A).a.type
assert typing.List[int] == attr.fields(A).b.type
assert typing.List[int] == attr.fields(A).c.type

@pytest.mark.parametrize("slots", [True, False])
def test_self_reference(self, slots):
"""
References to self class using quotes can be resolved.
"""

@attr.s(slots=slots, auto_attribs=True)
class A:
a: "A"
b: typing.Optional["A"]

assert "A" == attr.fields(A).a.type
assert typing.Optional["A"] == attr.fields(A).b.type

attr.resolve_types(A, globals(), locals())

assert A == attr.fields(A).a.type
assert typing.Optional[A] == attr.fields(A).b.type

@pytest.mark.parametrize("slots", [True, False])
def test_forward_reference(self, slots):
"""
Forward references can be resolved.
"""

@attr.s(slots=slots, auto_attribs=True)
class A:
a: typing.List["B"]

@attr.s(slots=slots, auto_attribs=True)
class B:
a: A

assert typing.List["B"] == attr.fields(A).a.type
assert A == attr.fields(B).a.type

attr.resolve_types(A, globals(), locals())

assert typing.List[B] == attr.fields(A).a.type
assert A == attr.fields(B).a.type

0 comments on commit f4f4a6f

Please sign in to comment.