Skip to content

Commit 5db5c49

Browse files
authored
Merge pull request #53 from lubieowoce/fix-getattr
Fixed infinite recursion when pytypes tries to access .__orig_class__ on an instance of a @TypeChecked class with __getattr__/__getattribute__
2 parents a2f0974 + 524a8dc commit 5db5c49

File tree

3 files changed

+70
-4
lines changed

3 files changed

+70
-4
lines changed

pytypes/type_util.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,26 @@ def _extra(tp):
162162
return None
163163

164164

165+
def _get_orig_class(obj):
166+
"""Returns `obj.__orig_class__` protecting from infinite recursion in `__getattr[ibute]__` wrapped in a `checker_tp`.
167+
(See `checker_tp` in `typechecker._typeinspect_func for context)
168+
Necessary if:
169+
- we're wrapping a method (`obj` is `self`/`cls`) and either
170+
- the object's class defines __getattribute__
171+
or
172+
- the object doesn't have an `__orig_class__` attribute
173+
and the object's class defines __getattr__.
174+
In such a situation, `parent_class = obj.__orig_class__`
175+
would call `__getattr[ibute]__`. But that method is wrapped in a `checker_tp` too,
176+
so then we'd go into the wrapped `__getattr[ibute]__` and do
177+
`parent_class = obj.__orig_class__`, which would call `__getattr[ibute]__` again, and so on.
178+
So to bypass `__getattr[ibute]__` we do this: """
179+
return object.__getattribute__(obj, '__orig_class__')
180+
181+
165182
def get_Generic_type(ob):
166183
try:
167-
return ob.__orig_class__
184+
return _get_orig_class(ob)
168185
except AttributeError:
169186
return ob.__class__
170187

@@ -499,7 +516,7 @@ def _deep_type(obj, checked, checked_len, depth = None, max_sample = None, get_t
499516
if get_type is None:
500517
get_type = type
501518
try:
502-
res = obj.__orig_class__
519+
res = _get_orig_class(obj)
503520
except AttributeError:
504521
res = get_type(obj)
505522
if depth == 0 or util._is_in(obj, checked[:checked_len]):

pytypes/typechecker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from .type_util import type_str, has_type_hints, _has_type_hints, is_builtin_type, \
3434
deep_type, _funcsigtypes, _issubclass, _isinstance, _find_typed_base_method, \
3535
_preprocess_typecheck, _raise_typecheck_error, _check_caller_type, TypeAgent, \
36-
_check_as_func, is_Tuple
36+
_check_as_func, is_Tuple, _get_orig_class
3737
from . import util, type_util
3838

3939
try:
@@ -797,7 +797,7 @@ def checker_tp(*args, **kw):
797797
parent_class = None
798798
if slf:
799799
try:
800-
parent_class = args_kw[0].__orig_class__
800+
parent_class = _get_orig_class(args_kw[0])
801801
except AttributeError:
802802
parent_class = args_kw[0].__class__
803803
elif clsm:

tests/test_typechecker.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,38 @@ def meth_2(self, c):
13721372
return 3*len(c)
13731373

13741374

1375+
@typechecked
1376+
class GetAttrDictWrapper(object):
1377+
"""Test a plausible use of __getattr__ -
1378+
A class that wraps a dict, enabling the values to be accessed as if they were attributes
1379+
(`d.abc` instead of `d['abc']`)
1380+
For example, the `pyrsistent` library does this on its dict replacement.
1381+
1382+
>>> o = GetAttrDictWrapper({'a': 5, 'b': 10})
1383+
>>> o.a
1384+
5
1385+
>>> o.b
1386+
10
1387+
>>> o.nonexistent
1388+
Traceback (most recent call last):
1389+
...
1390+
AttributeError('nonexistent')
1391+
1392+
"""
1393+
1394+
def __init__(self, dct):
1395+
# type: (dict) -> None
1396+
self.__dct = dct
1397+
1398+
def __getattr__(self, attr):
1399+
# type: (str) -> typing.Any
1400+
dct = self.__dct # can safely access the attribute because it exists so it won't trigger __getattr__
1401+
try:
1402+
return dct[attr]
1403+
except KeyError:
1404+
raise AttributeError(attr)
1405+
1406+
13751407
class TestTypecheck(unittest.TestCase):
13761408
def test_function(self):
13771409
self.assertEqual(testfunc(3, 2.5, 'abcd'), (9, 7.5))
@@ -2560,6 +2592,23 @@ def test_staticmethod(self):
25602592
tc.testmeth_static2(11, ('a', 'b'), 1.9))
25612593

25622594

2595+
class TestTypecheck_class_with_getattr(unittest.TestCase):
2596+
"""
2597+
See pull request:
2598+
https://github.com/Stewori/pytypes/pull/53
2599+
commit #:
2600+
e2523b347e52707f87d7078daad1a93940c12e2e
2601+
"""
2602+
def test_valid_access(self):
2603+
obj = GetAttrDictWrapper({'a': 5, 'b': 10})
2604+
self.assertEqual(obj.a, 5)
2605+
self.assertEqual(obj.b, 10)
2606+
2607+
def test_invalid_access(self):
2608+
obj = GetAttrDictWrapper({'a': 5, 'b': 10})
2609+
self.assertRaises(AttributeError, lambda: obj.nonexistent)
2610+
2611+
25632612
class TestTypecheck_module(unittest.TestCase):
25642613
def test_function_py2(self):
25652614
from testhelpers import modulewide_typecheck_testhelper_py2 as mth

0 commit comments

Comments
 (0)