-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Changes from 2 commits
93d4c19
6517ae4
ee76320
f059dfd
ca06a5a
edef0b1
b5c33d7
a07ad12
ebc51a8
ff16858
d6d9ecb
775c5ec
a8731e9
b0193fb
fd166b1
a2dbe35
52b9206
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 |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ | |
| PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, | ||
| true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike, | ||
| get_typ_args, set_typ_args, | ||
| StarType) | ||
| StarType, TypeQuery) | ||
| from mypy.nodes import ( | ||
| NameExpr, RefExpr, Var, FuncDef, OverloadedFuncDef, TypeInfo, CallExpr, | ||
| MemberExpr, IntExpr, StrExpr, BytesExpr, UnicodeExpr, FloatExpr, | ||
|
|
@@ -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) | ||
|
Collaborator
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. Why are
Collaborator
Author
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. This allows us to call function that are one of:
Should we disallow this? My general thinking is that we want to make As for functions that take explicitly annotated
Member
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. However nothing would be lost if
Collaborator
Author
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. Did you mean updated in Or silently treat them as
Collaborator
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 he means updated in typeshed. Type checking-wise, from the caller's perspective, there's no difference between an an argument of type 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.
Collaborator
Author
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 means we should probably go through all stubs in typeshed and parameters of type I see your point. How should we handle unresolved functions? (I imagine this would be the case when
Collaborator
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. It wouldn't break anything, but that's not a change we want to make: a function taking I think it's reasonable for now to disallow functions that have Unresolved functions should definitely be errors in this mode. They're unambiguously expressions with type |
||
| if (self.chk.options.disallow_untyped_calls and | ||
| self.chk.in_checked_function() and | ||
| isinstance(callee_type, CallableType) | ||
|
|
@@ -1670,7 +1670,8 @@ def visit_enum_index_expr(self, enum_type: TypeInfo, index: Expression, | |
|
|
||
| def visit_cast_expr(self, expr: CastExpr) -> Type: | ||
| """Type check a cast expression.""" | ||
| source_type = self.accept(expr.expr, type_context=AnyType(), allow_none_return=True) | ||
| source_type = self.accept(expr.expr, type_context=AnyType(), allow_none_return=True, | ||
| disallow_any=False) | ||
| target_type = expr.type | ||
| options = self.chk.options | ||
| if options.warn_redundant_casts and is_same_type(source_type, target_type): | ||
|
|
@@ -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, | ||
|
Collaborator
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'd consider naming this
Collaborator
Author
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 was looking for a better name. |
||
| ) -> Type: | ||
| """Type check a node in the given type context. If allow_none_return | ||
| is True and this expression is a call, allow it to return None. This | ||
|
|
@@ -2211,6 +2213,13 @@ def accept(self, | |
| self.type_context.pop() | ||
| assert typ is not None | ||
| self.chk.store_type(node, typ) | ||
|
|
||
| if (disallow_any and | ||
| not self.chk.is_stub and | ||
| has_any_type(typ) and | ||
| 'expr' in self.chk.options.disallow_any): | ||
|
Collaborator
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. 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.)
Collaborator
Author
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. Yeah, I think it's easier when you're looking through the code. |
||
| self.msg.disallowed_any_type(typ, node) | ||
|
|
||
| if not self.chk.in_checked_function(): | ||
| return AnyType() | ||
| else: | ||
|
|
@@ -2429,6 +2438,19 @@ def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type: | |
| return known_type | ||
|
|
||
|
|
||
| def has_any_type(t: Type) -> bool: | ||
| """Whether t contains an Any type""" | ||
| return t.accept(HasAnyType()) | ||
|
|
||
|
|
||
| class HasAnyType(TypeQuery[bool]): | ||
|
Collaborator
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. Elsewhere in the file
Collaborator
Author
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. Yep! |
||
| def __init__(self) -> None: | ||
| super().__init__(any) | ||
|
|
||
| def visit_any(self, t: AnyType) -> bool: | ||
| return True | ||
|
|
||
|
|
||
| def has_coroutine_decorator(t: Type) -> bool: | ||
| """Whether t came from a function decorated with `@coroutine`.""" | ||
| return isinstance(t, Instance) and t.type.fullname() == 'typing.AwaitableGenerator' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Collaborator
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. Unused import.
Collaborator
Author
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. Good catch! |
||
|
|
||
| from mypy.erasetype import erase_type | ||
| from mypy.errors import Errors | ||
|
|
@@ -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 ' | ||
|
Collaborator
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. This is a bit confusing, as the expression may not have the type "Any".
Collaborator
Author
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'm not sure what you mean.
Collaborator
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. Yep, exactly.
Collaborator
Author
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. Updated the error message to say |
||
| '(has type {})'.format(self.format(typ)), context) | ||
|
|
||
|
|
||
| def capitalize(s: str) -> str: | ||
| """Capitalize the first character of a string.""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -509,3 +509,50 @@ 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 | ||
|
|
||
|
Collaborator
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. 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
Collaborator
Author
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. Let me know if you have any other test ideas. |
||
| [case testDisallowAnyBasic] | ||
| # flags: --disallow-any=expr | ||
| from typing import List, Any, cast | ||
|
|
||
| def foo(z: int): | ||
| return z | ||
|
|
||
| def print(s: Any) -> None: | ||
| s = s + 1 | ||
| pass | ||
|
|
||
| class Foo: | ||
| g: Any = 2 | ||
|
|
||
| x: List[str] = ['hello'] | ||
|
|
||
| f: int = Foo.doo() | ||
| k = Foo.doo() | ||
|
|
||
| z = cast(int, Foo().g) | ||
| m = cast(Any, Foo().g) | ||
| y = Foo().g | ||
| print(x[0]) | ||
| print(x[foo(0)]) | ||
| [builtins fixtures/list.pyi] | ||
| [out] | ||
| main:8: error: Expressions of type "Any" are disallowed (has type "Any") | ||
|
Collaborator
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. It'd be better to use the inline error syntax here (which you may not have seen). Basically you can put
Collaborator
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. 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.
Collaborator
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. 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.
Collaborator
Author
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. Yes, I will split them up! :) |
||
| main:16: error: Type[Foo] has no attribute "doo" | ||
| main:17: error: Type[Foo] has no attribute "doo" | ||
| main:17: error: Expressions of type "Any" are disallowed (has type "Any") | ||
| main:20: error: Expressions of type "Any" are disallowed (has type "Any") | ||
| main:21: error: Expressions of type "Any" are disallowed (has type "Any") | ||
| main:23: error: Expressions of type "Any" are disallowed (has type "Any") | ||
|
|
||
| [case testDisallowAnyExprGeneric] | ||
| # flags: --disallow-any=expr | ||
| from typing import List | ||
|
|
||
| blargle: List = [] | ||
|
Collaborator
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. Nit: as much as I like
Collaborator
Author
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. oh well, yeah, |
||
| blargle.append(1) | ||
| k = blargle[0] | ||
| [builtins fixtures/list.pyi] | ||
| [out] | ||
| main:5: error: Expressions of type "Any" are disallowed (has type List[Any]) | ||
| main:6: error: Expressions of type "Any" are disallowed (has type List[Any]) | ||
| main:6: 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.
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 likelvalue_typeis the type from an explicit type annotation.As for not allowing
Anyexpressions 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=exprneeds 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:
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
Anys 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.