Skip to content

Commit d761722

Browse files
committed
Option to disallow omitting type parameters of generic types
(--disallow-any=generics) This code is based on python#3141 by pkch. This option disallows implicit Anys from omitted type parameters to generic types. For instance, `def x() -> List` would produce an error while `def x() -> List[Any]` is allowed. Note that with the flag enabled builtin generic types such as `list` and `set` are also forbidden.
1 parent 3427a5f commit d761722

File tree

6 files changed

+291
-33
lines changed

6 files changed

+291
-33
lines changed

mypy/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def type_check_only(sources: List[BuildSource], bin_dir: str, options: Options)
9797
options=options)
9898

9999

100-
disallow_any_options = ['unimported', 'expr', 'unannotated', 'decorated']
100+
disallow_any_options = ['unimported', 'expr', 'unannotated', 'decorated', 'generics']
101101

102102

103103
def disallow_any_argument_type(raw_options: str) -> List[str]:

mypy/messages.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
NON_BOOLEAN_IN_CONDITIONAL = 'Condition must be a boolean'
8989
DUPLICATE_TYPE_SIGNATURES = 'Function has duplicate type signatures'
9090
GENERIC_INSTANCE_VAR_CLASS_ACCESS = 'Access to generic instance variables via class is ambiguous'
91+
BARE_GENERIC = 'Missing type parameters for generic type'
92+
GENERIC_BUILTIN_TYPES_DISALLOWED = "Builtin generic types are disallowed. Use '{}' instead"
9193

9294
ARG_CONSTRUCTOR_NAMES = {
9395
ARG_POS: "Arg",

mypy/semanal.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,18 @@
7777
from mypy.errors import Errors, report_internal_error
7878
from mypy.messages import CANNOT_ASSIGN_TO_TYPE, MessageBuilder
7979
from mypy.types import (
80-
NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType,
81-
FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType,
82-
TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType,
83-
TypeQuery
80+
NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType, FunctionLike,
81+
UnboundType, TypeVarDef, TypeType, TupleType, UnionType, StarType, function_type, TypedDictType
8482
)
8583
from mypy.nodes import implicit_module_attrs
8684
from mypy.typeanal import (
8785
TypeAnalyser, TypeAnalyserPass3, analyze_type_alias, no_subscript_builtin_alias,
88-
TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type
86+
TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type, collect_any_types
8987
)
9088
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
9189
from mypy.sametypes import is_same_type
9290
from mypy.options import Options
93-
from mypy import experiments
91+
from mypy import experiments, messages
9492
from mypy.plugin import Plugin
9593
from mypy import join
9694

@@ -230,6 +228,7 @@ class SemanticAnalyzer(NodeVisitor):
230228
loop_depth = 0 # Depth of breakable loops
231229
cur_mod_id = '' # Current module id (or None) (phase 2)
232230
is_stub_file = False # Are we analyzing a stub file?
231+
is_typeshed_stub_file = False # Are we analyzing a typeshed stub file?
233232
imports = None # type: Set[str] # Imported modules (during phase 2 analysis)
234233
errors = None # type: Errors # Keeps track of generated errors
235234
plugin = None # type: Plugin # Mypy plugin for special casing of library features
@@ -274,6 +273,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
274273
self.cur_mod_node = file_node
275274
self.cur_mod_id = file_node.fullname()
276275
self.is_stub_file = fnam.lower().endswith('.pyi')
276+
self.is_typeshed_stub_file = self.errors.is_typeshed_file(file_node.path)
277277
self.globals = file_node.names
278278
self.patches = patches
279279

@@ -339,6 +339,7 @@ def file_context(self, file_node: MypyFile, fnam: str, options: Options,
339339
self.cur_mod_node = file_node
340340
self.cur_mod_id = file_node.fullname()
341341
self.is_stub_file = fnam.lower().endswith('.pyi')
342+
self.is_typeshed_stub_file = self.errors.is_typeshed_file(file_node.path)
342343
self.globals = file_node.names
343344
if active_type:
344345
self.enter_class(active_type.defn.info)
@@ -1529,6 +1530,8 @@ def type_analyzer(self, *,
15291530
tvar_scope,
15301531
self.fail,
15311532
self.plugin,
1533+
self.options,
1534+
self.is_typeshed_stub_file,
15321535
aliasing=aliasing,
15331536
allow_tuple_literal=allow_tuple_literal,
15341537
allow_unnormalized=self.is_stub_file)
@@ -1568,6 +1571,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
15681571
self.tvar_scope,
15691572
self.fail,
15701573
self.plugin,
1574+
self.options,
1575+
self.is_typeshed_stub_file,
15711576
allow_unnormalized=True)
15721577
if res and (not isinstance(res, Instance) or res.args):
15731578
# TODO: What if this gets reassigned?
@@ -3176,6 +3181,8 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
31763181
self.tvar_scope,
31773182
self.fail,
31783183
self.plugin,
3184+
self.options,
3185+
self.is_typeshed_stub_file,
31793186
allow_unnormalized=self.is_stub_file)
31803187
expr.analyzed = TypeAliasExpr(res, fallback=self.alias_fallback(res),
31813188
in_runtime=True)
@@ -3863,6 +3870,7 @@ def __init__(self, modules: Dict[str, MypyFile], errors: Errors) -> None:
38633870
def visit_file(self, file_node: MypyFile, fnam: str, options: Options) -> None:
38643871
self.errors.set_file(fnam, file_node.fullname())
38653872
self.options = options
3873+
self.is_typeshed_file = self.errors.is_typeshed_file(fnam)
38663874
with experiments.strict_optional_set(options.strict_optional):
38673875
self.accept(file_node)
38683876

