Skip to content

Commit 6db1663

Browse files
Corononilevkivskyi
authored andcommitted
Add recommendation to use string literal escaping (#7707)
This commit introduces a recommendation for classes that don't support indexing at runtime to be escaped as string literals on missing required generic type args (BARE_GENERIC) error. It should resolve #7539
1 parent d6120e9 commit 6db1663

File tree

4 files changed

+70
-20
lines changed

4 files changed

+70
-20
lines changed

docs/source/common_issues.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ Here's the above example modified to use ``MYPY``:
533533
def listify(arg: 'bar.BarClass') -> 'List[bar.BarClass]':
534534
return [arg]
535535
536+
.. _not-generic-runtime:
536537

537538
Using classes that are generic in stubs but not at runtime
538539
----------------------------------------------------------
@@ -568,6 +569,15 @@ string literal types or :py:data:`~typing.TYPE_CHECKING`:
568569
569570
results: 'Queue[int]' = Queue() # OK
570571
572+
If you are running Python 3.7+ you can use ``from __future__ import annotations``
573+
as a (nicer) alternative to string quotes, read more in :pep:`563`. For example:
574+
575+
.. code-block:: python
576+
577+
from __future__ import annotations
578+
from queue import Queue
579+
580+
results: Queue[int] = Queue() # This works at runtime
571581
572582
.. _silencing-linters:
573583

mypy/semanal.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def add_builtin_aliases(self, tree: MypyFile) -> None:
450450
target = self.named_type_or_none(target_name, [])
451451
assert target is not None
452452
# Transform List to List[Any], etc.
453-
fix_instance_types(target, self.fail)
453+
fix_instance_types(target, self.fail, self.note)
454454
alias_node = TypeAlias(target, alias,
455455
line=-1, column=-1, # there is no context
456456
no_args=True, normalized=True)
@@ -608,7 +608,7 @@ def analyze_func_def(self, defn: FuncDef) -> None:
608608
# has external return type `Coroutine[Any, Any, T]`.
609609
any_type = AnyType(TypeOfAny.special_form)
610610
ret_type = self.named_type_or_none('typing.Coroutine',
611-
[any_type, any_type, defn.type.ret_type])
611+
[any_type, any_type, defn.type.ret_type])
612612
assert ret_type is not None, "Internal error: typing.Coroutine not found"
613613
defn.type = defn.type.copy_modified(ret_type=ret_type)
614614

@@ -2490,7 +2490,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
24902490
# so we need to replace it with non-explicit Anys.
24912491
res = make_any_non_explicit(res)
24922492
no_args = isinstance(res, Instance) and not res.args # type: ignore
2493-
fix_instance_types(res, self.fail)
2493+
fix_instance_types(res, self.fail, self.note)
24942494
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
24952495
s.rvalue.analyzed = TypeAliasExpr(res, alias_tvars, no_args)
24962496
s.rvalue.analyzed.line = s.line

mypy/typeanal.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@
5555
'mypy_extensions.KwArg': ARG_STAR2,
5656
} # type: Final
5757

58+
GENERIC_STUB_NOT_AT_RUNTIME_TYPES = {
59+
'queue.Queue',
60+
'builtins._PathLike',
61+
} # type: Final
62+
5863

