From d97124e3cd43fc26d4a2513a9dfc39ce28a5ae63 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 6 Dec 2018 09:04:29 -0800 Subject: [PATCH] Prohibit some illegal uses of Literal 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()`, `Final[int]()` or `Protocol()` either). --- mypy/checkexpr.py | 19 ++++- mypy/messages.py | 10 ++- mypy/typeanal.py | 2 + test-data/unit/check-literal.test | 72 +++++++++++++++++-- test-data/unit/check-newtype.test | 7 +- test-data/unit/check-typeddict.test | 7 +- test-data/unit/lib-stub/typing_extensions.pyi | 12 ++-- 7 files changed, 108 insertions(+), 21 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e1beea078ca9..176ab8a9198d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -248,6 +248,9 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> except KeyError: # Undefined names should already be reported in semantic analysis. pass + if is_expr_literal_type(typ): + self.msg.cannot_use_function_with_type(e.callee.name, "Literal", e) + continue if ((isinstance(typ, IndexExpr) and isinstance(typ.analyzed, (TypeApplication, TypeAliasExpr))) or (isinstance(typ, NameExpr) and node and @@ -255,9 +258,9 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> self.msg.type_arguments_not_allowed(e) if isinstance(typ, RefExpr) and isinstance(typ.node, TypeInfo): if typ.node.typeddict_type: - self.msg.fail(messages.CANNOT_ISINSTANCE_TYPEDDICT, e) + self.msg.cannot_use_function_with_type(e.callee.name, "TypedDict", e) elif typ.node.is_newtype: - self.msg.fail(messages.CANNOT_ISINSTANCE_NEWTYPE, e) + self.msg.cannot_use_function_with_type(e.callee.name, "NewType", e) self.try_infer_partial_type(e) type_context = None if isinstance(e.callee, LambdaExpr): @@ -3629,3 +3632,15 @@ def is_literal_type_like(t: Optional[Type]) -> bool: return any(is_literal_type_like(item) for item in t.items) else: return False + + +def is_expr_literal_type(node: Expression) -> bool: + """Returns 'true' if the given node is a Literal""" + valid = ('typing.Literal', 'typing_extensions.Literal') + if isinstance(node, IndexExpr): + base = node.base + return isinstance(base, RefExpr) and base.fullname in valid + if isinstance(node, NameExpr): + underlying = node.node + return isinstance(underlying, TypeAlias) and isinstance(underlying.target, LiteralType) + return False diff --git a/mypy/messages.py b/mypy/messages.py index 6554d2dc163d..0bb754501a68 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -104,8 +104,6 @@ DUPLICATE_TYPE_SIGNATURES = 'Function has duplicate type signatures' # type: Final GENERIC_INSTANCE_VAR_CLASS_ACCESS = \ 'Access to generic instance variables via class is ambiguous' # type: Final -CANNOT_ISINSTANCE_TYPEDDICT = 'Cannot use isinstance() with a TypedDict type' # type: Final -CANNOT_ISINSTANCE_NEWTYPE = 'Cannot use isinstance() with a NewType type' # type: Final BARE_GENERIC = 'Missing type parameters for generic type' # type: Final IMPLICIT_GENERIC_ANY_BUILTIN = \ 'Implicit generic "Any". Use \'{}\' and specify generic parameters' # type: Final @@ -892,7 +890,9 @@ def incompatible_type_application(self, expected_arg_count: int, def alias_invalid_in_runtime_context(self, item: Type, ctx: Context) -> None: kind = (' to Callable' if isinstance(item, CallableType) else ' to Tuple' if isinstance(item, TupleType) else - ' to Union' if isinstance(item, UnionType) else '') + ' to Union' if isinstance(item, UnionType) else + ' to Literal' if isinstance(item, LiteralType) else + '') self.fail('The type alias{} is invalid in runtime context'.format(kind), ctx) 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: self.fail("Only concrete class can be given where {} is expected" .format(self.format(typ)), context) + def cannot_use_function_with_type( + self, method_name: str, type_name: str, context: Context) -> None: + self.fail("Cannot use {}() with a {} type".format(method_name, type_name), context) + def report_non_method_protocol(self, tp: TypeInfo, members: List[str], context: Context) -> None: self.fail("Only protocols that don't have non-method members can be" diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 0c79c11c10c6..c87b500776ed 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -45,6 +45,8 @@ 'typing.Tuple', 'typing.Type', 'typing.Union', + 'typing.Literal', + 'typing_extensions.Literal', } # type: Final ARG_KINDS_BY_CONSTRUCTOR = { diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index c7f35c94ec5e..fbb50cad0d7c 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -336,8 +336,7 @@ d: 3j + 2 # E: invalid type comment or annotation [case testLiteralDisallowComplexNumbersTypeAlias] from typing_extensions import Literal -at = Literal[3j] # E: Invalid type alias \ - # E: The type "Type[Literal]" is not generic and not indexable +at = Literal[3j] # E: Invalid type alias a: at # E: Invalid type "__main__.at" [builtins fixtures/complex.pyi] [out] @@ -367,8 +366,7 @@ c: [1, 2, 3] # E: Invalid type [case testLiteralDisallowCollectionsTypeAlias] from typing_extensions import Literal -at = Literal[{"a": 1, "b": 2}] # E: Invalid type alias \ - # E: The type "Type[Literal]" is not generic and not indexable +at = Literal[{"a": 1, "b": 2}] # E: Invalid type alias bt = {"a": 1, "b": 2} a: at # E: Invalid type "__main__.at" b: bt # E: Invalid type "__main__.bt" @@ -377,8 +375,7 @@ b: bt # E: Invalid type "__main__.bt" [case testLiteralDisallowCollectionsTypeAlias2] from typing_extensions import Literal -at = Literal[{1, 2, 3}] # E: Invalid type alias \ - # E: The type "Type[Literal]" is not generic and not indexable +at = Literal[{1, 2, 3}] # E: Invalid type alias bt = {1, 2, 3} a: at # E: Invalid type "__main__.at" b: bt # E: Invalid type "__main__.bt" @@ -1180,3 +1177,66 @@ b = b * a c = c.strip() # E: Incompatible types in assignment (expression has type "str", variable has type "Literal['foo']") [builtins fixtures/ops.pyi] [out] + + +-- +-- Tests that check we report errors when we try using Literal[...] +-- in invalid places. +-- + +[case testLiteralErrorsWithIsInstanceAndIsSubclass] +from typing_extensions import Literal +from typing_extensions import Literal as Renamed +import typing_extensions as indirect + +Alias = Literal[3] + +isinstance(3, Literal[3]) # E: Cannot use isinstance() with a Literal type +isinstance(3, Alias) # E: Cannot use isinstance() with a Literal type \ + # E: The type alias to Literal is invalid in runtime context +isinstance(3, Renamed[3]) # E: Cannot use isinstance() with a Literal type +isinstance(3, indirect.Literal[3]) # E: Cannot use isinstance() with a Literal type + +issubclass(int, Literal[3]) # E: Cannot use issubclass() with a Literal type +issubclass(int, Alias) # E: Cannot use issubclass() with a Literal type \ + # E: The type alias to Literal is invalid in runtime context +issubclass(int, Renamed[3]) # E: Cannot use issubclass() with a Literal type +issubclass(int, indirect.Literal[3]) # E: Cannot use issubclass() with a Literal type +[builtins fixtures/isinstancelist.pyi] +[out] + +[case testLiteralErrorsWhenSubclassed] +from typing_extensions import Literal +from typing_extensions import Literal as Renamed +import typing_extensions as indirect + +Alias = Literal[3] + +class Bad1(Literal[3]): pass # E: Invalid base class +class Bad2(Renamed[3]): pass # E: Invalid base class +class Bad3(indirect.Literal[3]): pass # E: Invalid base class +class Bad4(Alias): pass # E: Invalid base class +[out] + +[case testLiteralErrorsWhenInvoked-skip] +# TODO: We don't seem to correctly handle invoking types like +# 'Final' and 'Protocol' as well. When fixing this, also fix +# those types? +from typing_extensions import Literal +from typing_extensions import Literal as Renamed +import typing_extensions as indirect + +Alias = Literal[3] + +Literal[3]() # E: The type "Type[Literal]" is not generic and not indexable +Renamed[3]() # E: The type "Type[Literal]" is not generic and not indexable +indirect.Literal[3]() # E: The type "Type[Literal]" is not generic and not indexable +Alias() # E: The type alias to Literal is invalid in runtime context + +# TODO: Add appropriate error messages to the following lines +Literal() +Renamed() +indirect.Literal() +[builtins fixtures/isinstancelist.pyi] +[out] + diff --git a/test-data/unit/check-newtype.test b/test-data/unit/check-newtype.test index c7f43017003a..bc444c68ee03 100644 --- a/test-data/unit/check-newtype.test +++ b/test-data/unit/check-newtype.test @@ -357,12 +357,13 @@ from typing import NewType Any = NewType('Any', int) Any(5) -[case testNewTypeAndIsInstance] +[case testNewTypeWithIsInstanceAndIsSubclass] from typing import NewType T = NewType('T', int) d: object -if isinstance(d, T): # E: Cannot use isinstance() with a NewType type - reveal_type(d) # E: Revealed type is '__main__.T' +if isinstance(d, T): # E: Cannot use isinstance() with a NewType type + reveal_type(d) # E: Revealed type is '__main__.T' +issubclass(object, T) # E: Cannot use issubclass() with a NewType type [builtins fixtures/isinstancelist.pyi] [case testInvalidNewTypeCrash] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 4fabf234d534..9910b356f7d9 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -713,12 +713,13 @@ def set_coordinate(p: TaggedPoint, key: str, value: int) -> None: -- isinstance -[case testTypedDictAndInstance] +[case testTypedDictWithIsInstanceAndIsSubclass] from mypy_extensions import TypedDict D = TypedDict('D', {'x': int}) d: object -if isinstance(d, D): # E: Cannot use isinstance() with a TypedDict type - reveal_type(d) # E: Revealed type is '__main__.D' +if isinstance(d, D): # E: Cannot use isinstance() with a TypedDict type + reveal_type(d) # E: Revealed type is '__main__.D' +issubclass(object, D) # E: Cannot use issubclass() with a TypedDict type [builtins fixtures/isinstancelist.pyi] diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 2b75d3305ce2..fafcc6481dcd 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -1,11 +1,15 @@ -from typing import TypeVar +from typing import TypeVar, Any _T = TypeVar('_T') -class Protocol: pass +class _SpecialForm: + def __getitem__(self, typeargs: Any) -> Any: + pass + +Protocol: _SpecialForm = ... def runtime(x: _T) -> _T: pass -class Final: pass +Final: _SpecialForm = ... def final(x: _T) -> _T: pass -class Literal: pass +Literal: _SpecialForm = ...