@@ -3994,8 +4002,17 @@ def visit_type_application(self, e: TypeApplication) -> None:
39944002

39954003
def analyze(self, type: Optional[Type]) -> None:
39964004
if type:
3997-
analyzer = TypeAnalyserPass3(self.fail)
4005+
analyzer = TypeAnalyserPass3(self.fail, self.options, self.is_typeshed_file)
39984006
type.accept(analyzer)
4007+
self.check_for_omitted_generics(type)
4008+
4009+
def check_for_omitted_generics(self, typ: Type) -> None:
4010+
if 'generics' not in self.options.disallow_any or self.is_typeshed_file:
4011+
return
4012+
4013+
for t in collect_any_types(typ):
4014+
if t.from_omitted_generics:
4015+
self.fail(messages.BARE_GENERIC, t)
39994016

40004017
def fail(self, msg: str, ctx: Context, *, blocker: bool = False) -> None:
40014018
self.errors.report(ctx.get_line(), ctx.get_column(), msg)

mypy/typeanal.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from contextlib import contextmanager
88

9+
from mypy.options import Options
910
from mypy.types import (
1011
Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance,
1112
AnyType, CallableType, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor,
@@ -15,18 +16,16 @@
1516
)
1617

1718
from mypy.nodes import (
18-
TVAR, TYPE_ALIAS, UNBOUND_IMPORTED,
19-
TypeInfo, Context, SymbolTableNode, Var, Expression,
20-
IndexExpr, RefExpr, nongen_builtins, check_arg_names, check_arg_kinds,
21-
ARG_POS, ARG_NAMED, ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr
19+
TVAR, TYPE_ALIAS, UNBOUND_IMPORTED, TypeInfo, Context, SymbolTableNode, Var, Expression,
20+
IndexExpr, RefExpr, nongen_builtins, check_arg_names, check_arg_kinds, ARG_POS, ARG_NAMED,
21+
ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr
2222
)
2323
from mypy.tvar_scope import TypeVarScope
2424
from mypy.sametypes import is_same_type
2525
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
2626
from mypy.subtypes import is_subtype
2727
from mypy.plugin import Plugin, AnalyzerPluginInterface, AnalyzeTypeContext
28-
from mypy import nodes
29-
from mypy import experiments
28+
from mypy import nodes, messages
3029

3130

