Skip to content

Commit dcfebd7

Browse files
authored
Prohibit some illegal uses of Literal (#6034)
This pull request checks to make sure we report errors if the user tries using Literal types in invalid places. In particular, this PR... 1. Adds tests to make sure we cannot subclass Literals (mypy already directly supported this) 2. Checks to make sure we cannot use Literals inside `isinstance` and `issubclass` checks I also wanted to add a check preventing people from attempting to instantiate a Literal (e.g. disallow `Literal[3]()` or `Literal()`), but that might require a little more work and a few changes to `typing_extensions`. (We currently don't raise an error when people try doing things like `Final[int]()` or `Protocol[...]()` either).
1 parent 32263c2 commit dcfebd7

File tree

7 files changed

+108
-21
lines changed

7 files changed

+108
-21
lines changed

mypy/checkexpr.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,19 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
248248
except KeyError:
249249
# Undefined names should already be reported in semantic analysis.
250250
pass
251+
if is_expr_literal_type(typ):
252+
self.msg.cannot_use_function_with_type(e.callee.name, "Literal", e)
253+
continue
251254
if ((isinstance(typ, IndexExpr)
252255
and isinstance(typ.analyzed, (TypeApplication, TypeAliasExpr)))
253256
or (isinstance(typ, NameExpr) and node and
254257
isinstance(node.node, TypeAlias) and not node.node.no_args)):
255258
self.msg.type_arguments_not_allowed(e)
256259
if isinstance(typ, RefExpr) and isinstance(typ.node, TypeInfo):
257260
if typ.node.typeddict_type:
258-
self.msg.fail(messages.CANNOT_ISINSTANCE_TYPEDDICT, e)
261+
self.msg.cannot_use_function_with_type(e.callee.name, "TypedDict", e)
259262
elif typ.node.is_newtype:
260-
self.msg.fail(messages.CANNOT_ISINSTANCE_NEWTYPE, e)
263+
self.msg.cannot_use_function_with_type(e.callee.name, "NewType", e)
261264
self.try_infer_partial_type(e)
262265
type_context = None
263266
if isinstance(e.callee, LambdaExpr):
@@ -3629,3 +3632,15 @@ def is_literal_type_like(t: Optional[Type]) -> bool:
36293632
return any(is_literal_type_like(item) for item in t.items)
36303633
else:
36313634
return False
3635+
3636+
3637+
def is_expr_literal_type(node: Expression) -> bool:
3638+
"""Returns 'true' if the given node is a Literal"""
3639+
valid = ('typing.Literal', 'typing_extensions.Literal')
3640+
if isinstance(node, IndexExpr):
3641+
base = node.base
3642+
return isinstance(base, RefExpr) and base.fullname in valid
3643+
if isinstance(node, NameExpr):
3644+
underlying = node.node
3645+
return isinstance(underlying, TypeAlias) and isinstance(underlying.target, LiteralType)
3646+
return False

mypy/messages.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@
104104
DUPLICATE_TYPE_SIGNATURES = 'Function has duplicate type signatures' # type: Final
105105
GENERIC_INSTANCE_VAR_CLASS_ACCESS = \
106106
'Access to generic instance variables via class is ambiguous' # type: Final
107-
CANNOT_ISINSTANCE_TYPEDDICT = 'Cannot use isinstance() with a TypedDict type' # type: Final
108-
CANNOT_ISINSTANCE_NEWTYPE = 'Cannot use isinstance() with a NewType type' # type: Final
109107
BARE_GENERIC = 'Missing type parameters for generic type' # type: Final
110108
IMPLICIT_GENERIC_ANY_BUILTIN = \
111109
'Implicit generic "Any". Use \'{}\' and specify generic parameters' # type: Final
@@ -892,7 +890,9 @@ def incompatible_type_application(self, expected_arg_count: int,
892890
def alias_invalid_in_runtime_context(self, item: Type, ctx: Context) -> None:
893891
kind = (' to Callable' if isinstance(item, CallableType) else
894892
' to Tuple' if isinstance(item, TupleType) else
895-
' to Union' if isinstance(item, UnionType) else '')
893+
' to Union' if isinstance(item, UnionType) else
894+
' to Literal' if isinstance(item, LiteralType) else
895+
'')
896896
self.fail('The type alias{} is invalid in runtime context'.format(kind), ctx)
897897

898898
def could_not_infer_type_arguments(self, callee_type: CallableType, n: int,
@@ -1238,6 +1238,10 @@ def concrete_only_call(self, typ: Type, context: Context) -> None:
12381238
self.fail("Only concrete class can be given where {} is expected"
12391239
.format(self.format(typ)), context)
12401240

1241+
def cannot_use_function_with_type(
1242+
self, method_name: str, type_name: str, context: Context) -> None:
1243+
self.fail("Cannot use {}() with a {} type".format(method_name, type_name), context)
1244+
12411245
def report_non_method_protocol(self, tp: TypeInfo, members: List[str],
12421246
context: Context) -> None:
12431247
self.fail("Only protocols that don't have non-method members can be"

mypy/typeanal.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
'typing.Tuple',
4646
'typing.Type',
4747
'typing.Union',
48+
'typing.Literal',
49+
'typing_extensions.Literal',
4850
} # type: Final
4951

5052
ARG_KINDS_BY_CONSTRUCTOR = {

test-data/unit/check-literal.test

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,7 @@ d: 3j + 2 # E: invalid type comment or annotation
336336

337337
[case testLiteralDisallowComplexNumbersTypeAlias]
338338
from typing_extensions import Literal
339-
at = Literal[3j] # E: Invalid type alias \
340-
# E: The type "Type[Literal]" is not generic and not indexable
339+
at = Literal[3j] # E: Invalid type alias
341340
a: at # E: Invalid type "__main__.at"
342341
[builtins fixtures/complex.pyi]
343342
[out]
@@ -367,8 +366,7 @@ c: [1, 2, 3] # E: Invalid type
367366

368367
[case testLiteralDisallowCollectionsTypeAlias]
369368
from typing_extensions import Literal
370-
at = Literal[{"a": 1, "b": 2}] # E: Invalid type alias \
371-
# E: The type "Type[Literal]" is not generic and not indexable
369+
at = Literal[{"a": 1, "b": 2}] # E: Invalid type alias
372370
bt = {"a": 1, "b": 2}
373371
a: at # E: Invalid type "__main__.at"
374372
b: bt # E: Invalid type "__main__.bt"
@@ -377,8 +375,7 @@ b: bt # E: Invalid type "__main__.bt"
377375

378376
[case testLiteralDisallowCollectionsTypeAlias2]
379377
from typing_extensions import Literal
380-
at = Literal[{1, 2, 3}] # E: Invalid type alias \
381-
# E: The type "Type[Literal]" is not generic and not indexable
378+
at = Literal[{1, 2, 3}] # E: Invalid type alias
382379
bt = {1, 2, 3}
383380
a: at # E: Invalid type "__main__.at"
384381
b: bt # E: Invalid type "__main__.bt"
@@ -1180,3 +1177,66 @@ b = b * a
11801177
c = c.strip() # E: Incompatible types in assignment (expression has type "str", variable has type "Literal['foo']")
11811178
[builtins fixtures/ops.pyi]
11821179
[out]
1180+
1181+
1182+
--
1183+
-- Tests that check we report errors when we try using Literal[...]
1184+
-- in invalid places.
1185+
--
1186+
1187+
[case testLiteralErrorsWithIsInstanceAndIsSubclass]
1188+
from typing_extensions import Literal
1189+
from typing_extensions import Literal as Renamed
1190+
import typing_extensions as indirect
1191+
1192+
Alias = Literal[3]
1193+
1194+
isinstance(3, Literal[3]) # E: Cannot use isinstance() with a Literal type
1195+
isinstance(3, Alias) # E: Cannot use isinstance() with a Literal type \
1196+
# E: The type alias to Literal is invalid in runtime context
1197+
isinstance(3, Renamed[3]) # E: Cannot use isinstance() with a Literal type
1198+
isinstance(3, indirect.Literal[3]) # E: Cannot use isinstance() with a Literal type
1199+
1200+
issubclass(int, Literal[3]) # E: Cannot use issubclass() with a Literal type
1201+
issubclass(int, Alias) # E: Cannot use issubclass() with a Literal type \
1202+
# E: The type alias to Literal is invalid in runtime context
1203+
issubclass(int, Renamed[3]) # E: Cannot use issubclass() with a Literal type
1204+
issubclass(int, indirect.Literal[3]) # E: Cannot use issubclass() with a Literal type
1205+
[builtins fixtures/isinstancelist.pyi]
1206+
[out]
1207+
1208+
[case testLiteralErrorsWhenSubclassed]
1209+
from typing_extensions import Literal
1210+
from typing_extensions import Literal as Renamed
1211+
import typing_extensions as indirect
1212+
1213+
Alias = Literal[3]
1214+
1215+
class Bad1(Literal[3]): pass # E: Invalid base class
1216+
class Bad2(Renamed[3]): pass # E: Invalid base class
1217+
class Bad3(indirect.Literal[3]): pass # E: Invalid base class
1218+
class Bad4(Alias): pass # E: Invalid base class
1219+
[out]
1220+
1221+
[case testLiteralErrorsWhenInvoked-skip]
1222+
# TODO: We don't seem to correctly handle invoking types like
1223+
# 'Final' and 'Protocol' as well. When fixing this, also fix
1224+
# those types?
1225+
from typing_extensions import Literal
1226+
from typing_extensions import Literal as Renamed
1227+
import typing_extensions as indirect
1228+
1229+
Alias = Literal[3]
1230+
1231+
Literal[3]() # E: The type "Type[Literal]" is not generic and not indexable
1232+
Renamed[3]() # E: The type "Type[Literal]" is not generic and not indexable
1233+
indirect.Literal[3]() # E: The type "Type[Literal]" is not generic and not indexable
1234+
Alias() # E: The type alias to Literal is invalid in runtime context
1235+
1236+
# TODO: Add appropriate error messages to the following lines
1237+
Literal()
1238+
Renamed()
1239+
indirect.Literal()
1240+
[builtins fixtures/isinstancelist.pyi]
1241+
[out]
1242+

test-data/unit/check-newtype.test

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,13 @@ from typing import NewType
357357
Any = NewType('Any', int)
358358
Any(5)
359359

360-
[case testNewTypeAndIsInstance]
360+
[case testNewTypeWithIsInstanceAndIsSubclass]
361361
from typing import NewType
362362
T = NewType('T', int)
363363
d: object
364-
if isinstance(d, T): # E: Cannot use isinstance() with a NewType type
365-
reveal_type(d) # E: Revealed type is '__main__.T'
364+
if isinstance(d, T): # E: Cannot use isinstance() with a NewType type
365+
reveal_type(d) # E: Revealed type is '__main__.T'
366+
issubclass(object, T) # E: Cannot use issubclass() with a NewType type
366367
[builtins fixtures/isinstancelist.pyi]
367368

368369
[case testInvalidNewTypeCrash]

test-data/unit/check-typeddict.test

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -713,12 +713,13 @@ def set_coordinate(p: TaggedPoint, key: str, value: int) -> None:
713713

714714
-- isinstance
715715

716-
[case testTypedDictAndInstance]
716+
[case testTypedDictWithIsInstanceAndIsSubclass]
717717
from mypy_extensions import TypedDict
718718
D = TypedDict('D', {'x': int})
719719
d: object
720-
if isinstance(d, D): # E: Cannot use isinstance() with a TypedDict type
721-
reveal_type(d) # E: Revealed type is '__main__.D'
720+
if isinstance(d, D): # E: Cannot use isinstance() with a TypedDict type
721+
reveal_type(d) # E: Revealed type is '__main__.D'
722+
issubclass(object, D) # E: Cannot use issubclass() with a TypedDict type
722723
[builtins fixtures/isinstancelist.pyi]
723724

724725

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
from typing import TypeVar
1+
from typing import TypeVar, Any
22

33
_T = TypeVar('_T')
44

5-
class Protocol: pass
5+
class _SpecialForm:
6+
def __getitem__(self, typeargs: Any) -> Any:
7+
pass
8+
9+
Protocol: _SpecialForm = ...
610
def runtime(x: _T) -> _T: pass
711

8-
class Final: pass
12+
Final: _SpecialForm = ...
913
def final(x: _T) -> _T: pass
1014

11-
class Literal: pass
15+
Literal: _SpecialForm = ...

0 commit comments

Comments
 (0)