Skip to content

A real fix for issue #250 (failure with mock) #295

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

Merged
merged 6 commits into from
Oct 21, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 42 additions & 1 deletion python2/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,13 +603,54 @@ def test_orig_bases(self):
class C(typing.Dict[str, T]): pass
self.assertEqual(C.__orig_bases__, (typing.Dict[str, T],))

def test_naive_runtime_checks(self):
def naive_dict_check(obj, tp):
# Check if a dictionary conforms to Dict type
if len(tp.__parameters__) > 0:
return NotImplemented
Copy link
Member

Choose a reason for hiding this comment

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

NotImplemented is not a great value to return here -- it's really only supposed to be used by binary operators (e.g. __add__) to indicate that the reverse variant should be tried (i.e. __radd__). Maybe raising NotImplementedError would be better?

While this is just a test, it will be used as an example and copied, and raising makes it clearer that that's an unimplemented feature of the naive check.

if tp.__args__:
KT, VT = tp.__args__
return all(isinstance(k, KT) and isinstance(v, VT)
for k, v in obj.items())
self.assertTrue(naive_dict_check({'x': 1}, typing.Dict[typing.Text, int]))
self.assertFalse(naive_dict_check({1: 'x'}, typing.Dict[typing.Text, int]))
self.assertIs(naive_dict_check({1: 'x'}, typing.Dict[typing.Text, T]), NotImplemented)

def naive_generic_check(obj, tp):
# Check if an instance conforms to the generic class
if not hasattr(obj, '__orig_class__'):
return NotImplemented
return obj.__orig_class__ == tp
class Node(Generic[T]): pass
self.assertTrue(naive_generic_check(Node[int](), Node[int]))
self.assertFalse(naive_generic_check(Node[str](), Node[int]))
self.assertFalse(naive_generic_check(Node[str](), List))
self.assertIs(naive_generic_check([1,2,3], Node[int]), NotImplemented)

def naive_list_base_check(obj, tp):
# Check if list conforms to a List subclass
return all(isinstance(x, tp.__orig_bases__[0].__args__[0])
for x in obj)
class C(List[int]): pass
self.assertTrue(naive_list_base_check([1, 2, 3], C))
self.assertFalse(naive_list_base_check(['a', 'b'], C))

def test_multi_subscr_base(self):
T = TypeVar('T')
U = TypeVar('U')
V = TypeVar('V')
# these should just work
class C(List[T][U][V]): pass
class D(C, List[T][U][V]): pass
self.assertEqual(C.__parameters__, (V,))
self.assertEqual(D.__parameters__, (V,))
self.assertEqual(C[int].__parameters__, ())
self.assertEqual(D[int].__parameters__, ())
self.assertEqual(C[int].__args__, (int,))
self.assertEqual(D[int].__args__, (int,))
self.assertEqual(C.__bases__, (List,))
self.assertEqual(D.__bases__, (C, List))
self.assertEqual(C.__orig_bases__, (List[T][U][V],))
self.assertEqual(D.__orig_bases__, (C, List[T][U][V]))

def test_pickle(self):
global C # pickle wants to reference the class by name
Expand Down
14 changes: 9 additions & 5 deletions python2/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@ class GenericMeta(TypingMeta, abc.ABCMeta):
"""Metaclass for generic types."""

def __new__(cls, name, bases, namespace,
tvars=None, args=None, origin=None, extra=None):
tvars=None, args=None, origin=None, extra=None, orig_bases=None):
if tvars is not None:
# Called from __getitem__() below.
assert origin is not None
Expand Down Expand Up @@ -1086,7 +1086,7 @@ def __new__(cls, name, bases, namespace,
", ".join(str(g) for g in gvars)))
tvars = gvars

orig_bases = bases
initial_bases = bases
if extra is None:
extra = namespace.get('__extra__')
if extra is not None and type(extra) is abc.ABCMeta and extra not in bases:
Expand All @@ -1104,8 +1104,9 @@ def __new__(cls, name, bases, namespace,
self.__extra__ = extra
# Speed hack (https://github.com/python/typing/issues/196).
self.__next_in_mro__ = _next_in_mro(self)
if origin is None:
self.__orig_bases__ = orig_bases
# Preserve base classes on subclassing (__bases__ are type erased now).
if orig_bases is None:
self.__orig_bases__ = initial_bases

# This allows unparameterized generic collections to be used
# with issubclass() and isinstance() in the same way as their
Expand Down Expand Up @@ -1193,7 +1194,8 @@ def __getitem__(self, params):
tvars=tvars,
args=args,
origin=self,
extra=self.__extra__)
extra=self.__extra__,
orig_bases=self.__orig_bases__)

def __instancecheck__(self, instance):
# Since we extend ABC.__subclasscheck__ and
Expand Down Expand Up @@ -1240,6 +1242,8 @@ def __new__(cls, *args, **kwds):
else:
origin = _gorg(cls)
obj = cls.__next_in_mro__.__new__(origin)
if '__dict__' in cls.__dict__:
obj.__orig_class__ = cls
obj.__init__(*args, **kwds)
return obj

Expand Down
44 changes: 43 additions & 1 deletion src/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,13 +630,55 @@ def test_orig_bases(self):
class C(typing.Dict[str, T]): ...
self.assertEqual(C.__orig_bases__, (typing.Dict[str, T],))
Copy link
Member

Choose a reason for hiding this comment

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

