-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
@@ -1162,23 +1162,45 @@ class TypeType(Type): | |||
# a generic class instance, a union, Any, a type variable... | |||
item = None # type: Type | |||
|
|||
# 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 useTypeInfo
instead ofInstance
, but that would likely be a larger change. - Also provide a
make_normalized
static 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
super().__init__(line, column) | ||
if isinstance(item, CallableType) and item.is_type_obj(): | ||
self.item = item.fallback | ||
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.make
that accepts the same arguments asTypeType
constructor, but can return aUnionType
if appropriate (i.e., if passed aUnionType
as an item)TypeType()
withTypeType.make()
TypeType()
constructorThe potential alternative would be to:
TypeType
into_TypeType
TypeType()
that does precisely what myTypeType.make
doesTypeType()
function, while roughly equivalent toTypeType
class, isn't quite the same because we treat classes andCallable
s differently)Also in this PR:
testTypeUsingTypeCClassMethodUnion
, since it can now use the correct expect output