Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for string type annotations in attrs #2

Merged
merged 4 commits into from
Feb 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ language: python

matrix:
include:
- python: 3.5
env: TOXENV=py35
- python: 3.6
env: TOXENV=py36
- python: 3.7
Expand Down
32 changes: 31 additions & 1 deletion andi/andi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import sys
import types
from typing import (
Dict, List, Optional, Type, Callable, Union, Container,
get_type_hints,
Expand All @@ -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]]]:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kmike an alternative to fix the attrs problem would be to provide a inspect for classes. Something like:

def inspect_cls(cls: type):
    ...
   annotations = get_type_hints(cls.__init__, cls.__globals__)
    ...

In this case I think we would have access to the global namespace easily. What do you think?

"""
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 = {}
Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
addopts =
--ignore=tests/py36.py
--ignore=tests/py37.py
26 changes: 26 additions & 0 deletions tests/py36.py
Original file line number Diff line number Diff line change
@@ -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__)
13 changes: 13 additions & 0 deletions tests/py37.py
Original file line number Diff line number Diff line change
@@ -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
30 changes: 11 additions & 19 deletions tests/test_andi.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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]}
33 changes: 33 additions & 0 deletions tests/test_attrs_support.py
Original file line number Diff line number Diff line change
@@ -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'