diff --git a/pytypes/type_util.py b/pytypes/type_util.py index 29cf282..5576c6b 100644 --- a/pytypes/type_util.py +++ b/pytypes/type_util.py @@ -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__ @@ -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]): diff --git a/pytypes/typechecker.py b/pytypes/typechecker.py index 2427486..7d26ca6 100644 --- a/pytypes/typechecker.py +++ b/pytypes/typechecker.py @@ -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: @@ -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: diff --git a/tests/test_typechecker.py b/tests/test_typechecker.py index e0bd8e0..1ce5a07 100644 --- a/tests/test_typechecker.py +++ b/tests/test_typechecker.py @@ -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)) @@ -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