-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Self Type #2193
Self Type #2193
Conversation
@JukkaL It would be nice if you'll skim to see that nothing inherently wrong is going on here. |
I did a very quick pass and the feedback I have is that this needs more time to study than I have right now :-) I'll take a more careful look tomorrow. |
INVARIANT = 0 # type: int | ||
COVARIANT = 1 # type: int | ||
CONTRAVARIANT = 2 # type: int | ||
# TODO: SELF_VARIANCE = 2 # type: int |
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 do you think? I am not sure what this is, For now simple INVARIANT seems to suffice
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.
INVARIANT should be fine. I don't see a need for special variance.
) | ||
|
||
|
||
class TypeVarInstantiator(TypeTranslator): |
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.
Is it implemented somewhere else already?
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.
Look at mypy/expandtype.py
.
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.
When I use it I get partial types. I must be missing something about how the whole thing works.
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 partial types appear only when I use reveal_type, and it seems to be orthogonal to self-type specifically (it happens with normal generic functions too).
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.
My mistake - it was the erased
flag. I turned it off for this "inference".
I'm trying to kick the tires here and I don't understand why this example doesn't work: from typing import TypeVar
T = TypeVar('T', bound='C')
class C:
def copy(self: T) -> T:
return self
def clone(arg: T) -> T:
return arg.copy() The copy() method type-checks wonderfully, but on the very last line I get:
|
Sorry, I've been busy with other stuff and haven't been able to review this. Tomorrow should be better! |
Similar: from typing import Type, TypeVar
T = TypeVar('T', bound='C')
class C:
@classmethod
def new(cls: Type[T]) -> T:
return cls()
def make(cls: Type[T]) -> T:
return cls.new() Gives:
|
@@ -1648,9 +1647,10 @@ def accept(self, visitor: NodeVisitor[T]) -> T: | |||
# | |||
# If T is contravariant in Foo[T], Foo[object] is a subtype of | |||
# Foo[int], but not vice versa. | |||
CONTRAVARIANT = -1 # type: int |
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 change of the constant value seems unrelated to the rest of diff and unnecessary.
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.
My poor git skills seems to have failed me. I did revert this one.
@@ -698,6 +705,31 @@ def deserialize(cls, data: JsonDict) -> 'CallableType': | |||
is_classmethod_class=data['is_classmethod_class'], | |||
) | |||
|
|||
def bind_self(self, actual_self: Type = None) -> 'CallableType': |
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.
Our convention is to keep Type
(and Node
) subclasses as almost pure data. This is complex enough that it would be better moved to a module-level function. I suspect you can use a type variable ranging over FunctionLike
and CallableType
to give it a precise enough type.
class A: | ||
def copy(self: T) -> T: | ||
if B(): | ||
return A() |
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.
Try to use the # E: error message
convention for the error messages for readability and consistency. If it doesn't work because of all the notes (it shouldn't be a problem here), use a # Error
comment on lines that generate an error to make it easier to match errors to lines. You can leave the main: note: In ...
in the [out]
section and add # E: ...
comments here.
@@ -1608,13 +1608,17 @@ def analyze_super(self, e: SuperExpr, is_lvalue: bool) -> Type: | |||
return AnyType() | |||
if not self.chk.in_checked_function(): | |||
return AnyType() | |||
selftype = self.chk.function_stack[-1].arguments[0].variable.type | |||
return analyze_member_access(name=e.name, typ=fill_typevars(e.info), node=e, | |||
# fill_typevars(e.info) erases type variables |
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 comment isn't right. fill_typevars
adds type variables, erasure means something like stripping away type variables or replacing them with the upper bound / Any
, which isn't happening here. If I have a class define as class C(Generic[T]):
then fill_typevars
will create a type C[T]
when called with the type info for C
as an argument.
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.
So I did get it wrong there. Yet if I pass fill_typevars()
as actual_type I get the erasure on access.
I'm completely confused now :\
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. wait. it's only confusing because we talk about different variables... sorry. I'll update.
args = self.chk.function_stack[-1].arguments | ||
# An empty args with super() is an error, but we need something in declared_self | ||
declared_self = args[0].variable.type if args else erased_self | ||
return analyze_member_access(name=e.name, typ=erased_self, node=e, |
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.
All the checker tests pass if I pass declared_self
as the typ
argument. Can we just get rid of erased_self
here, as I don't see any use for it? We can return Any
without calling analyze_member_access
in case there are no arguments (worth generating an error message, though). This would help resolve the actual_self
documentation issue in analyze_member_access
as well, I believe, as we can rename the argument to original_type
and it doesn't need to be special cased for this particular call site. What do you think?
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 am not sure this will not break for generic classes. But perhaps we can wait and see.
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.
Yeah, it could well break for generic classes, but as we don't have tests for them we don't know whether they work right now. Let's do that separately. Even for generic classes this is perhaps not the best approach.
|
||
This is a general operation that supports various different variations: | ||
|
||
1. lvalue or non-lvalue access (i.e. setter or getter access) |
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.
Please retain This is general ...
and the following numbered list as they are still relevant.
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.
Oops. Sorry.
Okay, this is almost ready to merge! Thanks for your patience -- this is one of trickiest type system features that I've reviewed recently. It's also going to be pretty useful, I feel. If you address the comment about the docstring and the test case(https://github.com/python/mypy/pull/2193/files#r85375637) I can merge this. Then one of us can create tasks for follow-up work. |
Thank you all - it's been a pleasure :) |
@ilevkivskyi Sorry, I think that I didn't answer your question. I'm still undecided on this issue. Let's continue the discussion in separate issues that I just created (also feel free to create additional ones for things that I missed): #2351, #2352, #2353 and #2354. |
(Reopening #2408 after the revert #2414. This fixes parts of #2090) This PR does three things: Fix the handling of TupleType (pass original_type recursively) Fix the handling of TypeType (avoid ad-hoc buggy special casing) Make NamedTuple's _replace() and _make() return selftype, serving as test case for (1) and (2) As a consequence of (1), some error messages are changed, as exemplified in check-isinstance.test. I think it's better now, but if it isn't, perhaps we should separate original_type and report_type as discussed in #2193.
Implement #1212. I don't know if that's The Right Way To Do It.
There is no attempt to check or disallow abuse of this feature.