From 03f418c5c4bf6245ebc5c5c744b495be0f567c3c Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 11 Feb 2020 13:43:19 +0000 Subject: [PATCH 1/4] Support for string type annotations in attrs It doesn't cover all the possible cases because there are limitations in attrs itself. See https://github.com/python-attrs/attrs/issues/593 Enabled py35 in Travis --- .travis.yml | 2 ++ andi/andi.py | 33 ++++++++++++++++++++++++++++++++- pytest.ini | 4 ++++ tests/py36.py | 26 ++++++++++++++++++++++++++ tests/py37.py | 13 +++++++++++++ tests/test_andi.py | 22 ---------------------- tests/test_attrs_support.py | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 pytest.ini create mode 100644 tests/py36.py create mode 100644 tests/py37.py create mode 100644 tests/test_attrs_support.py diff --git a/.travis.yml b/.travis.yml index ea135bd..25c9c4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: python matrix: include: + - python: 3.5 + env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 - python: 3.7 diff --git a/andi/andi.py b/andi/andi.py index 965ea60..0d21650 100644 --- a/andi/andi.py +++ b/andi/andi.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import sys +import types from typing import ( Dict, List, Optional, Type, Callable, Union, Container, get_type_hints, @@ -7,11 +9,40 @@ from andi.typeutils import get_union_args, is_union +def _get_globalns_as_get_type_hints(func) -> Dict: + """ Global namespace resolution extracted from ``get_type_hints`` method """ + if isinstance(func, types.ModuleType): + return func.__dict__ + else: + nsobj = func + # Find globalns for the unwrapped object. + while hasattr(nsobj, '__wrapped__'): + nsobj = nsobj.__wrapped__ + return getattr(nsobj, '__globals__', {}) + + +def _get_globalns_for_attrs(func) -> Dict: + """ Adds partial support for postponed type annotations in attrs classes. + Also required to support attrs classes when + ``from __future__ import annotations`` is used (default for python 4.0). + See https://github.com/python-attrs/attrs/issues/593 """ + return dict(sys.modules[func.__module__].__dict__) + + +def _get_globalns(func) -> Dict: + """ Returns the global namespace that will be used for the resolution + of postponed type annotations """ + ns = dict(_get_globalns_for_attrs(func)) + ns.update(_get_globalns_as_get_type_hints(func)) + return ns + + def inspect(func: Callable) -> Dict[str, List[Optional[Type]]]: """ For each argument of the ``func`` return a list of possible types. """ - annotations = get_type_hints(func) + globalns = _get_globalns(func) + annotations = get_type_hints(func, globalns) annotations.pop('return', None) annotations.pop('cls', None) # FIXME: pop first argument of methods res = {} diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..217d599 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = + --ignore=tests/py36.py + --ignore=tests/py37.py \ No newline at end of file diff --git a/tests/py36.py b/tests/py36.py new file mode 100644 index 0000000..dce6ea7 --- /dev/null +++ b/tests/py36.py @@ -0,0 +1,26 @@ +import attr + +import andi + + +@attr.s(auto_attribs=True) +class A_36: + b: 'B_36' + + +@attr.s(auto_attribs=True) +class B_36: + a: A_36 + + +def cross_referenced_within_func(): + + @attr.s(auto_attribs=True) + class A: + b: 'B' + + @attr.s(auto_attribs=True) + class B: + a: A + + return andi.inspect(A.__init__), andi.inspect(B.__init__) \ No newline at end of file diff --git a/tests/py37.py b/tests/py37.py new file mode 100644 index 0000000..0196ffc --- /dev/null +++ b/tests/py37.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +import attr + + +@attr.s(auto_attribs=True) +class A_37: + b: B_37 + + +@attr.s(auto_attribs=True) +class B_37: + a: A_37 diff --git a/tests/test_andi.py b/tests/test_andi.py index 14d2434..57f481f 100644 --- a/tests/test_andi.py +++ b/tests/test_andi.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- -import inspect from typing import Union, Optional, TypeVar, Type -import attr -import pytest - import andi @@ -120,21 +116,3 @@ def from_foo(cls: Type[T], foo: Foo) -> T: assert andi.inspect(MyClass.from_foo) == {'foo': [Foo]} assert andi.to_provide(MyClass.from_foo, {Foo, Bar}) == {'foo': Foo} - -@pytest.mark.xfail -def test_attrs_string_type_references(): - """ ``get_type_hint`` function fails on attrs classes that reference - a type using a string (see class A below for an example). The reason - is that ``__init__.__globals__`` original content is lost by attrs. - See https://github.com/python-attrs/attrs/issues/593 - """ - @attr.s(auto_attribs=True) - class A: - b: 'B' - - @attr.s(auto_attribs=True) - class B: - a: A - - assert andi.inspect(B.__init__) == {'a': [A]} - assert andi.inspect(A.__init__) == {'b': [B]} diff --git a/tests/test_attrs_support.py b/tests/test_attrs_support.py new file mode 100644 index 0000000..c346cc3 --- /dev/null +++ b/tests/test_attrs_support.py @@ -0,0 +1,33 @@ +import sys + +import pytest + +import andi + + +@pytest.mark.skipif(sys.version_info < (3, 6), + reason="Annotating the types of class variables require Python 3.6 or higher") +def test_attrs_str_type_annotations_py36(): + from py36 import A_36, B_36 + assert andi.inspect(B_36.__init__) == {'a': [A_36]} + assert andi.inspect(A_36.__init__) == {'b': [B_36]} + + +@pytest.mark.skipif(sys.version_info < (3, 7), + reason="'from __future__ import annotations' require Python 3.7 or higher") +def test_attrs_str_type_annotations_py37(): + from py37 import A_37, B_37 + assert andi.inspect(B_37.__init__) == {'a': [A_37]} + assert andi.inspect(A_37.__init__) == {'b': [B_37]} + + +@pytest.mark.skipif(sys.version_info < (3, 6), + reason="Annotating the types of class variables require Python 3.6 or higher") +@pytest.mark.xfail +def test_attrs_str_type_annotations_within_func_py36(): + """ Andi don't work with attrs classes defined within a function. + More info at: https://github.com/python-attrs/attrs/issues/593#issuecomment-584632175""" + from py36 import cross_referenced_within_func + a_inspect, b_inspect = cross_referenced_within_func() + assert b_inspect['a'].__name__ == 'A' + assert a_inspect['b'].__name__ == 'B' From 665c6a53bb5ba511dfbfcbe25223d915c91500e8 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 11 Feb 2020 13:58:17 +0000 Subject: [PATCH 2/4] Fix mypy --- tests/py37.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/py37.py b/tests/py37.py index 0196ffc..8e719fe 100644 --- a/tests/py37.py +++ b/tests/py37.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from __future__ import annotations # type: ignore import attr From 3cf30d3a82f0f74189bc6195f8ea674c0c7ba361 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 11 Feb 2020 16:06:02 +0000 Subject: [PATCH 3/4] Link to code extracted --- andi/andi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/andi/andi.py b/andi/andi.py index 0d21650..7d690d0 100644 --- a/andi/andi.py +++ b/andi/andi.py @@ -10,7 +10,8 @@ def _get_globalns_as_get_type_hints(func) -> Dict: - """ Global namespace resolution extracted from ``get_type_hints`` method """ + """ Global namespace resolution extracted from ``get_type_hints`` method. + Python 3.7 (https://github.com/python/cpython/blob/3.7/Lib/typing.py#L981-L988) """ if isinstance(func, types.ModuleType): return func.__dict__ else: From 87c7826e649bd4e57a056328271276ff8a5f7add Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 11 Feb 2020 17:38:47 +0000 Subject: [PATCH 4/4] branch for modules not required. test for wrapped functions --- andi/andi.py | 22 ++++++++++------------ tests/test_andi.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/andi/andi.py b/andi/andi.py index 7d690d0..1a5ab5f 100644 --- a/andi/andi.py +++ b/andi/andi.py @@ -9,20 +9,18 @@ from andi.typeutils import get_union_args, is_union -def _get_globalns_as_get_type_hints(func) -> Dict: +def _get_globalns_as_get_type_hints(func: Callable) -> Dict: """ Global namespace resolution extracted from ``get_type_hints`` method. - Python 3.7 (https://github.com/python/cpython/blob/3.7/Lib/typing.py#L981-L988) """ - if isinstance(func, types.ModuleType): - return func.__dict__ - else: - nsobj = func - # Find globalns for the unwrapped object. - while hasattr(nsobj, '__wrapped__'): - nsobj = nsobj.__wrapped__ - return getattr(nsobj, '__globals__', {}) + Python 3.7 (https://github.com/python/cpython/blob/3.7/Lib/typing.py#L981-L988) + Note that this is only supporting functions as input. """ + nsobj = func + # Find globalns for the unwrapped object. + while hasattr(nsobj, '__wrapped__'): + nsobj = getattr(nsobj, '__wrapped__') + return getattr(nsobj, '__globals__', {}) -def _get_globalns_for_attrs(func) -> Dict: +def _get_globalns_for_attrs(func: Callable) -> Dict: """ Adds partial support for postponed type annotations in attrs classes. Also required to support attrs classes when ``from __future__ import annotations`` is used (default for python 4.0). @@ -30,7 +28,7 @@ def _get_globalns_for_attrs(func) -> Dict: return dict(sys.modules[func.__module__].__dict__) -def _get_globalns(func) -> Dict: +def _get_globalns(func: Callable) -> Dict: """ Returns the global namespace that will be used for the resolution of postponed type annotations """ ns = dict(_get_globalns_for_attrs(func)) diff --git a/tests/test_andi.py b/tests/test_andi.py index 57f481f..00a12bd 100644 --- a/tests/test_andi.py +++ b/tests/test_andi.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from functools import wraps from typing import Union, Optional, TypeVar, Type import andi @@ -116,3 +117,16 @@ def from_foo(cls: Type[T], foo: Foo) -> T: assert andi.inspect(MyClass.from_foo) == {'foo': [Foo]} assert andi.to_provide(MyClass.from_foo, {Foo, Bar}) == {'foo': Foo} + +def test_decorated(): + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + return fn(*args, **kwargs) + return wrapper + + @decorator + def func(x: 'Bar'): + pass + + assert andi.inspect(func) == {'x': [Bar]}