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

Conversation

ilevkivskyi
Copy link
Member

@gvanrossum
This PR actually fixes #250

The main idea here is optimizing generics for cases where a type information is added to existing code.
Now:

  • Node[int] and Node have identical __bases__ and identical __mro__[1:] (except for the first item, since it is the class itself).
  • After addition of typing information (i.e. making some classes generic), __mro__ is changed very little, at most one bare Generic appears in __mro__.
  • Consequently, only non-parameterized generics appear in __bases__ and __mro__[1:].

Interestingly, this could be achieved in few lines of code and no existing test break.

On the positive side of this approach, there is very little chance that existing code (even with sophisticated "magic") will break after addition of typing information.

On the negative side, it will be more difficult for runtime type-checkers to perform decorator-based type checks (e.g. enforce method overriding only by consistent methods). Essentially, now type erasure happens partially at the class creation time (all bases are reduced to origin).

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

Hm, I don't like losing the extra info from the bases and mro. While Mark probably wants me to erase all type information at runtime (leading to a much smaller typing.py) I prefer to save enough information so that runtime introspection can work.

E.g. if I write

T = TypeVar('T')
class C(Dict[str, T]): pass

I want to be able to reconstruct that T corresponds to the value of the dict.

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)
if any(isinstance(b, GenericMeta) and b is not Generic for b in bases):
Copy link
Member

Choose a reason for hiding this comment

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

This could use a comment explaining what's happening -- it looks like the idea is to remove bare Generic from the bases if there are other generic-derived bases, but to leave generic if if not.

@markshannon
Copy link
Member

I do indeed want a much smaller typing.py
After all, if whoever is writing this mythical runtime type-checker can implement a useful type-checker, then they can manage some variation on ast.parse(inspect.getsource(...))

@ilevkivskyi
Copy link
Member Author

ilevkivskyi commented Oct 10, 2016

@gvanrossum
I added the comment you requested and I propose a compromise solution: in latest commit I added a field __orig_bases__ to non-parameterized generics that preserves original bases before we start all manipulations with __extra__ and __bases__. This way:

  • The fix still works (and should prevent other bugs in existing code after addition of typing info)
  • Only some classes in typing.py get slightly bigger (as compared to Mark's ideal).
  • One can extract all the necessary original typing info at runtime using __orig_bases__ and little effort.

@gvanrossum
Copy link
Member

gvanrossum commented Oct 10, 2016 via email

@ilevkivskyi
Copy link
Member Author

@gvanrossum

Can you explain how this fixes #250 better than the previous fix for that issue?

Oh, sorry, I should have started from this. There was no fix. I just mentioned it in #289

A side note, while playing with this I have found an easy way to fix #250. I will submit a PR later when we will decide on the permanent fix for this one.

And there by "a PR later" I meant exactly this PR, but it looks like you closed #250 by #289.
Sorry for misunderstanding.

@gvanrossum
Copy link
Member

gvanrossum commented Oct 11, 2016

Actually 250 was auto-closed by GitHub because of the phrase "fix #250" in your commit message. (I reopened it.)

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

Sorry that these reviews take so long to turn around. Many things I can review between two bytes of breakfast, but typing stuff requires real thought. :-)

self.__parameters__ = tvars
self.__args__ = args
self.__origin__ = origin
self.__extra__ = extra
# Speed hack (https://github.com/python/typing/issues/196).
self.__next_in_mro__ = _next_in_mro(self)
if origin is None:
Copy link
Member

Choose a reason for hiding this comment

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

This condition is a little mysterious. It seems to say "if we were called from a class statement" (per line 947). I wonder if it would be more straightforward to set __orig_bases__ exactly in case orig_bases != bases? Or maybe not?

Copy link
Member Author

Choose a reason for hiding this comment

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

I changed this a bit, to make it more clear and convenient to use. Now __orig_bases__ is always created on subclassing, and then copied on __getitem__. So that one don't need to use _gorg to find original bases, they are accessible immediately. I added a comment in code as well.

T = TypeVar('T')
U = TypeVar('U')
V = TypeVar('V')
# these should just work
Copy link
Member

Choose a reason for hiding this comment

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

That's slang for "these shouldn't crash", but it would be pedagogical to show what the various new dunder attributes of C and D actually are.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, fixed this.

def test_orig_bases(self):
T = TypeVar('T')
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].

@ilevkivskyi
Copy link
Member Author

@gvanrossum
I agree with your comments, please take a look at the new version (details of changes are in replies to your comments in code).

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

Just a few nits -- I'm pretty positive about this now!

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.

@@ -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.

@ilevkivskyi
Copy link
Member Author

@gvanrossum Thank you for review! Fixed both points.

@gvanrossum gvanrossum merged commit a38bdac into python:master Oct 21, 2016
@gvanrossum
Copy link
Member

OK godspeed!

@gvanrossum
Copy link
Member

Python 3.6 beta 3 goes out on Oct 31. We have some time to think about this still. Did you have other things you really wanted to get in?

@ilevkivskyi
Copy link
Member Author

@gvanrossum

Python 3.6 beta 3 goes out on Oct 31. We have some time to think about this still. Did you have other things you really wanted to get in?

I have an idea of how to fix all three issues #298 #299 #301 in a single PR. It should be not big. However, it will change how the types store parameters. Now, e.g. Union uses __union_params__, Callable uses __args__ and __result__, etc. I am going to change this so that all use the same scheme as Generic: __parameters__, __args__, and __origin__. This will allow to "unify" several pieces of code and fix the three mentioned issues. I think I will finish this by Monday or Tuesday.

@gvanrossum
Copy link
Member

gvanrossum commented Oct 25, 2016 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Inheriting from instantiated generic class causes mocking to fail
3 participants