-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Update tests to prepare for variable redefinition with new type #6095
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
After the planned change, test cases no longer can rely on code like this generating an error, since the two assignments create two logically separate variables with independent types: ``` x = 0 x = '' # Currently an error, but likely not in the future ``` Instead, we use an if statement to prevent redefinition. This works since redefinition will only happen unconditionally: ``` x = 0 if int(): x = '' # Still an error ``` Even this may stop working in the future, and we may need to another major test case update then. If we always use the `if int():` idiom, finding things that need changing will be easy. In new test cases we should generally avoid using assignment statements such as the above to generate errors. Instead, we can use `reveal_type()` and calls to functions with incompatible argument types, for example.
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 have some random comments below, I summarize them all here:
First, I would gather all the patterns used, and add a single explanatory comment at the top of one of the test files, so this information is not lost. I have found:
if int(): ...
if str(): ...
if 1: ...
x
(look-up in the same scope)def dummy(): x
(look-up from a nested scope)
It would be helpful to add some motivation for each of those.
Second, I am worried about shadowing final names. This kind of defeats the purpose. We can discuss this separately however, but I would prefer to not modify those tests.
Third, interaction with Any
is a bit unexpected, it can create both "false" positives, and "false" negatives (depending on the order), again probably we can discuss this separately.
Finally, there is a use case that is problematic with current strategy (allow redefinition only for non-overlapping life-times):
x = 1.1
x = 1
if blah():
x = 1.2
currently this works, but will (IIUC) cause an error with the new logic. I think it would be better to have a mini-design doc to discuss strategy and details of the new logic.
if int(): | ||
d, a = b, b # E: Incompatible types in assignment (expression has type "B", variable has type "A") | ||
if int(): | ||
d, d = d, d, d # E: Too many values to unpack (2 expected, 3 provided) |
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.
Why if int():
is needed here?
d[a()] = a # E: "A" not callable | ||
|
||
a = d[a] | ||
if int(): | ||
a = d[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.
I am also not sure why it is needed here. There is no error on this line. Will this create a new variable with type Any
that shows the one with type A
?
@@ -74,11 +77,11 @@ class N(IntEnum): | |||
x = 1 | |||
y = 1 | |||
n = 1 | |||
n = N.x # Subclass of int, so it's okay | |||
if int(): | |||
n = N.x # Subclass of int, so it's okay |
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 one is also not clear. I assume we want to test that there is no error even with the new semantics.
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. If we omitted the if statement, the assignment would not test anything interesting.
class A: | ||
pass | ||
|
||
[case testFloatLiteral] | ||
a = 0.0 | ||
b = None # type: A | ||
b = 1.1 # E: Incompatible types in assignment (expression has type "float", variable has type "A") | ||
a = 1.1 | ||
if 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.
What is specific about str()
here so it is used instead of 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.
int
was not defined in the builtins fixture, so I decided to use str()
instead.
test-data/unit/check-final.test
Outdated
@@ -339,22 +339,29 @@ class C: | |||
from typing import Final | |||
|
|||
x: Final = 1 | |||
x |
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 strongly prefer to not shadow final names, since this kind of defeats the purpose.
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 implementation does some additional checks in a later pass after renaming to prevent the redefinition of final names. It should preserve the existing semantics of final names. Or is there something else you are worried about?
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 x
is reference is used to trigger the renaming machinery. Otherwise the second definition would be trivially rejected, as it would not count as a new definition. My implementation only renames a variable if it has been read, since this shouldn't be treated as a redefinition:
x = None # type: List[int]
x = [] # Not a redefinition
@@ -300,6 +307,7 @@ class B(A): | |||
b = 1 | |||
|
|||
x = A() | |||
def f(): x # Prevent redefinition |
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 would create a full list of "hack" patterns used, and place it in one of the test files as a comment.
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'll add a separate file where I describe them, as these aren't specific to any particular test file.
@@ -310,17 +310,15 @@ main:4: error: Invalid type "__main__.T" | |||
from typing import NewType | |||
|
|||
a = 3 | |||
a = NewType('a', int) | |||
def f(): 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.
Allowing redefinition with a different kind of symbol looks dangerous from the point of view of fine grained incremental, but maybe it is OK.
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.
Since the implementation uses renaming, the this shouldn't be dangerous as long as the renaming is implemented correctly. The implementation I have does renaming before semantic analysis so we can't always detect the kind of symbol being defined. Function and class definitions won't be able to redefine variables.
A good idea! I'll add some documentation about these.
I think that final semantics should be effectively the same. I don't remember why I modified the test cases -- it may have been due to changes in the details of error messages. I'll double check this before merging.
This can be discussed once we have the proposed implementation for the feature. I'd argue that the new behavior is usually an improvement -- they caught many false negatives in Dropbox codebases, and they also fix false positives. There is no explicit interaction with
Yes, this could generate new errors. I didn't see many examples of this in the Dropbox S codebase though, so it's probably rare. |
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.
Thanks for updates! This looks good now.
After the planned change, test cases no longer can rely on code
like this generating an error, since the two assignments will
create two logically separate variables with independent types:
Instead, the idea is to use an if statement to prevent redefinition.
This works since redefinition will only happen unconditionally:
Even this may stop working in the future, and we may need to
another major test case update then. If we always use the
if int():
idiom, finding things that need changing will beeasy. (Sometimes
if int():
didn't work and I usedif 1:
instead.)
In new test cases we should generally avoid using assignment
statements such as the above to generate errors. Instead, we
can use
reveal_type()
and calls to functions with incompatibleargument types, for example.