Skip to content
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
21 changes: 19 additions & 2 deletions pytypes/type_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,26 @@ def _extra(tp):
return None


def _get_orig_class(obj):
"""Returns `obj.__orig_class__` protecting from infinite recursion in `__getattr[ibute]__` wrapped in a `checker_tp`.
(See `checker_tp` in `typechecker._typeinspect_func for context)
Necessary if:
- we're wrapping a method (`obj` is `self`/`cls`) and either
- the object's class defines __getattribute__
or
- the object doesn't have an `__orig_class__` attribute
and the object's class defines __getattr__.
In such a situation, `parent_class = obj.__orig_class__`
would call `__getattr[ibute]__`. But that method is wrapped in a `checker_tp` too,
so then we'd go into the wrapped `__getattr[ibute]__` and do
`parent_class = obj.__orig_class__`, which would call `__getattr[ibute]__` again, and so on.
So to bypass `__getattr[ibute]__` we do this: """
return object.__getattribute__(obj, '__orig_class__')


def get_Generic_type(ob):
try:
return ob.__orig_class__
return _get_orig_class(ob)
except AttributeError:
return ob.__class__

Expand Down Expand Up @@ -499,7 +516,7 @@ def _deep_type(obj, checked, checked_len, depth = None, max_sample = None, get_t
if get_type is None:
get_type = type
try:
res = obj.__orig_class__
res = _get_orig_class(obj)
except AttributeError:
res = get_type(obj)
if depth == 0 or util._is_in(obj, checked[:checked_len]):
Expand Down
4 changes: 2 additions & 2 deletions pytypes/typechecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from .type_util import type_str, has_type_hints, _has_type_hints, is_builtin_type, \
deep_type, _funcsigtypes, _issubclass, _isinstance, _find_typed_base_method, \
_preprocess_typecheck, _raise_typecheck_error, _check_caller_type, TypeAgent, \
_check_as_func, is_Tuple
_check_as_func, is_Tuple, _get_orig_class
from . import util, type_util

try:
Expand Down Expand Up @@ -797,7 +797,7 @@ def checker_tp(*args, **kw):
parent_class = None
if slf:
try:
parent_class = args_kw[0].__orig_class__
parent_class = _get_orig_class(args_kw[0])
except AttributeError:
parent_class = args_kw[0].__class__
elif clsm:
Expand Down
49 changes: 49 additions & 0 deletions tests/test_typechecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,38 @@ def meth_2(self, c):
return 3*len(c)


@typechecked
class GetAttrDictWrapper(object):
"""Test a plausible use of __getattr__ -
A class that wraps a dict, enabling the values to be accessed as if they were attributes
(`d.abc` instead of `d['abc']`)
For example, the `pyrsistent` library does this on its dict replacement.

>>> o = GetAttrDictWrapper({'a': 5, 'b': 10})
>>> o.a
5
>>> o.b
10
>>> o.nonexistent
Traceback (most recent call last):
...
AttributeError('nonexistent')

"""

def __init__(self, dct):
# type: (dict) -> None
self.__dct = dct

def __getattr__(self, attr):
# type: (str) -> typing.Any
dct = self.__dct # can safely access the attribute because it exists so it won't trigger __getattr__
try:
return dct[attr]
except KeyError:
raise AttributeError(attr)


class TestTypecheck(unittest.TestCase):
def test_function(self):
self.assertEqual(testfunc(3, 2.5, 'abcd'), (9, 7.5))
Expand Down Expand Up @@ -2560,6 +2592,23 @@ def test_staticmethod(self):
tc.testmeth_static2(11, ('a', 'b'), 1.9))


class TestTypecheck_class_with_getattr(unittest.TestCase):
"""
See pull request:
https://github.com/Stewori/pytypes/pull/53
commit #:
e2523b347e52707f87d7078daad1a93940c12e2e
"""
def test_valid_access(self):
obj = GetAttrDictWrapper({'a': 5, 'b': 10})
self.assertEqual(obj.a, 5)
self.assertEqual(obj.b, 10)

def test_invalid_access(self):
obj = GetAttrDictWrapper({'a': 5, 'b': 10})
self.assertRaises(AttributeError, lambda: obj.nonexistent)


class TestTypecheck_module(unittest.TestCase):
def test_function_py2(self):
from testhelpers import modulewide_typecheck_testhelper_py2 as mth
Expand Down