Skip to content

Commit

Permalink
Prohibit some illegal uses of Literal (#6034)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Michael0x2a authored Dec 9, 2018
1 parent 32263c2 commit dcfebd7
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 21 deletions.
19 changes: 17 additions & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,16 +248,19 @@ 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
isinstance(node.node, TypeAlias) and not node.node.no_args)):
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):
Expand Down Expand Up @@ -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
10 changes: 7 additions & 3 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
'typing.Tuple',
'typing.Type',
'typing.Union',
'typing.Literal',
'typing_extensions.Literal',
} # type: Final

ARG_KINDS_BY_CONSTRUCTOR = {
Expand Down
72 changes: 66 additions & 6 deletions test-data/unit/check-literal.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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]

7 changes: 4 additions & 3 deletions test-data/unit/check-newtype.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 4 additions & 3 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]


Expand Down
12 changes: 8 additions & 4 deletions test-data/unit/lib-stub/typing_extensions.pyi
Original file line number Diff line number Diff line change
@@ -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 = ...

0 comments on commit dcfebd7

Please sign in to comment.