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..1a5ab5f 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,39 @@ from andi.typeutils import get_union_args, is_union +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) + 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: 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). + See https://github.com/python-attrs/attrs/issues/593 """ + return dict(sys.modules[func.__module__].__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)) + 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..8e719fe --- /dev/null +++ b/tests/py37.py @@ -0,0 +1,13 @@ +from __future__ import annotations # type: ignore + +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..00a12bd 100644 --- a/tests/test_andi.py +++ b/tests/test_andi.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- -import inspect +from functools import wraps from typing import Union, Optional, TypeVar, Type -import attr -import pytest - import andi @@ -121,20 +118,15 @@ def from_foo(cls: Type[T], foo: Foo) -> T: 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' +def test_decorated(): + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + return fn(*args, **kwargs) + return wrapper - @attr.s(auto_attribs=True) - class B: - a: A + @decorator + def func(x: 'Bar'): + pass - assert andi.inspect(B.__init__) == {'a': [A]} - assert andi.inspect(A.__init__) == {'b': [B]} + assert andi.inspect(func) == {'x': [Bar]} 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'