3231
T = TypeVar('T')
@@ -56,6 +55,8 @@ def analyze_type_alias(node: Expression,
5655
tvar_scope: TypeVarScope,
5756
fail_func: Callable[[str, Context], None],
5857
plugin: Plugin,
58+
options: Options,
59+
is_typeshed_stub: bool,
5960
allow_unnormalized: bool = False) -> Optional[Type]:
6061
"""Return type if node is valid as a type alias rvalue.
6162
@@ -98,8 +99,8 @@ def analyze_type_alias(node: Expression,
9899
except TypeTranslationError:
99100
fail_func('Invalid type alias', node)
100101
return None
101-
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, plugin,
102-
aliasing=True, allow_unnormalized=allow_unnormalized)
102+
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, plugin, options,
103+
is_typeshed_stub, aliasing=True, allow_unnormalized=allow_unnormalized)
103104
return type.accept(analyzer)
104105

105106

@@ -122,7 +123,9 @@ def __init__(self,
122123
lookup_fqn_func: Callable[[str], SymbolTableNode],
123124
tvar_scope: TypeVarScope,
124125
fail_func: Callable[[str, Context], None],
125-
plugin: Plugin, *,
126+
plugin: Plugin,
127+
options: Options,
128+
is_typeshed_stub: bool, *,
126129
aliasing: bool = False,
127130
allow_tuple_literal: bool = False,
128131
allow_unnormalized: bool = False) -> None:
@@ -136,6 +139,8 @@ def __init__(self,
136139
self.nesting_level = 0
137140
self.allow_unnormalized = allow_unnormalized
138141
self.plugin = plugin
142+
self.options = options
143+
self.is_typeshed_stub = is_typeshed_stub
139144

140145
def visit_unbound_type(self, t: UnboundType) -> Type:
141146
if t.optional:
@@ -170,7 +175,11 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
170175
elif fullname == 'typing.Tuple':
171176
if len(t.args) == 0 and not t.empty_tuple_index:
172177
# Bare 'Tuple' is same as 'tuple'
173-
return self.named_type('builtins.tuple')
178+
if 'generics' in self.options.disallow_any and not self.is_typeshed_stub:
179+
self.fail(messages.BARE_GENERIC, t)
180+
typ = self.named_type('builtins.tuple', line=t.line, column=t.column)
181+
typ.from_generic_builtin = True
182+
return typ
174183
if len(t.args) == 2 and isinstance(t.args[1], EllipsisType):
175184
# Tuple[T, ...] (uniform, variable-length tuple)
176185
instance = self.named_type('builtins.tuple', [self.anal_type(t.args[0])])
@@ -190,7 +199,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
190199
return self.analyze_callable_type(t)
191200
elif fullname == 'typing.Type':
192201
if len(t.args) == 0:
193-
return TypeType(AnyType(), line=t.line)
202+
any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column)
203+
return TypeType(any_type, line=t.line, column=t.column)
194204
if len(t.args) != 1:
195205
self.fail('Type[...] must have exactly one type argument', t)
196206
item = self.anal_type(t.args[0])
@@ -219,7 +229,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
219229
act_len = len(an_args)
220230
if exp_len > 0 and act_len == 0:
221231
# Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...]
222-
return self.replace_alias_tvars(override, all_vars, [AnyType()] * exp_len,
232+
any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column)
233+
return self.replace_alias_tvars(override, all_vars, [any_type] * exp_len,
223234
t.line, t.column)
224235
if exp_len == 0 and act_len == 0:
225236
return override
@@ -255,6 +266,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
255266
# valid count at this point. Thus we may construct an
256267
# Instance with an invalid number of type arguments.
257268
instance = Instance(info, self.anal_array(t.args), t.line, t.column)
269+
instance.from_generic_builtin = sym.normalized
258270
tup = info.tuple_type
259271
if tup is not None:
260272
# The class has a Tuple[...] base class so it will be
@@ -395,10 +407,11 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
395407
fallback = self.named_type('builtins.function')
396408
if len(t.args) == 0:
397409
# Callable (bare). Treat as Callable[..., Any].
398-
ret = CallableType([AnyType(), AnyType()],
410+
any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column)
411+
ret = CallableType([any_type, any_type],
399412
[nodes.ARG_STAR, nodes.ARG_STAR2],
400413
[None, None],
401-
ret_type=AnyType(),
414+
ret_type=any_type,
402415
fallback=fallback,
403416
is_ellipsis_args=True)
404417
elif len(t.args) == 2:
@@ -552,10 +565,13 @@ def anal_var_defs(self, var_defs: List[TypeVarDef]) -> List[TypeVarDef]:
552565
vd.line))
553566
return a
554567

555-
def named_type(self, fully_qualified_name: str, args: List[Type] = None) -> Instance:
568+
def named_type(self, fully_qualified_name: str,
569+
args: List[Type] = None,
570+
line: int = -1,
571+
column: int = -1) -> Instance:
556572
node = self.lookup_fqn_func(fully_qualified_name)
557573
assert isinstance(node.node, TypeInfo)
558-
return Instance(node.node, args or [])
574+
return Instance(node.node, args or [], line=line, column=column)
559575

560576
def tuple_type(self, items: List[Type]) -> TupleType:
561577
return TupleType(items, fallback=self.named_type('builtins.tuple', [AnyType()]))
@@ -581,16 +597,29 @@ class TypeAnalyserPass3(TypeVisitor[None]):
581597
to types.
582598
"""
583599