5964
def analyze_type_alias(node: Expression,
6065
api: SemanticAnalyzerCoreInterface,
@@ -221,8 +226,13 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
221226
res = get_proper_type(res)
222227
if (isinstance(res, Instance) and len(res.args) != len(res.type.type_vars) and
223228
not self.defining_alias):
224-
fix_instance(res, self.fail, disallow_any=disallow_any, use_generic_error=True,
225-
unexpanded_type=t)
229+
fix_instance(
230+
res,
231+
self.fail,
232+
self.note,
233+
disallow_any=disallow_any,
234+
use_generic_error=True,
235+
unexpanded_type=t)
226236
return res
227237
elif isinstance(node, TypeInfo):
228238
return self.analyze_type_with_type_info(node, t.args, t)
@@ -319,13 +329,14 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
319329

320330
def get_omitted_any(self, typ: Type, fullname: Optional[str] = None) -> AnyType:
321331
disallow_any = not self.is_typeshed_stub and self.options.disallow_any_generics
322-
return get_omitted_any(disallow_any, self.fail, typ, fullname)
332+
return get_omitted_any(disallow_any, self.fail, self.note, typ, fullname)
323333

324334
def analyze_type_with_type_info(self, info: TypeInfo, args: List[Type], ctx: Context) -> Type:
325335
"""Bind unbound type when were able to find target TypeInfo.
326336
327337
This handles simple cases like 'int', 'modname.UserClass[str]', etc.
328338
"""
339+
329340
if len(args) > 0 and info.fullname() == 'builtins.tuple':
330341
fallback = Instance(info, [AnyType(TypeOfAny.special_form)], ctx.line)
331342
return TupleType(self.anal_array(args), fallback, ctx.line)
@@ -337,7 +348,7 @@ def analyze_type_with_type_info(self, info: TypeInfo, args: List[Type], ctx: Con
337348
instance = Instance(info, self.anal_array(args), ctx.line, ctx.column)
338349
# Check type argument count.
339350
if len(instance.args) != len(info.type_vars) and not self.defining_alias:
340-
fix_instance(instance, self.fail,
351+
fix_instance(instance, self.fail, self.note,
341352
disallow_any=self.options.disallow_any_generics and
342353
not self.is_typeshed_stub)
343354

@@ -891,10 +902,10 @@ def tuple_type(self, items: List[Type]) -> TupleType:
891902
TypeVarList = List[Tuple[str, TypeVarExpr]]
892903

893904
# Mypyc doesn't support callback protocols yet.
894-
FailCallback = Callable[[str, Context, DefaultNamedArg(Optional[ErrorCode], 'code')], None]
905+
MsgCallback = Callable[[str, Context, DefaultNamedArg(Optional[ErrorCode], 'code')], None]
895906

896907

897-
def get_omitted_any(disallow_any: bool, fail: FailCallback,
908+
def get_omitted_any(disallow_any: bool, fail: MsgCallback, note: MsgCallback,
898909
typ: Type, fullname: Optional[str] = None,
899910
unexpanded_type: Optional[Type] = None) -> AnyType:
900911
if disallow_any:
@@ -907,17 +918,31 @@ def get_omitted_any(disallow_any: bool, fail: FailCallback,
907918
typ = unexpanded_type or typ
908919
type_str = typ.name if isinstance(typ, UnboundType) else format_type_bare(typ)
909920

910-
fail(message_registry.BARE_GENERIC.format(quote_type_string(type_str)), typ,
911-
code=codes.TYPE_ARG)
921+
fail(
922+
message_registry.BARE_GENERIC.format(
923+
quote_type_string(type_str)),
924+
typ,
925+
code=codes.TYPE_ARG)
926+
927+
if fullname in GENERIC_STUB_NOT_AT_RUNTIME_TYPES:
928+
# Recommend `from __future__ import annotations` or to put type in quotes
929+
# (string literal escaping) for classes not generic at runtime
930+
note(
931+
"Subscripting classes that are not generic at runtime may require "
932+
"escaping, see https://mypy.readthedocs.io/"
933+
"en/latest/common_issues.html#not-generic-runtime",
934+
typ,
935+
code=codes.TYPE_ARG)
936+
912937
any_type = AnyType(TypeOfAny.from_error, line=typ.line, column=typ.column)
913938
else:
914939
any_type = AnyType(TypeOfAny.from_omitted_generics, line=typ.line, column=typ.column)
915940
return any_type
916941

917942

918-
def fix_instance(t: Instance, fail: FailCallback,
943+
def fix_instance(t: Instance, fail: MsgCallback, note: MsgCallback,
919944
disallow_any: bool, use_generic_error: bool = False,
920-
unexpanded_type: Optional[Type] = None) -> None:
945+
unexpanded_type: Optional[Type] = None,) -> None:
921946
"""Fix a malformed instance by replacing all type arguments with Any.
922947
923948
Also emit a suitable error if this is not due to implicit Any's.
@@ -927,7 +952,7 @@ def fix_instance(t: Instance, fail: FailCallback,
927952
fullname = None # type: Optional[str]
928953
else:
929954
fullname = t.type.fullname()
930-
any_type = get_omitted_any(disallow_any, fail, t, fullname, unexpanded_type)
955+
any_type = get_omitted_any(disallow_any, fail, note, t, fullname, unexpanded_type)
931956
t.args = [any_type] * len(t.type.type_vars)
932957
return
933958
# Invalid number of type parameters.
@@ -950,7 +975,7 @@ def fix_instance(t: Instance, fail: FailCallback,
950975

951976

952977
def expand_type_alias(target: Type, alias_tvars: List[str], args: List[Type],
953-
fail: FailCallback, no_args: bool, ctx: Context, *,
978+
fail: MsgCallback, no_args: bool, ctx: Context, *,
954979
unexpanded_type: Optional[Type] = None,
955980
disallow_any: bool = False) -> Type:
956981
"""Expand a (generic) type alias target following the rules outlined in TypeAlias docstring.
@@ -998,7 +1023,7 @@ def set_any_tvars(tp: Type, vars: List[str],
9981023
newline: int, newcolumn: int, *,
9991024
from_error: bool = False,
10001025
disallow_any: bool = False,
1001-
fail: Optional[FailCallback] = None,
1026+
fail: Optional[MsgCallback] = None,
10021027
unexpanded_type: Optional[Type] = None) -> ProperType:
10031028
if from_error or disallow_any:
10041029
type_of_any = TypeOfAny.from_error
@@ -1181,20 +1206,21 @@ def make_optional_type(t: Type) -> Type:
11811206
return UnionType([t, NoneType()], t.line, t.column)
11821207

11831208

1184-
def fix_instance_types(t: Type, fail: FailCallback) -> None:
1209+
def fix_instance_types(t: Type, fail: MsgCallback, note: MsgCallback) -> None:
11851210
"""Recursively fix all instance types (type argument count) in a given type.
11861211
11871212
For example 'Union[Dict, List[str, int]]' will be transformed into
11881213
'Union[Dict[Any, Any], List[Any]]' in place.
11891214
"""
1190-
t.accept(InstanceFixer(fail))
1215+
t.accept(InstanceFixer(fail, note))
11911216

11921217

11931218
class InstanceFixer(TypeTraverserVisitor):
1194-
def __init__(self, fail: FailCallback) -> None:
1219+
def __init__(self, fail: MsgCallback, note: MsgCallback) -> None:
11951220
self.fail = fail
1221+
self.note = note
11961222

11971223
def visit_instance(self, typ: Instance) -> None:
11981224
super().visit_instance(typ)
11991225
if len(typ.args) != len(typ.type.type_vars):
1200-
fix_instance(typ, self.fail, disallow_any=False, use_generic_error=True)
1226+
fix_instance(typ, self.fail, self.note, disallow_any=False, use_generic_error=True)

test-data/unit/cmdline.test

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,3 +1444,17 @@ some_file.py:1: error: invalid syntax [syntax]
14441444
...ooks_like_we_started_typing_something_but_then. = did_not_notice(an_ex...
14451445
^
14461446
== Return code: 2
1447+
1448+
[case testSpecialTypeshedGenericNote]
1449+
# cmd: mypy --disallow-any-generics --python-version=3.6 test.py
1450+
[file test.py]
1451+
from os import PathLike
1452+
from queue import Queue
1453+
1454+
p: PathLike
1455+
q: Queue
1456+
[out]
1457+
test.py:4: error: Missing type parameters for generic type "_PathLike"
1458+
test.py:4: note: Subscripting classes that are not generic at runtime may require escaping, see https://mypy.readthedocs.io/en/latest/common_issues.html#not-generic-runtime
1459+
test.py:5: error: Missing type parameters for generic type "Queue"
1460+
test.py:5: note: Subscripting classes that are not generic at runtime may require escaping, see https://mypy.readthedocs.io/en/latest/common_issues.html#not-generic-runtime

0 commit comments

Comments
 (0)