-
Notifications
You must be signed in to change notification settings - Fork 258
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
Conversation
There was a problem hiding this 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): |
There was a problem hiding this comment.
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.
I do indeed want a much smaller typing.py |
@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
And there by "a PR later" I meant exactly this PR, but it looks like you closed #250 by #289. |
Actually 250 was auto-closed by GitHub because of the phrase "fix #250" in your commit message. (I reopened it.) |
There was a problem hiding this 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: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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],)) |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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]
.
…for generic instances
@gvanrossum |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
@gvanrossum Thank you for review! Fixed both points. |
OK godspeed! |
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. |
SGTM. I originally gave them all different names to ensure I would not
confuse myself, but I think in retrospect it's better to unify.
|
@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]
andNode
have identical__bases__
and identical__mro__[1:]
(except for the first item, since it is the class itself).__mro__
is changed very little, at most one bareGeneric
appears in__mro__
.__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).