584-
def __init__(self, fail_func: Callable[[str, Context], None]) -> None:
600+
def __init__(self,
601+
fail_func: Callable[[str, Context], None],
602+
options: Options,
603+
is_typeshed_stub: bool) -> None:
585604
self.fail = fail_func
605+
self.options = options
606+
self.is_typeshed_stub = is_typeshed_stub
586607

587608
def visit_instance(self, t: Instance) -> None:
588609
info = t.type
589610
# Check type argument count.
590611
if len(t.args) != len(info.type_vars):
591612
if len(t.args) == 0:
613+
from_builtins = t.type.fullname() in nongen_builtins and not t.from_generic_builtin
614+
if ('generics' in self.options.disallow_any and
615+
not self.is_typeshed_stub and
616+
from_builtins):
617+
alternative = nongen_builtins[t.type.fullname()]
618+
self.fail(messages.GENERIC_BUILTIN_TYPES_DISALLOWED.format(alternative), t)
592619
# Insert implicit 'Any' type arguments.
593-
t.args = [AnyType()] * len(info.type_vars)
620+
any_type = AnyType(from_omitted_generics=not from_builtins, line=t.line,
621+
column=t.line)
622+
t.args = [any_type] * len(info.type_vars)
594623
return
595624
# Invalid number of type parameters.
596625
n = len(info.type_vars)
@@ -775,6 +804,26 @@ def visit_typeddict_type(self, t: TypedDictType) -> bool:
775804
return False
776805

777806

807+
def collect_any_types(t: Type) -> List[AnyType]:
808+
"""Return all inner `AnyType`s of type t"""
809+
return t.accept(CollectAnyTypesQuery())
810+
811+
812+
class CollectAnyTypesQuery(TypeQuery[List[AnyType]]):
813+
def __init__(self) -> None:
814+
super().__init__(self.combine_lists_strategy)
815+
816+
def visit_any(self, t: AnyType) -> List[AnyType]:
817+
return [t]
818+
819+
@classmethod
820+
def combine_lists_strategy(cls, it: Iterable[List[AnyType]]) -> List[AnyType]:
821+
result = [] # type: List[AnyType]
822+
for l in it:
823+
result.extend(l)
824+
return result
825+
826+
778827
def make_optional_type(t: Type) -> Type:
779828
"""Return the type corresponding to Optional[t].
780829

mypy/types.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
"""Classes for representing mypy types."""
22

3-
from abc import abstractmethod
43
import copy
4+
from abc import abstractmethod
55
from collections import OrderedDict
66
from typing import (
7-
Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Sequence, Optional, Union, Iterable,
8-
NamedTuple, Callable,
7+
Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Optional, Union, Iterable, NamedTuple,
8+
Callable,
99
)
1010

1111
import mypy.nodes
12+
from mypy import experiments
1213
from mypy.nodes import (
13-
INVARIANT, SymbolNode,
14-
ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, ARG_NAMED, ARG_NAMED_OPT,
14+
INVARIANT, SymbolNode, ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, ARG_NAMED, ARG_NAMED_OPT,
1515
)
1616
from mypy.sharedparse import argument_elide_name
1717
from mypy.util import IdMapper
18-
from mypy import experiments
19-
2018

2119
T = TypeVar('T')
2220

@@ -255,13 +253,16 @@ class AnyType(Type):
255253
def __init__(self,
256254
implicit: bool = False,
257255
from_unimported_type: bool = False,
256+
from_omitted_generics: bool = False,
258257
line: int = -1,
259258
column: int = -1) -> None:
260259
super().__init__(line, column)
261260
# Was this Any type was inferred without a type annotation?
262261
self.implicit = implicit
263262
# Does this come from an unfollowed import? See --disallow-any=unimported option
264263
self.from_unimported_type = from_unimported_type
264+
# Does this type come from omitted generics?
265+
self.from_omitted_generics = from_omitted_generics
265266

266267
def accept(self, visitor: 'TypeVisitor[T]') -> T:
267268
return visitor.visit_any(self)
@@ -385,6 +386,7 @@ class Instance(Type):
385386
args = None # type: List[Type]
386387
erased = False # True if result of type variable substitution
387388
invalid = False # True if recovered after incorrect number of type arguments error
389+
from_generic_builtin = False # True if created from a generic builtin (e.g. list() or set())
388390

389391
def __init__(self, typ: mypy.nodes.TypeInfo, args: List[Type],
390392
line: int = -1, column: int = -1, erased: bool = False) -> None:

0 commit comments

Comments
 (0)