-
-
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
Changes from 1 commit
b55b591
f024e89
8d5fc1e
2a19a2b
d729749
1f557d5
7adac28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
raise NotImplementedError | ||
|
||
def _init(self, item: Type, *, line: int = -1, column: int = -1) -> None: | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. If you follow my suggestion from above this would be unnecessary. |
||
instance = object.__new__(cls) | ||
instance._init(*args, **kwargs) | ||
return instance | ||
|
||
def __copy__(self) -> 'Type': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. That was needed because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I should have said "the default (And to confirm, yes, of course if I get rid of |
||
return TypeType.make(self.item) | ||
|
||
@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 commentThe reason will be displayed to describe this comment to others. Learn more. I think it is better to use |
||
line=line, column=column) | ||
else: | ||
return TypeType.__new__(TypeType, item, line=line, column=column) | ||
|
||
def accept(self, visitor: 'TypeVisitor[T]') -> T: | ||
return visitor.visit_type_type(self) | ||
|
||
def serialize(self) -> JsonDict: | ||
return {'.class': 'TypeType', 'item': self.item.serialize()} | ||
|
||
@classmethod | ||
def deserialize(cls, data: JsonDict) -> 'TypeType': | ||
def deserialize(cls, data: JsonDict) -> Type: | ||
assert data['.class'] == 'TypeType' | ||
return TypeType(deserialize_type(data['item'])) | ||
return TypeType.make(deserialize_type(data['item'])) | ||
|
||
|
||
# | ||
|
@@ -1360,7 +1382,7 @@ def visit_overloaded(self, t: Overloaded) -> Type: | |
return Overloaded(items=items) | ||
|
||
def visit_type_type(self, t: TypeType) -> Type: | ||
return TypeType(t.item.accept(self), line=t.line, column=t.column) | ||
return TypeType.make(t.item.accept(self), line=t.line, column=t.column) | ||
|
||
|
||
class TypeStrVisitor(SyntheticTypeVisitor[str]): | ||
|
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:
__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.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 isUnion
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 ofType
which don't need normalization.Uh oh!
There was an error while loading. Please reload this page.
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)
, whereitem
is aType
that sometimes isUnionType
, and sometimes another subtype ofType
. Of course, the correct approach is to doif 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 aUnionType
only rarely, so the tests won't catch it. Static typing won't catch it either because I can't tell__init__
thatitem
can be of any subtype ofType
exceptUnionType
.So what I was trying to do is enforce correctness by doing
isinstance
check insideTypeType
; 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 acceptUnionType
so mypy can prevent you from calling it with the wrong argument type. CallingTypeType(item)
would be disallowed ifitem
has typeType
.The signature of
__init__
could be something like this (not sure about the exact union type):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 amake_normalized
. In most cases, I only know the argument is of typeType
, not which particular subtype it is. So I end up having to callmake_normalized
everywhere. And that's dangerous because in mymake_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).