-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Option to disallow all expressions of type Any(--disallow-any=expr) #3519
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
The option disallows all expressions of type Any except: * if a value of type Any used as a second parameter to `cast` * if a value of type Any is assigned to a variable with an explicit type annotation
(Was causing failures because of --disallow-untyped-defs)
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 good overall! I think all my comments are either nits or about naming or testing.
mypy/checker.py
Outdated
@@ -1736,7 +1736,7 @@ def check_simple_assignment(self, lvalue_type: Type, rvalue: Expression, | |||
# '...' is always a valid initializer in a stub. | |||
return AnyType() | |||
else: | |||
rvalue_type = self.expr_checker.accept(rvalue, lvalue_type) | |||
rvalue_type = self.expr_checker.accept(rvalue, lvalue_type, disallow_any=False) |
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.
Does this make sure there's an explicit type annotation? Also, I'm not convinced we want to allow this even so.
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, that's a good point. I will change that to disallow_any=lvalue_type is None
. It seems like lvalue_type
is the type from an explicit type annotation.
As for not allowing Any
expressions when assigning to a variable with an explicit type annotation, here are my thoughts.
People are going to be calling untyped functions from their code. --disallow-any=expr
needs to allow for this, otherwise no one will use it.
In my mind, there are two good ways to do it: explicit cast (call to function cast
) and implicit cast (assignment to a variable with an explicit type annotation).
The advantage of an explicit cast is that you can't do it accidentally.
However, the explicit cast syntax makes the code less readable.
Compare with an implicit cast:
# explicit cast
result = cast(float, some_long_method_name(that, (has, many), complicated=params))
# implicit cast
result = some_long_method_name(that, (has, many), complicated=params) # type: float
A big disadvantage of an implicit cast is that someone can use it accidentally. They won't look at the definition of the function they are calling and just put a type there thinking that mypy will complain if the type is wrong.
This PR has both implicit and explicit cast implemented because I think it's worth having both.
But I would be interested in hearing your reasoning for this!
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 see the same pros and cons as you -- I just weigh them differently. Mainly, I think it's important for it to be really clear where the Any
s are.
That said, IIRC it's not particularly common to annotate the result of a function call, so maybe the type annotation serves that purpose well enough. Let's go with that for now.
mypy/messages.py
Outdated
@@ -897,6 +897,10 @@ def typeddict_item_name_not_found(self, | |||
def type_arguments_not_allowed(self, context: Context) -> None: | |||
self.fail('Parameterized generics cannot be used with class or instance checks', context) | |||
|
|||
def disallowed_any_type(self, typ: Type, context: Context) -> None: | |||
self.fail('Expressions of type "Any" are disallowed ' |
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 is a bit confusing, as the expression may not have the type "Any".
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'm not sure what you mean.
Do you mean a situation when a type contains Any
(e.g. List[Any]
)?
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.
Yep, exactly.
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.
Updated the error message to say Expressions of type that contains type "Any" are disallowed (has type {})
if type is not Any
.
test-data/unit/check-flags.test
Outdated
print(x[foo(0)]) | ||
[builtins fixtures/list.pyi] | ||
[out] | ||
main:8: error: Expressions of type "Any" are disallowed (has type "Any") |
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.
It'd be better to use the inline error syntax here (which you may not have seen). Basically you can put # E: ERROR MESSAGE
at the end of a line in a test, and it will be understood as indicating the appropriate error output on that line.
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.
You'll have to break this up into multiple tests so you can do this (because there are multiple errors on some lines), but it'd be worthwhile.
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.
Thinking some more about this: I'd like to see this split up into multiple tests anyway, each testing a specific aspect (both positive and negative) of this PR (and named appropriately). E.g. testDisallowAnyAllowsAnyInCast
or similar.
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, I will split them up! :)
test-data/unit/check-flags.test
Outdated
# flags: --disallow-any=expr | ||
from typing import List | ||
|
||
blargle: List = [] |
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.
Nit: as much as I like blargle
, l
is probably better here.
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 well, yeah, l
is more understandable.
mypy/checkexpr.py
Outdated
return t.accept(HasAnyType()) | ||
|
||
|
||
class HasAnyType(TypeQuery[bool]): |
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.
Elsewhere in the file TypeQuery
is referred to as the partially qualified types.TypeQuery
. I'd do that here for consistency.
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.
Yep!
mypy/checkexpr.py
Outdated
@@ -2191,7 +2192,8 @@ def visit_backquote_expr(self, e: BackquoteExpr) -> Type: | |||
def accept(self, | |||
node: Expression, | |||
type_context: Type = None, | |||
allow_none_return: bool = False | |||
allow_none_return: bool = False, | |||
disallow_any: bool = True, |
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'd consider naming this always_allow_any
(or similar) instead. It's a bit misleading as-is.
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 was looking for a better name. always_allow_any
is good, I think.
mypy/checkexpr.py
Outdated
if (disallow_any and | ||
not self.chk.is_stub and | ||
has_any_type(typ) and | ||
'expr' in self.chk.options.disallow_any): |
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.
Nit: The last conditional should be first here. It's standard (in this codebase at least) for conditionals involving flags for the flag to be checked first. (And I think that makes the most conceptual sense, though I'm not sure I can well explain why.)
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, I think it's easier when you're looking through the code.
Also, it might be a little faster.
mypy/checkexpr.py
Outdated
@@ -204,7 +204,7 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: | |||
or isinstance(typ, NameExpr) and node and node.kind == nodes.TYPE_ALIAS): | |||
self.msg.type_arguments_not_allowed(e) | |||
self.try_infer_partial_type(e) | |||
callee_type = self.accept(e.callee) | |||
callee_type = self.accept(e.callee, disallow_any=False) |
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 are Any
exprs allowed here?
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 allows us to call function that are one of:
- unresolved
- unannotated or take
Any
as a parameter
Should we disallow this? My general thinking is that we want to make --disallow-any=expr
easy to enable per-module. That way you can turn the option on for current module even if other modules are not well annotated.
As for functions that take explicitly annotated Any
parameters, I think in a world with perfect stubs, we would disallow calling those. However, that's not the case (at least now) and, for instance, print
function accept arguments of type Any
. This would mean that you can't call print
with --disallow-any=expr
enabled.
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.
However nothing would be lost if print
's args were converted to object
.
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.
Did you mean updated in typeshed
?
Or silently treat them as object
if this flag is enabled? I think the second is a good idea but would be really hard to implement, from a technical viewpoint.
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 he means updated in typeshed. Type checking-wise, from the caller's perspective, there's no difference between an an argument of type Any
and and argument of type object
.
Anyway, I don't think this should be allowed. The primary purpose of this flag is not to be something that's easy to enable -- it's to provide clear, strong guarantees about how thoroughly a module is type checked. In my mind, that sets a high bar for any exceptions we'd consider making.
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 means we should probably go through all stubs in typeshed and parameters of type Any
to object
. I can look into it. Just to confirm, it shouldn't break anything, right?
I see your point.
I still think we should not give an error if a user is calling a function that takes Any
. Often, they can't modify the stubs of the function they are calling.
That being said, I'm okay with disallowing this and then seeing if it's a problem for the users.
How should we handle unresolved functions? (I imagine this would be the case when --disallow-any=expr
is used alongside --ignore-missing-imports
)
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.
It wouldn't break anything, but that's not a change we want to make: a function taking Any
and a function taking object
implies pretty different things about what that function supports.
I think it's reasonable for now to disallow functions that have Any
as an argument. We may need to relax this requirement later, but it's easier to start strict and then remove errors if they're too much.
Unresolved functions should definitely be errors in this mode. They're unambiguously expressions with type Any
.
mypy/messages.py
Outdated
@@ -6,7 +6,7 @@ | |||
import re | |||
import difflib | |||
|
|||
from typing import cast, List, Dict, Any, Sequence, Iterable, Tuple | |||
from typing import cast, List, Dict, Any, Sequence, Iterable, Tuple, Optional |
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.
Unused import.
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.
Good catch!
I changed my PyCharm settings to complain if there's an unused import when I'm committing.
Addressed first round of code review feedback! |
mypy/checker.py
Outdated
rvalue) | ||
is_cast = isinstance(rvalue, CallExpr) and rvalue.is_cast() | ||
init_type = self.expr_checker.accept(rvalue, always_allow_any=is_cast) | ||
self.infer_variable_type(inferred, lvalue, init_type, rvalue) |
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 the purpose of this change?
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.
For the following case:
m = cast(Any, Foo().g)
After splitting up the test cases like you suggested, I realized that that case doesn't work.
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.
Actually, I am going to roll back this particular change because of what we discussed here.
mypy/messages.py
Outdated
else: | ||
infix = 'type that contains type' | ||
self.fail('Expressions of {} "Any" are disallowed ' | ||
'(has type {})'.format(infix, self.format(typ)), context) |
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 this may still need some wordsmithing. "Expressions of type that contains type 'Any' are disallowed" is not very easy to parse, IMO.
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.
Also Expressions of type "Any" are disallowed (has type "Any")
is redundant, which could be confusing.
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.
How about Expression type contains "Any" (has type {})
?
If the actual type is Any
, we can say Expressions has type "Any"
.
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's great!
test-data/unit/check-flags.test
Outdated
return s # E: Expressions of type "Any" are disallowed (has type "Any") | ||
|
||
g(0) | ||
g('hello') |
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 the purpose of this line? It seems like it's testing the same thing as the previous.
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.
Yep, it does test the same thing. I will remove this line.
test-data/unit/check-flags.test
Outdated
def g(s): | ||
return s # E: Expressions of type "Any" are disallowed (has type "Any") | ||
|
||
g(0) |
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.
It'd be nice to put a short comment on this line explaining what it's testing and why it's not an error.
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.
After what we discussed here, it will be an error.
test-data/unit/check-flags.test
Outdated
v = g(1) # E: Expressions of type "Any" are disallowed (has type "Any") | ||
w: int = g(1) | ||
|
||
[case testDisallowAnyExprExplicitAnyParam] |
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.
Not sure I fully understand what this test is testing.
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.
It's the same as testDisallowAnyExprUnannotatedFunction
except this time function is annotated but type parameter is explicitly Any
.
test-data/unit/check-flags.test
Outdated
g: Any = 2 | ||
|
||
z = cast(int, Foo().g) | ||
m = cast(Any, Foo().g) |
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 don't think this should be allowed. You won't be able to use m
without error!
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.
Good point, will change.
test-data/unit/check-flags.test
Outdated
g: Any = 2 | ||
|
||
z: int = Foo().g | ||
m: Any = Foo().g |
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 probably shouldn't be allowed either. Also, could you add a comment-style annotation to this test?
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.
Good point, will change.
test-data/unit/check-flags.test
Outdated
m: Any = Foo().g | ||
[builtins fixtures/list.pyi] | ||
|
||
[case testDisallowAnyExprAllowsNonExistentMembers] |
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 this test really unique? Given the behavior of the other tests, it seems like this is primarily testing that calling a nonexistent function returns Any
after the error, which doesn't need to be tested here.
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, you are right. I will remove the test :)
I pushed some commits that should address almost all of second round of code review.
|
test-data/unit/check-flags.test
Outdated
|
||
# because expected type is List[Any] the inferred type of [''] is List[Any], which causes | ||
# more than one error on the line below | ||
g(['']) |
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.
As stated in the comment above, expecged type is List[Any]
, which causes the inferred type of ['']
to be List[Any]
.
Honestly, this behavior seems correct but the user gets a whole bunch of errors that are hard to understand.
@ddfisher 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've changed my mind on the functions-with-arguments-of-type-Any issue -- I think it's too hard for users to understand, and isn't necessarily in the sprit of this flag. I think it'd be better to allow those instead. That should help with a lot of confusing errors.
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.
Just a note that g([''])
is still an going to be an error because type of ['']
is inferred with expected type of List[Any]
. and ['']
technically has type List[Any]
# Conflicts: # mypy/main.py
docs/source/command_line.rst
Outdated
mypy will output an error unless the expression is immediately | ||
used as an argument to ``cast`` or assigned to a variable with an | ||
explicit type annotation. Note that declaring a variable of type ``Any`` | ||
or casting to type ``Any`` is not allowed. |
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.
should this be "is allowed"
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.
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.
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.
A few nitpicks and a functionality change. Sorry for the churn.
Remaining things to do (per in person convo):
- Remove error message for functions with
Any
arguments. - Update error messages with the better wording you came up with.
- Fix comment nitpicks.
mypy/checkexpr.py
Outdated
@@ -185,7 +185,7 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: | |||
"""Type check a call expression.""" | |||
if e.analyzed: | |||
# It's really a special form that only looks like a call. | |||
return self.accept(e.analyzed, self.type_context[-1]) | |||
return self.accept(e.analyzed, self.type_context[-1], always_allow_any=True) |
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, exactly, is this allowing? At a minimum, I think this needs a comment about why always_allow_any=True
.
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'm not totally sure. I removed it and none of the tests failed, so I am going to remove it. I also played around with it a little bit and didn't find any difference.
I did some investigation what it could be and I think it was me trying to allow cast(Any, foo)
.
But since then we decided to not allow that.
@@ -204,7 +204,7 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: | |||
or isinstance(typ, NameExpr) and node and node.kind == nodes.TYPE_ALIAS): | |||
self.msg.type_arguments_not_allowed(e) | |||
self.try_infer_partial_type(e) | |||
callee_type = self.accept(e.callee) | |||
callee_type = self.accept(e.callee, always_allow_any=True) |
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 always_allow_any=True
here so we can give a more specific error message in check_arg
? If so, could you add a comment about that?
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 it is.
I think we are not giving any errors for calls to function that accept Any
anymore.
Should I still add 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 think it's still worth a quick comment, yeah.
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.
After talking about this in person, we decided that adding info about calling untyped functions to documentation would be enough.
test-data/unit/check-flags.test
Outdated
|
||
# because expected type is List[Any] the inferred type of [''] is List[Any], which causes | ||
# more than one error on the line below | ||
g(['']) |
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've changed my mind on the functions-with-arguments-of-type-Any issue -- I think it's too hard for users to understand, and isn't necessarily in the sprit of this flag. I think it'd be better to allow those instead. That should help with a lot of confusing errors.
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 is almost there! Just a couple more things to fix. Thanks for bearing through this super long review.
test-data/unit/check-flags.test
Outdated
[case testDisallowAnyExprUnannotatedFunction] | ||
# flags: --disallow-any=expr | ||
def g(s): | ||
return s # E: Expression has type "Any" |
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 for not noticing this earlier, but I don't think we should give this warning in unannotated functions. Should be easy to fix, though!
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, not reporting errors in unannotated functions makes sense.
Do you think it's worth mentioning it in documentation?
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.
@ddfisher I think Github may have hidden this comment. I think that's something worth including in documentation.
What's your opinion on this?
@@ -515,3 +515,59 @@ x, y = 1, 2 # type: Unchecked, Unchecked | |||
[out] | |||
main:4: error: Type of variable becomes "Any" due to an unfollowed import | |||
main:6: error: A type on this line becomes "Any" due to an unfollowed import | |||
|
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.
It'd be nice to have some tests for the basic functionality. These tests do a good job of exercising the more interesting edge cases, but it's worth testing putting an Any
type in a variety of expressions to make sure it's caught/the behavior is what we expect.
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.
Let me know if you have any other test ideas.
Thanks for reviewing so many changed and iterations! |
Looks good! Thanks again for bearing through the long review! 🎉 |
The option disallows all expressions of type Any except:
cast