I know there are no docs for __parameters__ etc. either, but I would still like to see some kind of example code that takes a class like this and recovers what it means given the various new dunder methods (__origin__, __orig_bases__, __parameters__). E.g. if I had a dict {x: y} how would I check that it's a subclass of Dict[str, T]? (The answer should reveal that the type of x must be str and T must be the type of y.)

Copy link
Member Author

Choose a reason for hiding this comment

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

It is quite difficult to write a reasonable runtime type check functions. I just added three naive functions just to illustrate how to use the dunder attributes for typical type checking tasks.

In process of doing this I realized that it is difficult to perform runtime type checks since type erasure happens not only at subclassing, but also at generic class instantiation. In the new commit I added __orig_class__ to instances (if subclass allows this, i.e., it does not use __slots__) that saves a reference to original class before type erasure.
So that now Node[int]().__class__ is Node, while Node[int]().__orig_class__ is Node[int].


def test_naive_runtime_checks(self):
def naive_dict_check(obj, tp):
# Check if a dictionary conforms to Dict type
if len(tp.__parameters__) > 0:
return NotImplemented
if tp.__args__:
KT, VT = tp.__args__
return all(isinstance(k, KT) and isinstance(v, VT)
for k, v in obj.items())
self.assertTrue(naive_dict_check({'x': 1}, typing.Dict[str, int]))
self.assertFalse(naive_dict_check({1: 'x'}, typing.Dict[str, int]))
self.assertIs(naive_dict_check({1: 'x'}, typing.Dict[str, T]), NotImplemented)

def naive_generic_check(obj, tp):
# Check if an instance conforms to the generic class
if not hasattr(obj, '__orig_class__'):
return NotImplemented
return obj.__orig_class__ == tp
class Node(Generic[T]): ...
self.assertTrue(naive_generic_check(Node[int](), Node[int]))
self.assertFalse(naive_generic_check(Node[str](), Node[int]))
self.assertFalse(naive_generic_check(Node[str](), List))
self.assertIs(naive_generic_check([1,2,3], Node[int]), NotImplemented)

def naive_list_base_check(obj, tp):
# Check if list conforms to a List subclass
return all(isinstance(x, tp.__orig_bases__[0].__args__[0])
for x in obj)
class C(List[int]): ...
self.assertTrue(naive_list_base_check([1, 2, 3], C))
self.assertFalse(naive_list_base_check(['a', 'b'], C))

def test_multi_subscr_base(self):
T = TypeVar('T')
U = TypeVar('U')
V = TypeVar('V')
# these should just work
class C(List[T][U][V]): ...
class D(C, List[T][U][V]): ...
self.assertEqual(C.__parameters__, (V,))
self.assertEqual(D.__parameters__, (V,))
self.assertEqual(C[int].__parameters__, ())
self.assertEqual(D[int].__parameters__, ())
self.assertEqual(C[int].__args__, (int,))
self.assertEqual(D[int].__args__, (int,))
self.assertEqual(C.__bases__, (List,))
self.assertEqual(D.__bases__, (C, List))
self.assertEqual(C.__orig_bases__, (List[T][U][V],))
self.assertEqual(D.__orig_bases__, (C, List[T][U][V]))


def test_pickle(self):
global C # pickle wants to reference the class by name
Expand Down
14 changes: 9 additions & 5 deletions src/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ class GenericMeta(TypingMeta, abc.ABCMeta):
"""Metaclass for generic types."""

def __new__(cls, name, bases, namespace,
tvars=None, args=None, origin=None, extra=None):
tvars=None, args=None, origin=None, extra=None, orig_bases=None):
if tvars is not None:
# Called from __getitem__() below.
assert origin is not None
Expand Down Expand Up @@ -979,7 +979,7 @@ def __new__(cls, name, bases, namespace,
", ".join(str(g) for g in gvars)))
tvars = gvars

orig_bases = bases
initial_bases = bases
if extra is not None and type(extra) is abc.ABCMeta and extra not in bases:
bases = (extra,) + bases
bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b for b in bases)
Expand All @@ -995,8 +995,9 @@ def __new__(cls, name, bases, namespace,
self.__extra__ = extra
# Speed hack (https://github.com/python/typing/issues/196).
self.__next_in_mro__ = _next_in_mro(self)
if origin is None:
self.__orig_bases__ = orig_bases
# Preserve base classes on subclassing (__bases__ are type erased now).
if orig_bases is None:
self.__orig_bases__ = initial_bases

# This allows unparameterized generic collections to be used
# with issubclass() and isinstance() in the same way as their
Expand Down Expand Up @@ -1084,7 +1085,8 @@ def __getitem__(self, params):
tvars=tvars,
args=args,
origin=self,
extra=self.__extra__)
extra=self.__extra__,
orig_bases=self.__orig_bases__)

def __instancecheck__(self, instance):
# Since we extend ABC.__subclasscheck__ and
Expand Down Expand Up @@ -1128,6 +1130,8 @@ def __new__(cls, *args, **kwds):
else:
origin = _gorg(cls)
obj = cls.__next_in_mro__.__new__(origin)
if '__dict__' in cls.__dict__:
obj.__orig_class__ = cls
Copy link
Member

Choose a reason for hiding this comment

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

I think I'd just catch AttributeError here -- the "dict in dict" idiom feels obscure.

obj.__init__(*args, **kwds)
return obj

Expand Down