-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Standardize Type[Union[...]] -> Union[Type[...]] #3209
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
* Add more tests for Type[Union[...]] * Force TypeType.make() to be used everywhere instead of TypeType() * Accidentally fixed testTypeUsingTypeCClassMethodUnion
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 like the idea of normalizing to a canonical representation, and I like how it is done (apart from one minor comment). This is quite significant change, so that I would like to ask @JukkaL if it is OK.
mypy/types.py
Outdated
| @staticmethod | ||
| def make(item: Type, *, line: int = -1, column: int = -1) -> Type: | ||
| if isinstance(item, UnionType): | ||
| return UnionType([TypeType.make(union_item) for union_item in item.items], |
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 it is better to use UnionType.make_union here instead.
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 looks a reasonable thing to do, but the implementation is a bit over-complicated and I'd like to see it simplified (see comments). Also can you fix the merge conflict?
mypy/types.py
Outdated
| # this class should not be created directly | ||
| # use make method which may return either TypeType or UnionType | ||
| # this is to ensure Type[Union[A, B]] is always represented as Union[Type[A], Type[B]] | ||
| def __init__(self, item: Type, *, line: int = -1, column: int = -1) -> 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.
Hmm this is pretty magical. My suggestion is to do something along these lines:
- Have a
__init__as before, but have it only acceptUnion[Any, Instance]or similar in the signature. Thus it can't be called with a union argument. Not sure what the exact set of accepted types should be. Maybe the best thing would be to useTypeInfoinstead ofInstance, but that would likely be a larger change. - Also provide a
make_normalizedstatic method that also accepts other kinds of types and call the normal constructor one or more times. It can also handleCallableType.
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.
But how would we know (statically) whether to call make_normalized or normal constructor? In some cases, we don't if the type is Union or 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.
The rule wold be to always call make_normalized if the type could be a union (or a callable). The normal constructor would only be usable for specific subtypes of Type which don't need normalization.
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.
Yes, right - and I can easily do right now.
What I meant is that in the future someone might incorrectly call TypeType(item), where item is a Type that sometimes is UnionType, and sometimes another subtype of Type. Of course, the correct approach is to do if isinstance(item, UnionType) ... else ..., but it's totally not obvious for a contributor unfamiliar with this issue (and even to me in a few months).
That would introduce a hard-to-detect bug: x may be a UnionType only rarely, so the tests won't catch it. Static typing won't catch it either because I can't tell __init__ that item can be of any subtype of Type exceptUnionType.
So what I was trying to do is enforce correctness by doing isinstance check inside TypeType; but of course, it can't be done inside __init__, so I had to disable __init__ (to prevent accidental calls to it) and add __new__.
I can easily change it though if you think it's a bit too weird.
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.
The __init__ argument type wouldn't accept UnionType so mypy can prevent you from calling it with the wrong argument type. Calling TypeType(item) would be disallowed if item has type Type.
The signature of __init__ could be something like this (not sure about the exact union type):
def __init__(self, item: Union[AnyType, Instance], ...) -> None: # no Type allowed!
...You can also mention the alternative way of constructing TypeType instances in the docstring for __init__.
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.
What I'm struggling with is, how to tell at the call site (where I create TypeType) whether it should use a regular constructor or a make_normalized. In most cases, I only know the argument is of type Type, not which particular subtype it is. So I end up having to call make_normalized everywhere. And that's dangerous because in my make_normalized I have to crash mypy if an unexpected type appears (see my last commit).
I still like the approach without __new__, I just can't figure out how to do it safely.
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.
Oh I think this will work (see final commit).
mypy/types.py
Outdated
| else: | ||
| self.item = item | ||
|
|
||
| def __new__(cls, *args, **kwargs): # type: ignore |
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.
If you follow my suggestion from above this would be unnecessary.
mypy/types.py
Outdated
| instance._init(*args, **kwargs) | ||
| return instance | ||
|
|
||
| def __copy__(self) -> 'Type': |
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.
What's this for? I think at least if you follow my suggestion this won't be needed.
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 was needed because types.copy_type uses copy.copy(t), and the default copy simply returns the (unchanged) original object which is not the right semantics here (it breaks join_types).
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, I should have said "the default copy as part of its mechanism calls __new__ without arguments, and that's not ok for TypeType.__new__.
(And to confirm, yes, of course if I get rid of __new__, this becomes unnecessary.)
|
I fixed the merge conflict, and will try to address your other comments soon. |
2a19a2b to
8d5fc1e
Compare
efd62f5 to
d729749
Compare
|
@pkch |
|
@ilevkivskyi ah right. fixed. |
|
@JukkaL @ilevkivskyi I incorporated all the comments so far, so when you get a chance can you look again? |
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.
Looks good to me. I leave this up to Jukka to merge.
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.
LGTM
This is the last commit from my #3005 PR that was just merged. @ilevkivskyi asked me to separate that commit into a standalone PR, see this discussion. Essentially, @ilevkivskyi asked me to check if
Type[Union[A, B]]is handled correctly (that is, as equivalent toUnion[Type[A], Type[B]]) in my PR. I realized that it's not, but it's also not handled correctly in existing code base (see here, for example).One way to fix it, without adding a lot of new logic to the code, is to use
Union[Type[A], Type[B]]internally to representType[Union[A, B]]. Of course, we'll need to make sure it always happens. One safe way to do that is what I've done in this PR:TypeType.makethat accepts the same arguments asTypeTypeconstructor, but can return aUnionTypeif appropriate (i.e., if passed aUnionTypeas an item)TypeType()withTypeType.make()TypeType()constructorThe potential alternative would be to:
TypeTypeinto_TypeTypeTypeType()that does precisely what myTypeType.makedoesTypeType()function, while roughly equivalent toTypeTypeclass, isn't quite the same because we treat classes andCallables differently)Also in this PR:
testTypeUsingTypeCClassMethodUnion, since it can now use the correct expect output