diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 9146c303811b..b9974b9c361e 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -1,13 +1,14 @@ """Translate an Expression to a Type value.""" from mypy.nodes import ( - Expression, NameExpr, MemberExpr, IndexExpr, TupleExpr, + Expression, NameExpr, MemberExpr, IndexExpr, TupleExpr, IntExpr, FloatExpr, UnaryExpr, ListExpr, StrExpr, BytesExpr, UnicodeExpr, EllipsisExpr, CallExpr, get_member_expr_fullname ) from mypy.fastparse import parse_type_comment from mypy.types import ( - Type, UnboundType, TypeList, EllipsisType, AnyType, Optional, CallableArgument, TypeOfAny + Type, UnboundType, TypeList, EllipsisType, AnyType, Optional, CallableArgument, TypeOfAny, + LiteralType, RawLiteralType, ) @@ -37,7 +38,12 @@ def expr_to_unanalyzed_type(expr: Expression, _parent: Optional[Expression] = No name = None # type: Optional[str] if isinstance(expr, NameExpr): name = expr.name - return UnboundType(name, line=expr.line, column=expr.column) + if name == 'True': + return RawLiteralType(True, 'builtins.bool', line=expr.line, column=expr.column) + elif name == 'False': + return RawLiteralType(False, 'builtins.bool', line=expr.line, column=expr.column) + else: + return UnboundType(name, line=expr.line, column=expr.column) elif isinstance(expr, MemberExpr): fullname = get_member_expr_fullname(expr) if fullname: @@ -108,11 +114,26 @@ def expr_to_unanalyzed_type(expr: Expression, _parent: Optional[Expression] = No elif isinstance(expr, (StrExpr, BytesExpr, UnicodeExpr)): # Parse string literal type. try: - result = parse_type_comment(expr.value, expr.line, None) - assert result is not None + node = parse_type_comment(expr.value, expr.line, None) + assert node is not None + if isinstance(node, UnboundType) and node.original_str_expr is None: + node.original_str_expr = expr.value + return node except SyntaxError: + return RawLiteralType(expr.value, 'builtins.str', line=expr.line, column=expr.column) + elif isinstance(expr, UnaryExpr): + typ = expr_to_unanalyzed_type(expr.expr) + if isinstance(typ, RawLiteralType) and isinstance(typ.value, int) and expr.op == '-': + typ.value *= -1 + return typ + else: raise TypeTranslationError() - return result + elif isinstance(expr, IntExpr): + return RawLiteralType(expr.value, 'builtins.int', line=expr.line, column=expr.column) + elif isinstance(expr, FloatExpr): + # Floats are not valid parameters for RawLiteralType, so we just + # pass in 'None' for now. We'll report the appropriate error at a later stage. + return RawLiteralType(None, 'builtins.float', line=expr.line, column=expr.column) elif isinstance(expr, EllipsisExpr): return EllipsisType(expr.line) else: diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 563e5d069828..ac1d28e98043 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -32,7 +32,7 @@ ) from mypy.types import ( Type, CallableType, AnyType, UnboundType, TupleType, TypeList, EllipsisType, CallableArgument, - TypeOfAny, Instance, + TypeOfAny, Instance, RawLiteralType, ) from mypy import defaults from mypy import messages @@ -53,6 +53,9 @@ Expression as ast3_Expression, Str, Index, + Num, + UnaryOp, + USub, ) except ImportError: if sys.version_info.minor > 2: @@ -1138,12 +1141,46 @@ def visit_Name(self, n: Name) -> Type: return UnboundType(n.id, line=self.line) def visit_NameConstant(self, n: NameConstant) -> Type: - return UnboundType(str(n.value)) + if isinstance(n.value, bool): + return RawLiteralType(n.value, 'builtins.bool', line=self.line) + else: + return UnboundType(str(n.value), line=self.line) + + # UnaryOp(op, operand) + def visit_UnaryOp(self, n: UnaryOp) -> Type: + # We support specifically Literal[-4] and nothing else. + # For example, Literal[+4] or Literal[~6] is not supported. + typ = self.visit(n.operand) + if isinstance(typ, RawLiteralType) and isinstance(n.op, USub): + if isinstance(typ.value, int): + typ.value *= -1 + return typ + self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(n, 'col_offset', -1)) + return AnyType(TypeOfAny.from_error) + + # Num(number n) + def visit_Num(self, n: Num) -> Type: + # Could be either float or int + numeric_value = n.n + if isinstance(numeric_value, int): + return RawLiteralType(numeric_value, 'builtins.int', line=self.line) + elif isinstance(numeric_value, float): + # Floats and other numbers are not valid parameters for RawLiteralType, so we just + # pass in 'None' for now. We'll report the appropriate error at a later stage. + return RawLiteralType(None, 'builtins.float', line=self.line) + else: + self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(n, 'col_offset', -1)) + return AnyType(TypeOfAny.from_error) # Str(string s) def visit_Str(self, n: Str) -> Type: - return (parse_type_comment(n.s.strip(), self.line, self.errors) or - AnyType(TypeOfAny.from_error)) + try: + node = parse_type_comment(n.s.strip(), self.line, errors=None) + if isinstance(node, UnboundType) and node.original_str_expr is None: + node.original_str_expr = n.s + return node or AnyType(TypeOfAny.from_error) + except SyntaxError: + return RawLiteralType(n.s, 'builtins.str', line=self.line) # Subscript(expr value, slice slice, expr_context ctx) def visit_Subscript(self, n: ast3.Subscript) -> Type: diff --git a/mypy/indirection.py b/mypy/indirection.py index 2776277acaa7..4e3390a65e3c 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -90,6 +90,9 @@ def visit_tuple_type(self, t: types.TupleType) -> Set[str]: def visit_typeddict_type(self, t: types.TypedDictType) -> Set[str]: return self._visit(t.items.values()) | self._visit(t.fallback) + def visit_raw_literal_type(self, t: types.RawLiteralType) -> Set[str]: + assert False, "Unexpected RawLiteralType after semantic analysis phase" + def visit_literal_type(self, t: types.LiteralType) -> Set[str]: return self._visit(t.fallback) diff --git a/mypy/meet.py b/mypy/meet.py index af2a8bc38ad9..ccf75eab98e9 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -235,6 +235,13 @@ def is_none_typevar_overlap(t1: Type, t2: Type) -> bool: elif isinstance(right, CallableType): right = right.fallback + if isinstance(left, LiteralType) and isinstance(right, LiteralType): + return left == right + elif isinstance(left, LiteralType): + left = left.fallback + elif isinstance(right, LiteralType): + right = right.fallback + # Finally, we handle the case where left and right are instances. if isinstance(left, Instance) and isinstance(right, Instance): diff --git a/mypy/messages.py b/mypy/messages.py index 2162c06acbf0..b5d254be2acf 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -19,7 +19,7 @@ from mypy.erasetype import erase_type from mypy.errors import Errors from mypy.types import ( - Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType, + Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType, LiteralType, UnionType, NoneTyp, AnyType, Overloaded, FunctionLike, DeletedType, TypeType, UninhabitedType, TypeOfAny, ForwardRef, UnboundType ) @@ -297,6 +297,8 @@ def format_bare(self, typ: Type, verbosity: int = 0) -> str: self.format_bare(item_type))) s = 'TypedDict({{{}}})'.format(', '.join(items)) return s + elif isinstance(typ, LiteralType): + return str(typ) elif isinstance(typ, UnionType): # Only print Unions as Optionals if the Optional wouldn't have to contain another Union print_as_optional = (len(typ.items) - diff --git a/mypy/semanal_newtype.py b/mypy/semanal_newtype.py index 4e41ea2a8d51..f4511e01bdee 100644 --- a/mypy/semanal_newtype.py +++ b/mypy/semanal_newtype.py @@ -5,7 +5,7 @@ from typing import Tuple, Optional -from mypy.types import Type, Instance, CallableType, NoneTyp, TupleType +from mypy.types import Type, Instance, CallableType, NoneTyp, TupleType, AnyType, TypeOfAny from mypy.nodes import ( AssignmentStmt, NewTypeExpr, CallExpr, NameExpr, RefExpr, Context, StrExpr, BytesExpr, UnicodeExpr, Block, FuncDef, Argument, TypeInfo, Var, SymbolTableNode, GDEF, MDEF, ARG_POS @@ -107,13 +107,21 @@ def check_newtype_args(self, name: str, call: CallExpr, context: Context) -> Opt has_failed = True # Check second argument + msg = "Argument 2 to NewType(...) must be a valid type" try: unanalyzed_type = expr_to_unanalyzed_type(args[1]) except TypeTranslationError: - self.fail("Argument 2 to NewType(...) must be a valid type", context) + self.fail(msg, context) return None + old_type = self.api.anal_type(unanalyzed_type) + # The caller of this function assumes that if we return a Type, it's always + # a valid one. So, we translate AnyTypes created from errors into None. + if isinstance(old_type, AnyType) and old_type.type_of_any == TypeOfAny.from_error: + self.fail(msg, context) + return None + return None if has_failed else old_type def build_newtype_typeinfo(self, name: str, old_type: Type, base_type: Instance) -> TypeInfo: diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 5086eebeba5e..82cec789c3d0 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -22,7 +22,7 @@ ) from mypy.types import ( Type, Instance, AnyType, TypeOfAny, CallableType, TupleType, TypeVarType, TypedDictType, - UnionType, TypeType, Overloaded, ForwardRef, TypeTranslator, function_type + UnionType, TypeType, Overloaded, ForwardRef, TypeTranslator, function_type, LiteralType, ) from mypy.errors import Errors, report_internal_error from mypy.options import Options @@ -704,6 +704,13 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: assert isinstance(fallback, Instance) return TypedDictType(items, t.required_keys, fallback, t.line, t.column) + def visit_literal_type(self, t: LiteralType) -> Type: + if self.check_recursion(t): + return AnyType(TypeOfAny.from_error) + fallback = self.visit_instance(t.fallback, from_fallback=True) + assert isinstance(fallback, Instance) + return LiteralType(t.value, fallback, t.line, t.column) + def visit_union_type(self, t: UnionType) -> Type: if self.check_recursion(t): return AnyType(TypeOfAny.from_error) diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index 241cdc988112..edfdc076e7d7 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -59,6 +59,7 @@ Type, SyntheticTypeVisitor, Instance, AnyType, NoneTyp, CallableType, DeletedType, PartialType, TupleType, TypeType, TypeVarType, TypedDictType, UnboundType, UninhabitedType, UnionType, Overloaded, TypeVarDef, TypeList, CallableArgument, EllipsisType, StarType, LiteralType, + RawLiteralType, ) from mypy.util import get_prefix, replace_object_state from mypy.typestate import TypeState @@ -391,6 +392,9 @@ def visit_typeddict_type(self, typ: TypedDictType) -> None: value_type.accept(self) typ.fallback.accept(self) + def visit_raw_literal_type(self, t: RawLiteralType) -> None: + assert False, "Unexpected RawLiteralType after semantic analysis phase" + def visit_literal_type(self, typ: LiteralType) -> None: typ.fallback.accept(self) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 0d8e62fa6d48..4c6633a294ad 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -327,8 +327,11 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool: else: return False - def visit_literal_type(self, t: LiteralType) -> bool: - raise NotImplementedError() + def visit_literal_type(self, left: LiteralType) -> bool: + if isinstance(self.right, LiteralType): + return left == self.right + else: + return self._is_subtype(left.fallback, self.right) def visit_overloaded(self, left: Overloaded) -> bool: right = self.right @@ -1172,7 +1175,10 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool: return self._is_proper_subtype(left.fallback, right) def visit_literal_type(self, left: LiteralType) -> bool: - raise NotImplementedError() + if isinstance(self.right, LiteralType): + return left == self.right + else: + return self._is_proper_subtype(left.fallback, self.right) def visit_overloaded(self, left: Overloaded) -> bool: # TODO: What's the right thing to do here? diff --git a/mypy/test/data.py b/mypy/test/data.py index e36edfae6c5a..3cab2f262c33 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -42,6 +42,7 @@ def parse_test_case(case: 'DataDrivenTestCase') -> None: join = posixpath.join # type: ignore out_section_missing = case.suite.required_out_section + normalize_output = True files = [] # type: List[Tuple[str, str]] # path and contents output_files = [] # type: List[Tuple[str, str]] # path and contents for output files @@ -98,8 +99,11 @@ def parse_test_case(case: 'DataDrivenTestCase') -> None: full = join(base_path, m.group(1)) deleted_paths.setdefault(num, set()).add(full) elif re.match(r'out[0-9]*$', item.id): + if item.arg == 'skip-path-normalization': + normalize_output = False + tmp_output = [expand_variables(line) for line in item.data] - if os.path.sep == '\\': + if os.path.sep == '\\' and normalize_output: tmp_output = [fix_win_path(line) for line in tmp_output] if item.id == 'out' or item.id == 'out1': output = tmp_output @@ -147,6 +151,7 @@ def parse_test_case(case: 'DataDrivenTestCase') -> None: case.expected_rechecked_modules = rechecked_modules case.deleted_paths = deleted_paths case.triggered = triggered or [] + case.normalize_output = normalize_output class DataDrivenTestCase(pytest.Item): # type: ignore # inheriting from Any @@ -168,6 +173,10 @@ class DataDrivenTestCase(pytest.Item): # type: ignore # inheriting from Any # Files/directories to clean up after test case; (is directory, path) tuples clean_up = None # type: List[Tuple[bool, str]] + # Whether or not we should normalize the output to standardize things like + # forward vs backward slashes in file paths for Windows vs Linux. + normalize_output = True + def __init__(self, parent: 'DataSuiteCollector', suite: 'DataSuite', diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 36c1d7f24465..642ce9355522 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -80,6 +80,7 @@ 'check-ctypes.test', 'check-dataclasses.test', 'check-final.test', + 'check-literal.test', ] @@ -177,7 +178,8 @@ def run_case_once(self, testcase: DataDrivenTestCase, assert sys.path[0] == plugin_dir del sys.path[0] - a = normalize_error_messages(a) + if testcase.normalize_output: + a = normalize_error_messages(a) # Make sure error messages match if incremental_step == 0: diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index b47afce62936..aeca03684d4d 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -85,14 +85,19 @@ def test_python_cmdline(testcase: DataDrivenTestCase) -> None: actual_output_content = output_file.read().splitlines() normalized_output = normalize_file_output(actual_output_content, os.path.abspath(test_temp_dir)) - if testcase.suite.native_sep and os.path.sep == '\\': - normalized_output = [fix_cobertura_filename(line) for line in normalized_output] - normalized_output = normalize_error_messages(normalized_output) + # We always normalize things like timestamp, but only handle operating-system + # specific things if requested. + if testcase.normalize_output: + if testcase.suite.native_sep and os.path.sep == '\\': + normalized_output = [fix_cobertura_filename(line) + for line in normalized_output] + normalized_output = normalize_error_messages(normalized_output) assert_string_arrays_equal(expected_content.splitlines(), normalized_output, 'Output file {} did not match its expected output'.format( path)) else: - out = normalize_error_messages(err + out) + if testcase.normalize_output: + out = normalize_error_messages(err + out) obvious_result = 1 if out else 0 if obvious_result != result: out.append('== Return code: {}'.format(result)) diff --git a/mypy/test/testmerge.py b/mypy/test/testmerge.py index 1a3648f6188e..37b1e7c935c3 100644 --- a/mypy/test/testmerge.py +++ b/mypy/test/testmerge.py @@ -93,7 +93,8 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: # Verify that old AST nodes are removed from the expression type map. assert expr not in new_types - a = normalize_error_messages(a) + if testcase.normalize_output: + a = normalize_error_messages(a) assert_string_arrays_equal( testcase.output, a, diff --git a/mypy/test/testsemanal.py b/mypy/test/testsemanal.py index b07f31fbf5d9..e86f645ed87e 100644 --- a/mypy/test/testsemanal.py +++ b/mypy/test/testsemanal.py @@ -30,6 +30,7 @@ 'semanal-abstractclasses.test', 'semanal-namedtuple.test', 'semanal-typeddict.test', + 'semenal-literal.test', 'semanal-classvar.test', 'semanal-python2.test'] @@ -78,6 +79,7 @@ def test_semanal(testcase: DataDrivenTestCase) -> None: if (not f.path.endswith((os.sep + 'builtins.pyi', 'typing.pyi', 'mypy_extensions.pyi', + 'typing_extensions.pyi', 'abc.pyi', 'collections.pyi')) and not os.path.basename(f.path).startswith('_') @@ -86,7 +88,8 @@ def test_semanal(testcase: DataDrivenTestCase) -> None: a += str(f).split('\n') except CompileError as e: a = e.messages - a = normalize_error_messages(a) + if testcase.normalize_output: + a = normalize_error_messages(a) assert_string_arrays_equal( testcase.output, a, 'Invalid semantic analyzer output ({}, line {})'.format(testcase.file, @@ -116,8 +119,10 @@ def test_semanal_error(testcase: DataDrivenTestCase) -> None: # Verify that there was a compile error and that the error messages # are equivalent. a = e.messages + if testcase.normalize_output: + a = normalize_error_messages(a) assert_string_arrays_equal( - testcase.output, normalize_error_messages(a), + testcase.output, a, 'Invalid compiler output ({}, line {})'.format(testcase.file, testcase.line)) diff --git a/mypy/test/testtransform.py b/mypy/test/testtransform.py index 9e22c15285c5..b4703f1906ac 100644 --- a/mypy/test/testtransform.py +++ b/mypy/test/testtransform.py @@ -66,7 +66,8 @@ def test_transform(testcase: DataDrivenTestCase) -> None: a += str(f).split('\n') except CompileError as e: a = e.messages - a = normalize_error_messages(a) + if testcase.normalize_output: + a = normalize_error_messages(a) assert_string_arrays_equal( testcase.output, a, 'Invalid semantic analyzer output ({}, line {})'.format(testcase.file, diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 15deeaa2343e..6ed2d25a652a 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -9,7 +9,7 @@ from mypy.meet import meet_types from mypy.types import ( UnboundType, AnyType, CallableType, TupleType, TypeVarDef, Type, Instance, NoneTyp, Overloaded, - TypeType, UnionType, UninhabitedType, true_only, false_only, TypeVarId, TypeOfAny + TypeType, UnionType, UninhabitedType, true_only, false_only, TypeVarId, TypeOfAny, LiteralType ) from mypy.nodes import ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, CONTRAVARIANT, INVARIANT, COVARIANT from mypy.subtypes import is_subtype, is_more_precise, is_proper_subtype @@ -245,6 +245,37 @@ def test_is_proper_subtype_invariance(self) -> None: assert_false(is_proper_subtype(fx.gb, fx.ga)) assert_false(is_proper_subtype(fx.ga, fx.gb)) + def test_is_proper_subtype_and_subtype_literal_types(self) -> None: + fx = self.fx + + lit1 = LiteralType(1, fx.a) + lit2 = LiteralType("foo", fx.b) + lit3 = LiteralType("bar", fx.b) + + assert_true(is_proper_subtype(lit1, fx.a)) + assert_false(is_proper_subtype(lit1, fx.b)) + assert_false(is_proper_subtype(fx.a, lit1)) + assert_true(is_proper_subtype(fx.uninhabited, lit1)) + assert_false(is_proper_subtype(lit1, fx.uninhabited)) + assert_true(is_proper_subtype(lit1, lit1)) + assert_false(is_proper_subtype(lit1, lit2)) + assert_false(is_proper_subtype(lit2, lit3)) + + assert_true(is_subtype(lit1, fx.a)) + assert_false(is_subtype(lit1, fx.b)) + assert_false(is_subtype(fx.a, lit1)) + assert_true(is_subtype(fx.uninhabited, lit1)) + assert_false(is_subtype(lit1, fx.uninhabited)) + assert_true(is_subtype(lit1, lit1)) + assert_false(is_subtype(lit1, lit2)) + assert_false(is_subtype(lit2, lit3)) + + assert_false(is_proper_subtype(lit1, fx.anyt)) + assert_false(is_proper_subtype(fx.anyt, lit1)) + + assert_true(is_subtype(lit1, fx.anyt)) + assert_true(is_subtype(fx.anyt, lit1)) + # can_be_true / can_be_false def test_empty_tuple_always_false(self) -> None: diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index e61776b02cc5..b30bb92df275 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -20,7 +20,7 @@ from mypy.types import ( Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType, LiteralType, - Instance, NoneTyp, TypeType, TypeOfAny, + RawLiteralType, Instance, NoneTyp, TypeType, TypeOfAny, UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef, UnboundType, ErasedType, ForwardRef, StarType, EllipsisType, TypeList, CallableArgument, ) @@ -127,6 +127,10 @@ def visit_callable_argument(self, t: CallableArgument) -> T: def visit_ellipsis_type(self, t: EllipsisType) -> T: pass + @abstractmethod + def visit_raw_literal_type(self, t: RawLiteralType) -> T: + pass + @trait class TypeTranslator(TypeVisitor[Type]): @@ -278,6 +282,9 @@ def visit_tuple_type(self, t: TupleType) -> T: def visit_typeddict_type(self, t: TypedDictType) -> T: return self.query_types(t.items.values()) + def visit_raw_literal_type(self, t: RawLiteralType) -> T: + return self.strategy([]) + def visit_literal_type(self, t: LiteralType) -> T: return self.strategy([]) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index b7a7f2dea98b..646bb4e05b0d 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -16,8 +16,9 @@ CallableType, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, SyntheticTypeVisitor, StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args, CallableArgument, get_type_vars, TypeQuery, union_items, TypeOfAny, ForwardRef, Overloaded, - LiteralType, + LiteralType, RawLiteralType, ) +from mypy.fastparse import TYPE_COMMENT_SYNTAX_ERROR from mypy.nodes import ( TVAR, MODULE_REF, UNBOUND_IMPORTED, TypeInfo, Context, SymbolTableNode, Var, Expression, @@ -283,6 +284,8 @@ def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: return item elif fullname in ('mypy_extensions.NoReturn', 'typing.NoReturn'): return UninhabitedType(is_noreturn=True) + elif fullname in ('typing_extensions.Literal', 'typing.Literal'): + return self.analyze_literal_type(t) elif isinstance(sym.node, TypeAlias): self.aliases_used.add(sym.node.fullname()) all_vars = sym.node.alias_tvars @@ -460,8 +463,32 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: ]) return TypedDictType(items, set(t.required_keys), t.fallback) + def visit_raw_literal_type(self, t: RawLiteralType) -> Type: + # We should never see a bare Literal. We synthesize these raw literals + # in the earlier stages of semantic analysis, but those + # "fake literals" should always be wrapped in an UnboundType + # corresponding to 'Literal'. + # + # Note: if at some point in the distant future, we decide to + # make signatures like "foo(x: 20) -> None" legal, we can change + # this method so it generates and returns an actual LiteralType + # instead. + if t.base_type_name == 'builtins.int' or t.base_type_name == 'builtins.bool': + # The only time it makes sense to use an int or bool is inside of + # a literal type. + self.fail("Invalid type: try using Literal[{}] instead?".format(repr(t.value)), t) + elif t.base_type_name == 'builtins.float': + self.fail("Invalid type: float literals cannot be used as a type", t) + else: + # For other types like strings, it's unclear if the user meant + # to construct a literal type or just misspelled a regular type. + # So, we leave just a generic "syntax error" error. + self.fail('Invalid type: ' + TYPE_COMMENT_SYNTAX_ERROR, t) + + return AnyType(TypeOfAny.from_error) + def visit_literal_type(self, t: LiteralType) -> Type: - raise NotImplementedError() + return t def visit_star_type(self, t: StarType) -> Type: return StarType(self.anal_type(t.type), t.line) @@ -562,6 +589,81 @@ def analyze_callable_args(self, arglist: TypeList) -> Optional[Tuple[List[Type], check_arg_kinds(kinds, [arglist] * len(args), self.fail) return args, kinds, names + def analyze_literal_type(self, t: UnboundType) -> Type: + if len(t.args) == 0: + self.fail('Literal[...] must have at least one parameter', t) + return AnyType(TypeOfAny.from_error) + + output = [] # type: List[Type] + for i, arg in enumerate(t.args): + analyzed_types = self.analyze_literal_param(i + 1, arg, t) + if analyzed_types is None: + return AnyType(TypeOfAny.from_error) + else: + output.extend(analyzed_types) + return UnionType.make_union(output, line=t.line) + + def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[List[Type]]: + # This UnboundType was originally defined as a string. + if isinstance(arg, UnboundType) and arg.original_str_expr is not None: + return [LiteralType( + value=arg.original_str_expr, + fallback=self.named_type('builtins.str'), + line=arg.line, + column=arg.column, + )] + + # If arg is an UnboundType that was *not* originally defined as + # a string, try expanding it in case it's a type alias or something. + if isinstance(arg, UnboundType): + arg = self.anal_type(arg) + + # Literal[...] cannot contain Any. Give up and add an error message + # (if we haven't already). + if isinstance(arg, AnyType): + # Note: We can encounter Literals containing 'Any' under three circumstances: + # + # 1. If the user attempts use an explicit Any as a parameter + # 2. If the user is trying to use an enum value imported from a module with + # no type hints, giving it an an implicit type of 'Any' + # 3. If there's some other underlying problem with the parameter. + # + # We report an error in only the first two cases. In the third case, we assume + # some other region of the code has already reported a more relevant error. + # + # TODO: Once we start adding support for enums, make sure we reprt a custom + # error for case 2 as well. + if arg.type_of_any != TypeOfAny.from_error: + self.fail('Parameter {} of Literal[...] cannot be of type "Any"'.format(idx), ctx) + return None + elif isinstance(arg, RawLiteralType): + # A raw literal. Convert it directly into a literal. + if arg.base_type_name == 'builtins.float': + self.fail( + 'Parameter {} of Literal[...] cannot be of type "float"'.format(idx), + ctx) + return None + + fallback = self.named_type(arg.base_type_name) + assert isinstance(fallback, Instance) + return [LiteralType(arg.value, fallback, line=arg.line, column=arg.column)] + elif isinstance(arg, (NoneTyp, LiteralType)): + # Types that we can just add directly to the literal/potential union of literals. + return [arg] + elif isinstance(arg, UnionType): + out = [] + for union_arg in arg.items: + union_result = self.analyze_literal_param(idx, union_arg, ctx) + if union_result is None: + return None + out.extend(union_result) + return out + elif isinstance(arg, ForwardRef): + return [arg] + else: + self.fail('Parameter {} of Literal[...] is invalid'.format(idx), ctx) + return None + def analyze_type(self, t: Type) -> Type: return t.accept(self) @@ -759,7 +861,18 @@ def visit_typeddict_type(self, t: TypedDictType) -> None: item_type.accept(self) def visit_literal_type(self, t: LiteralType) -> None: - raise NotImplementedError() + # We've already validated that the LiteralType + # contains either some literal expr like int, str, or + # bool in the previous pass -- we were able to do this + # since we had direct access to the underlying expression + # at those stages. + # + # The only thing we have left to check is to confirm + # whether LiteralTypes of the form 'Literal[Foo.bar]' + # contain enum members or not. + # + # TODO: implement this. + pass def visit_union_type(self, t: UnionType) -> None: for item in t.items: diff --git a/mypy/types.py b/mypy/types.py index cf9c887d6782..201c40b30045 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -38,9 +38,14 @@ # # Note: this type also happens to correspond to types that can be # directly converted into JSON. The serialize/deserialize methods -# of 'LiteralType' rely on this, as well 'server.astdiff.SnapshotTypeVisitor' -# and 'types.TypeStrVisitor'. If we end up adding any non-JSON-serializable -# types to this list, we should make sure to edit those methods to match. +# of 'LiteralType' relies on this, as well as +# 'server.astdiff.SnapshotTypeVisitor' and 'types.TypeStrVisitor'. +# If we end up adding any non-JSON-serializable types to this list, +# we should make sure to edit those methods to match. +# +# Alternatively, we should consider getting rid of this alias and +# moving any shared special serialization/deserialization code into +# RawLiteralType or something instead. LiteralValue = Union[int, str, bool, None] # If we only import type_visitor in the middle of the file, mypy @@ -236,48 +241,64 @@ def deserialize(cls, data: JsonDict) -> 'TypeVarDef': class UnboundType(Type): """Instance type that has not been bound during semantic analysis.""" - __slots__ = ('name', 'args', 'optional', 'empty_tuple_index') + __slots__ = ('name', 'args', 'optional', 'empty_tuple_index', 'original_str_expr') def __init__(self, - name: str, + name: Optional[str], args: Optional[List[Type]] = None, line: int = -1, column: int = -1, optional: bool = False, - empty_tuple_index: bool = False) -> None: + empty_tuple_index: bool = False, + original_str_expr: Optional[str] = None, + ) -> None: super().__init__(line, column) if not args: args = [] + assert name is not None self.name = name self.args = args # Should this type be wrapped in an Optional? self.optional = optional # Special case for X[()] self.empty_tuple_index = empty_tuple_index + # If this UnboundType was originally defined as a str, keep track of + # the original contents of that string. This way, if this UnboundExpr + # ever shows up inside of a LiteralType, we can determine whether that + # Literal[...] is valid or not. E.g. Literal[foo] is most likely invalid + # (unless 'foo' is an alias for another literal or something) and + # Literal["foo"] most likely is. + # + # We keep track of the entire string instead of just using a boolean flag + # so we can distinguish between things like Literal["foo"] vs + # Literal[" foo "]. + self.original_str_expr = original_str_expr def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_unbound_type(self) def __hash__(self) -> int: - return hash((self.name, self.optional, tuple(self.args))) + return hash((self.name, self.optional, tuple(self.args), self.original_str_expr)) def __eq__(self, other: object) -> bool: if not isinstance(other, UnboundType): return NotImplemented return (self.name == other.name and self.optional == other.optional and - self.args == other.args) + self.args == other.args and self.original_str_expr == other.original_str_expr) def serialize(self) -> JsonDict: return {'.class': 'UnboundType', 'name': self.name, 'args': [a.serialize() for a in self.args], + 'expr': self.original_str_expr, } @classmethod def deserialize(cls, data: JsonDict) -> 'UnboundType': assert data['.class'] == 'UnboundType' return UnboundType(data['name'], - [deserialize_type(a) for a in data['args']]) + [deserialize_type(a) for a in data['args']], + original_str_expr=data['expr']) class CallableArgument(Type): @@ -1256,6 +1277,58 @@ def zipall(self, right: 'TypedDictType') \ yield (item_name, None, right_item_type) +class RawLiteralType(Type): + """A synthetic type representing any type that could plausibly be something + that lives inside of a literal. + + This synthetic type is only used at the beginning stages of semantic analysis + and should be completely removing during the process for mapping UnboundTypes to + actual types. + + For example, `Foo[1]` is initially represented as the following: + + UnboundType( + name='Foo', + args=[ + RawLiteralType(value=1, base_type_name='builtins.int'), + ], + ) + + As we perform semantic analysis, this type will transform into one of two + possible forms. + + If 'Foo' was an alias for 'Literal' all along, this type is transformed into: + + LiteralType(value=1, fallback=int_instance_here) + + Alternatively, if 'Foo' is an unrelated class, we report an error and instead + produce something like this: + + Instance(type=typeinfo_for_foo, args=[AnyType(TypeOfAny.from_error)) + """ + def __init__(self, value: LiteralValue, base_type_name: str, + line: int = -1, column: int = -1) -> None: + super().__init__(line, column) + self.value = value + self.base_type_name = base_type_name + + def accept(self, visitor: 'TypeVisitor[T]') -> T: + assert isinstance(visitor, SyntheticTypeVisitor) + return visitor.visit_raw_literal_type(self) + + def serialize(self) -> JsonDict: + assert False, "Synthetic types don't serialize" + + def __hash__(self) -> int: + return hash((self.value, self.base_type_name)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, RawLiteralType): + return self.base_type_name == other.base_type_name and self.value == other.value + else: + return NotImplemented + + class LiteralType(Type): """The type of a Literal instance. Literal[Value] @@ -1757,6 +1830,9 @@ def item_str(name: str, typ: str) -> str: suffix = ', fallback={}'.format(t.fallback.accept(self)) return 'TypedDict({}{}{})'.format(prefix, s, suffix) + def visit_raw_literal_type(self, t: RawLiteralType) -> str: + return repr(t.value) + def visit_literal_type(self, t: LiteralType) -> str: return 'Literal[{}]'.format(repr(t.value)) diff --git a/test-data/unit/check-fastparse.test b/test-data/unit/check-fastparse.test index e01b7d9e5f5e..50a8c0c2263b 100644 --- a/test-data/unit/check-fastparse.test +++ b/test-data/unit/check-fastparse.test @@ -41,7 +41,6 @@ x = None # type: Tuple[x][x] # E: invalid type comment or annotation x = None # type: Iterable[x][x] # E: invalid type comment or annotation x = None # type: Callable[..., int][x] # E: invalid type comment or annotation x = None # type: Callable[..., int].x # E: invalid type comment or annotation -x = None # type: Tuple[1] # E: invalid type comment or annotation def f1(x): # E: invalid type comment or annotation # type: (Tuple[int, str].x) -> None @@ -61,9 +60,6 @@ def f5(x): # E: invalid type comment or annotation def f6(x): # E: invalid type comment or annotation # type: (Callable[..., int].x) -> None pass -def f7(x): # E: invalid type comment or annotation - # type: (Tuple[1]) -> None - pass [case testFastParseInvalidTypes3] @@ -77,7 +73,6 @@ x: Tuple[x][x] # E: invalid type comment or annotation x: Iterable[x][x] # E: invalid type comment or annotation x: Callable[..., int][x] # E: invalid type comment or annotation x: Callable[..., int].x # E: invalid type comment or annotation -x: Tuple[1] # E: invalid type comment or annotation x = None # type: Tuple[int, str].x # E: invalid type comment or annotation x = None # type: Iterable[x].x # E: invalid type comment or annotation @@ -85,7 +80,6 @@ x = None # type: Tuple[x][x] # E: invalid type comment or annotation x = None # type: Iterable[x][x] # E: invalid type comment or annotation x = None # type: Callable[..., int][x] # E: invalid type comment or annotation x = None # type: Callable[..., int].x # E: invalid type comment or annotation -x = None # type: Tuple[1] # E: invalid type comment or annotation def f1(x: Tuple[int, str].x) -> None: pass # E: invalid type comment or annotation def f2(x: Iterable[x].x) -> None: pass # E: invalid type comment or annotation @@ -93,7 +87,6 @@ def f3(x: Tuple[x][x]) -> None: pass # E: invalid type comment or annotation def f4(x: Iterable[x][x]) -> None: pass # E: invalid type comment or annotation def f5(x: Callable[..., int][x]) -> None: pass # E: invalid type comment or annotation def f6(x: Callable[..., int].x) -> None: pass # E: invalid type comment or annotation -def f7(x: Tuple[1]) -> None: pass # E: invalid type comment or annotation [case testFastParseProperty] diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index dc0963372ead..42ee137c19e4 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -1612,7 +1612,7 @@ def WrongArg(x, y): return y # something else sensible, because other tests require the stub not have anything # that looks like a function call. F = Callable[[WrongArg(int, 'x')], int] # E: Invalid argument constructor "__main__.WrongArg" -G = Callable[[Arg(1, 'x')], int] # E: Invalid type alias # E: Value of type "int" is not indexable +G = Callable[[Arg(1, 'x')], int] # E: Invalid type: try using Literal[1] instead? H = Callable[[VarArg(int, 'x')], int] # E: VarArg arguments should not have names I = Callable[[VarArg(int)], int] # ok J = Callable[[VarArg(), KwArg()], int] # ok @@ -1634,7 +1634,7 @@ from mypy_extensions import Arg, VarArg, KwArg def WrongArg(x, y): return y -def b(f: Callable[[Arg(1, 'x')], int]): pass # E: invalid type comment or annotation +def b(f: Callable[[Arg(1, 'x')], int]): pass # Invalid type. Try using Literal[1] instead? def d(f: Callable[[VarArg(int)], int]): pass # ok def e(f: Callable[[VarArg(), KwArg()], int]): pass # ok def g(f: Callable[[Arg(name='x', type=int)], int]): pass # ok @@ -1671,7 +1671,7 @@ e(f1) # E: Argument 1 to "e" has incompatible type "Callable[[VarArg(Any)], int [case testCallableWrongTypeType] from typing import Callable from mypy_extensions import Arg -def b(f: Callable[[Arg(1, 'x')], int]): pass # E: invalid type comment or annotation +def b(f: Callable[[Arg(1, 'x')], int]): pass # E: Invalid type: try using Literal[1] instead? [builtins fixtures/dict.pyi] [case testCallableTooManyVarArg] diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 6535efde1261..4bfcf192897c 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -1973,8 +1973,8 @@ T = TypeVar('T') class C(Sequence[T], Generic[T]): pass C[0] = 0 [out] -main:4: error: Type expected within [...] main:4: error: Unsupported target for indexed assignment +main:4: error: Invalid type: try using Literal[0] instead? [case testNoCrashOnPartialMember] class C: diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test new file mode 100644 index 000000000000..2fab35cc10bb --- /dev/null +++ b/test-data/unit/check-literal.test @@ -0,0 +1,662 @@ +-- +-- Check to see how we handle raw types, error handling, and other +-- semantic analysis shenanigans +-- + +[case testLiteralInvalidString] +from typing_extensions import Literal +def f1(x: 'A[') -> None: pass # E: Invalid type: syntax error in type comment +def g1(x: Literal['A[']) -> None: pass +reveal_type(f1) # E: Revealed type is 'def (x: Any)' +reveal_type(g1) # E: Revealed type is 'def (x: Literal['A['])' + +def f2(x: 'A B') -> None: pass # E: Invalid type: syntax error in type comment +def g2(x: Literal['A B']) -> None: pass +reveal_type(f2) # E: Revealed type is 'def (x: Any)' +reveal_type(g2) # E: Revealed type is 'def (x: Literal['A B'])' +[out] + +[case testLiteralInvalidTypeComment] +from typing_extensions import Literal +def f(x): # E: syntax error in type comment + # type: (A[) -> None + pass + +[case testLiteralInvalidTypeComment2] +from typing_extensions import Literal +def f(x): # E: Invalid type: syntax error in type comment + # type: ("A[") -> None + pass + +def g(x): + # type: (Literal["A["]) -> None + pass + +reveal_type(f) # E: Revealed type is 'def (x: Any)' +reveal_type(g) # E: Revealed type is 'def (x: Literal['A['])' +[out] + +[case testLiteralParsingPython2] +# flags: --python-version 2.7 +from typing import Optional +from typing_extensions import Literal + +def f(x): # E: Invalid type: syntax error in type comment + # type: ("A[") -> None + pass + +def g(x): + # type: (Literal["A["]) -> None + pass + +x = None # type: Optional[1] # E: Invalid type: try using Literal[1] instead? +y = None # type: Optional[Literal[1]] + +reveal_type(x) # E: Revealed type is 'Union[Any, None]' +reveal_type(y) # E: Revealed type is 'Union[Literal[1], None]' +[out] + +[case testLiteralInsideOtherTypes] +from typing import Tuple +from typing_extensions import Literal + +x: Tuple[1] # E: Invalid type: try using Literal[1] instead? +def foo(x: Tuple[1]) -> None: ... # E: Invalid type: try using Literal[1] instead? + +y: Tuple[Literal[2]] +def bar(x: Tuple[Literal[2]]) -> None: ... +reveal_type(x) # E: Revealed type is 'Tuple[Any]' +reveal_type(y) # E: Revealed type is 'Tuple[Literal[2]]' +reveal_type(bar) # E: Revealed type is 'def (x: Tuple[Literal[2]])' +[out] + +[case testLiteralInsideOtherTypesPython2] +# flags: --python-version 2.7 +from typing import Tuple, Optional +from typing_extensions import Literal + +x = None # type: Optional[Tuple[1]] # E: Invalid type: try using Literal[1] instead? +def foo(x): # E: Invalid type: try using Literal[1] instead? + # type: (Tuple[1]) -> None + pass + +y = None # type: Optional[Tuple[Literal[2]]] +def bar(x): + # type: (Tuple[Literal[2]]) -> None + pass +reveal_type(x) # E: Revealed type is 'Union[Tuple[Any], None]' +reveal_type(y) # E: Revealed type is 'Union[Tuple[Literal[2]], None]' +reveal_type(bar) # E: Revealed type is 'def (x: Tuple[Literal[2]])' +[out] + +[case testLiteralInsideOtherTypesTypeCommentsPython3] +# flags: --python-version 3.7 +from typing import Tuple, Optional +from typing_extensions import Literal + +x = None # type: Optional[Tuple[1]] # E: Invalid type: try using Literal[1] instead? +def foo(x): # E: Invalid type: try using Literal[1] instead? + # type: (Tuple[1]) -> None + pass + +y = None # type: Optional[Tuple[Literal[2]]] +def bar(x): + # type: (Tuple[Literal[2]]) -> None + pass +reveal_type(x) # E: Revealed type is 'Union[Tuple[Any], None]' +reveal_type(y) # E: Revealed type is 'Union[Tuple[Literal[2]], None]' +reveal_type(bar) # E: Revealed type is 'def (x: Tuple[Literal[2]])' +[out] + +[case testLiteralRenamingImportWorks] +from typing_extensions import Literal as Foo + +x: Foo[3] +reveal_type(x) # E: Revealed type is 'Literal[3]' + +y: Foo["hello"] +reveal_type(y) # E: Revealed type is 'Literal['hello']' +[out] + +[case testLiteralRenamingImportViaAnotherImportWorks] +from other_module import Foo, Bar + +x: Foo[3] +y: Bar + +reveal_type(x) # E: Revealed type is 'Literal[3]' +reveal_type(y) # E: Revealed type is 'Literal[4]' + +[file other_module.py] +from typing_extensions import Literal as Foo +Bar = Foo[4] +[out] + +[case testLiteralRenamingImportNameConfusion] +from typing_extensions import Literal as Foo + +x: Foo["Foo"] +reveal_type(x) # E: Revealed type is 'Literal['Foo']' + +y: Foo[Foo] # E: Literal[...] must have at least one parameter +[out] + +-- +-- Check to make sure we can construct the correct range of literal +-- types (and correctly reject invalid literal types) +-- +-- Note: the assignment tests exercise the logic in 'fastparse.py'; +-- the type alias tests exercise the logic in 'exprtotype.py'. +-- + +[case testLiteralBasicIntUsage] +from typing_extensions import Literal + +a1: Literal[4] +b1: Literal[0x2a] +c1: Literal[-300] + +reveal_type(a1) # E: Revealed type is 'Literal[4]' +reveal_type(b1) # E: Revealed type is 'Literal[42]' +reveal_type(c1) # E: Revealed type is 'Literal[-300]' + +a2t = Literal[4] +b2t = Literal[0x2a] +c2t = Literal[-300] +a2: a2t +b2: b2t +c2: c2t + +reveal_type(a2) # E: Revealed type is 'Literal[4]' +reveal_type(b2) # E: Revealed type is 'Literal[42]' +reveal_type(c2) # E: Revealed type is 'Literal[-300]' +[out] + +[case testLiteralBasicBoolUsage] +from typing_extensions import Literal + +a1: Literal[True] +b1: Literal[False] + +reveal_type(a1) # E: Revealed type is 'Literal[True]' +reveal_type(b1) # E: Revealed type is 'Literal[False]' + +a2t = Literal[True] +b2t = Literal[False] +a2: a2t +b2: b2t + +reveal_type(a2) # E: Revealed type is 'Literal[True]' +reveal_type(b2) # E: Revealed type is 'Literal[False]' +[builtins fixtures/bool.pyi] +[out] + +[case testLiteralBasicStrUsage] +from typing_extensions import Literal + +a: Literal[""] +b: Literal[" foo bar "] +c: Literal[' foo bar '] + +reveal_type(a) # E: Revealed type is 'Literal['']' +reveal_type(b) # E: Revealed type is 'Literal[' foo bar ']' +reveal_type(c) # E: Revealed type is 'Literal[' foo bar ']' +[out] + +[case testLiteralBasicStrUsageSlashes] +from typing_extensions import Literal + +a: Literal[r"foo\nbar"] +b: Literal["foo\nbar"] + +reveal_type(a) +reveal_type(b) +[out skip-path-normalization] +main:6: error: Revealed type is 'Literal['foo\\nbar']' +main:7: error: Revealed type is 'Literal['foo\nbar']' + +[case testLiteralBasicNoneUsage] +from typing_extensions import Literal +a: Literal[None] +reveal_type(a) # E: Revealed type is 'None' +# Note: Literal[None] and None are equivalent +[out] + +[case testLiteralDisallowAny] +from typing import Any +from typing_extensions import Literal +from missing_module import BadAlias # E: Cannot find module named 'missing_module' \ + # N: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports + +a: Literal[Any] # E: Parameter 1 of Literal[...] cannot be of type "Any" +b: Literal[BadAlias] # E: Parameter 1 of Literal[...] cannot be of type "Any" + +reveal_type(a) # E: Revealed type is 'Any' +reveal_type(b) # E: Revealed type is 'Any' +[out] + +[case testLiteralDisallowActualTypes] +from typing_extensions import Literal + +a: Literal[int] # E: Parameter 1 of Literal[...] is invalid +b: Literal[float] # E: Parameter 1 of Literal[...] is invalid +c: Literal[bool] # E: Parameter 1 of Literal[...] is invalid +d: Literal[str] # E: Parameter 1 of Literal[...] is invalid + +reveal_type(a) # E: Revealed type is 'Any' +reveal_type(b) # E: Revealed type is 'Any' +reveal_type(c) # E: Revealed type is 'Any' +reveal_type(d) # E: Revealed type is 'Any' +[builtins fixtures/primitives.pyi] +[out] + +[case testLiteralDisallowFloats] +from typing_extensions import Literal +a1: Literal[3.14] # E: Parameter 1 of Literal[...] cannot be of type "float" +b1: 3.14 # E: Invalid type: float literals cannot be used as a type + +a2t = Literal[3.14] # E: Parameter 1 of Literal[...] cannot be of type "float" +b2t = 3.14 + +a2: a2t +reveal_type(a2) # E: Revealed type is 'Any' +b2: b2t # E: Invalid type "__main__.b2t" + +[out] + +[case testLiteralDisallowComplexNumbers] +from typing_extensions import Literal +a: Literal[3j] # E: invalid type comment or annotation +b: Literal[3j + 2] # E: invalid type comment or annotation +c: 3j # E: invalid type comment or annotation +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 +a: at # E: Invalid type "__main__.at" +[builtins fixtures/complex.pyi] +[out] + +[case testLiteralDisallowComplexExpressions] +from typing_extensions import Literal +a: Literal[3 + 4] # E: invalid type comment or annotation +b: Literal[" foo ".trim()] # E: invalid type comment or annotation +c: Literal[+42] # E: invalid type comment or annotation +d: Literal[~12] # E: invalid type comment or annotation +[out] + +[case testLiteralDisallowCollections] +from typing_extensions import Literal +a: Literal[{"a": 1, "b": 2}] # E: invalid type comment or annotation +b: literal[{1, 2, 3}] # E: invalid type comment or annotation +c: {"a": 1, "b": 2} # E: invalid type comment or annotation +d: {1, 2, 3} # E: invalid type comment or annotation + +[case testLiteralDisallowCollections2] +from typing_extensions import Literal +a: (1, 2, 3) # E: Syntax error in type annotation \ + # N: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn) +b: Literal[[1, 2, 3]] # E: Parameter 1 of Literal[...] is invalid +c: [1, 2, 3] # E: Invalid type +[out] + +[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 +bt = {"a": 1, "b": 2} +a: at # E: Invalid type "__main__.at" +b: bt # E: Invalid type "__main__.bt" +[builtins fixtures/dict.pyi] +[out] + +[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 +bt = {1, 2, 3} +a: at # E: Invalid type "__main__.at" +b: bt # E: Invalid type "__main__.bt" +[builtins fixtures/set.pyi] +[out] + + +-- +-- Test mixing and matching literals with other types +-- + +[case testLiteralMultipleValues] +# flags: --strict-optional +from typing_extensions import Literal +a: Literal[1, 2, 3] +b: Literal["a", "b", "c"] +c: Literal[1, "b", True, None] +d: Literal[1, 1, 1] +e: Literal[None, None, None] +reveal_type(a) # E: Revealed type is 'Union[Literal[1], Literal[2], Literal[3]]' +reveal_type(b) # E: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c']]' +reveal_type(c) # E: Revealed type is 'Union[Literal[1], Literal['b'], Literal[True], None]' + +# Note: I was thinking these should be simplified, but it seems like +# mypy doesn't simplify unions with duplicate values with other types. +reveal_type(d) # E: Revealed type is 'Union[Literal[1], Literal[1], Literal[1]]' +reveal_type(e) # E: Revealed type is 'Union[None, None, None]' +[builtins fixtures/bool.pyi] +[out] + +[case testLiteralMultipleValuesExplicitTuple] +from typing_extensions import Literal +# Unfortunately, it seems like typed_ast is unable to distinguish this from +# Literal[1, 2, 3]. So we treat the two as being equivalent for now. +a: Literal[1, 2, 3] +b: Literal[(1, 2, 3)] +reveal_type(a) # E: Revealed type is 'Union[Literal[1], Literal[2], Literal[3]]' +reveal_type(b) # E: Revealed type is 'Union[Literal[1], Literal[2], Literal[3]]' +[out] + +[case testLiteralNestedUsage] +# flags: --strict-optional + +from typing_extensions import Literal +a: Literal[Literal[3], 4, Literal["foo"]] +reveal_type(a) # E: Revealed type is 'Union[Literal[3], Literal[4], Literal['foo']]' + +alias_for_literal = Literal[5] +b: Literal[alias_for_literal] +reveal_type(b) # E: Revealed type is 'Literal[5]' + +another_alias = Literal[1, None] +c: Literal[alias_for_literal, another_alias, "r"] +reveal_type(c) # E: Revealed type is 'Union[Literal[5], Literal[1], None, Literal['r']]' + +basic_mode = Literal["r", "w", "a"] +basic_with_plus = Literal["r+", "w+", "a+"] +combined: Literal[basic_mode, basic_with_plus] +reveal_type(combined) # E: Revealed type is 'Union[Literal['r'], Literal['w'], Literal['a'], Literal['r+'], Literal['w+'], Literal['a+']]' +[out] + +[case testLiteralBiasTowardsAssumingForwardReference] +from typing_extensions import Literal + +a: "Foo" +reveal_type(a) # E: Revealed type is '__main__.Foo' + +b: Literal["Foo"] +reveal_type(b) # E: Revealed type is 'Literal['Foo']' + +c: "Literal[Foo]" # E: Parameter 1 of Literal[...] is invalid + +d: "Literal['Foo']" +reveal_type(d) # E: Revealed type is 'Literal['Foo']' + +class Foo: pass +[out] + +[case testLiteralBiasTowardsAssumingForwardReferenceForTypeAliases] +from typing_extensions import Literal + +a: "Foo" +reveal_type(a) # E: Revealed type is 'Literal[5]' + +b: Literal["Foo"] +reveal_type(b) # E: Revealed type is 'Literal['Foo']' + +c: "Literal[Foo]" +reveal_type(c) # E: Revealed type is 'Literal[5]' + +d: "Literal['Foo']" +reveal_type(d) # E: Revealed type is 'Literal['Foo']' + +e: Literal[Foo, 'Foo'] +reveal_type(e) # E: Revealed type is 'Union[Literal[5], Literal['Foo']]' + +Foo = Literal[5] +[out] + +[case testLiteralBiasTowardsAssumingForwardReferencesForTypeComments] +from typing_extensions import Literal + +a = None # type: Foo +reveal_type(a) # E: Revealed type is '__main__.Foo' + +b = None # type: "Foo" +reveal_type(b) # E: Revealed type is '__main__.Foo' + +c = None # type: Literal["Foo"] +reveal_type(c) # E: Revealed type is 'Literal['Foo']' + +d = None # type: Literal[Foo] # E: Parameter 1 of Literal[...] is invalid + +class Foo: pass +[out] + + +-- +-- Check how we handle very basic subtyping and other useful things +-- + +[case testLiteralCallingFunction] +from typing_extensions import Literal +def foo(x: Literal[3]) -> None: pass + +a: Literal[1] +b: Literal[2] +c: int + +foo(a) # E: Argument 1 to "foo" has incompatible type "Literal[1]"; expected "Literal[3]" +foo(b) # E: Argument 1 to "foo" has incompatible type "Literal[2]"; expected "Literal[3]" +foo(c) # E: Argument 1 to "foo" has incompatible type "int"; expected "Literal[3]" +[out] + +[case testLiteralCallingFunctionWithUnionLiteral] +from typing_extensions import Literal +def foo(x: Literal[1, 2, 3]) -> None: pass + +a: Literal[1] +b: Literal[2, 3] +c: Literal[4, 5] +d: int + +foo(a) +foo(b) +foo(c) # E: Argument 1 to "foo" has incompatible type "Union[Literal[4], Literal[5]]"; expected "Union[Literal[1], Literal[2], Literal[3]]" +foo(d) # E: Argument 1 to "foo" has incompatible type "int"; expected "Union[Literal[1], Literal[2], Literal[3]]" +[out] + +[case testLiteralCallingFunctionWithStandardBase] +from typing_extensions import Literal +def foo(x: int) -> None: pass + +a: Literal[1] +b: Literal[1, -4] +c: Literal[4, 'foo'] + +foo(a) +foo(b) +foo(c) # E: Argument 1 to "foo" has incompatible type "Union[Literal[4], Literal['foo']]"; expected "int" +[out] + +[case testLiteralCheckSubtypingStrictOptional] +# flags: --strict-optional +from typing import Any, NoReturn +from typing_extensions import Literal + +lit: Literal[1] +def f_lit(x: Literal[1]) -> None: pass + +def fa(x: Any) -> None: pass +def fb(x: NoReturn) -> None: pass +def fc(x: None) -> None: pass + +a: Any +b: NoReturn +c: None + +fa(lit) +fb(lit) # E: Argument 1 to "fb" has incompatible type "Literal[1]"; expected "NoReturn" +fc(lit) # E: Argument 1 to "fc" has incompatible type "Literal[1]"; expected "None" + +f_lit(a) +f_lit(b) +f_lit(c) # E: Argument 1 to "f_lit" has incompatible type "None"; expected "Literal[1]" +[out] + +[case testLiteralCheckSubtypingNoStrictOptional] +# flags: --no-strict-optional +from typing import Any, NoReturn +from typing_extensions import Literal + +lit: Literal[1] +def f_lit(x: Literal[1]) -> None: pass + +def fa(x: Any) -> None: pass +def fb(x: NoReturn) -> None: pass +def fc(x: None) -> None: pass + +a: Any +b: NoReturn +c: None + +fa(lit) +fb(lit) # E: Argument 1 to "fb" has incompatible type "Literal[1]"; expected "NoReturn" +fc(lit) # E: Argument 1 to "fc" has incompatible type "Literal[1]"; expected "None" + +f_lit(a) +f_lit(b) +f_lit(c) +[out] + + +[case testLiteralCallingOverloadedFunction] +from typing import overload, Generic, TypeVar, Any +from typing_extensions import Literal + +T = TypeVar('T') +class IOLike(Generic[T]): pass + +@overload +def foo(x: Literal[1]) -> IOLike[int]: ... +@overload +def foo(x: Literal[2]) -> IOLike[str]: ... +@overload +def foo(x: int) -> IOLike[Any]: ... +def foo(x: int) -> IOLike[Any]: + if x == 1: + return IOLike[int]() + elif x == 2: + return IOLike[str]() + else: + return IOLike() + +a: Literal[1] +b: Literal[2] +c: int +d: Literal[3] + +reveal_type(foo(a)) # E: Revealed type is '__main__.IOLike[builtins.int]' +reveal_type(foo(b)) # E: Revealed type is '__main__.IOLike[builtins.str]' +reveal_type(foo(c)) # E: Revealed type is '__main__.IOLike[Any]' +foo(d) +[builtins fixtures/ops.pyi] +[out] + +[case testLiteralVariance] +from typing import Generic, TypeVar +from typing_extensions import Literal + +T = TypeVar('T') +T_co = TypeVar('T_co', covariant=True) +T_contra = TypeVar('T_contra', contravariant=True) + +class Invariant(Generic[T]): pass +class Covariant(Generic[T_co]): pass +class Contravariant(Generic[T_contra]): pass + +a1: Invariant[Literal[1]] +a2: Invariant[Literal[1, 2]] +a3: Invariant[Literal[1, 2, 3]] +a2 = a1 # E: Incompatible types in assignment (expression has type "Invariant[Literal[1]]", variable has type "Invariant[Union[Literal[1], Literal[2]]]") +a2 = a3 # E: Incompatible types in assignment (expression has type "Invariant[Union[Literal[1], Literal[2], Literal[3]]]", variable has type "Invariant[Union[Literal[1], Literal[2]]]") + +b1: Covariant[Literal[1]] +b2: Covariant[Literal[1, 2]] +b3: Covariant[Literal[1, 2, 3]] +b2 = b1 +b2 = b3 # E: Incompatible types in assignment (expression has type "Covariant[Union[Literal[1], Literal[2], Literal[3]]]", variable has type "Covariant[Union[Literal[1], Literal[2]]]") + +c1: Contravariant[Literal[1]] +c2: Contravariant[Literal[1, 2]] +c3: Contravariant[Literal[1, 2, 3]] +c2 = c1 # E: Incompatible types in assignment (expression has type "Contravariant[Literal[1]]", variable has type "Contravariant[Union[Literal[1], Literal[2]]]") +c2 = c3 +[out] + +[case testLiteralInListAndSequence] +from typing import List, Sequence +from typing_extensions import Literal + +def foo(x: List[Literal[1, 2]]) -> None: pass +def bar(x: Sequence[Literal[1, 2]]) -> None: pass + +a: List[Literal[1]] +b: List[Literal[1, 2, 3]] + +foo(a) # E: Argument 1 to "foo" has incompatible type "List[Literal[1]]"; expected "List[Union[Literal[1], Literal[2]]]" \ + # N: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \ + # N: Consider using "Sequence" instead, which is covariant +foo(b) # E: Argument 1 to "foo" has incompatible type "List[Union[Literal[1], Literal[2], Literal[3]]]"; expected "List[Union[Literal[1], Literal[2]]]" +bar(a) +bar(b) # E: Argument 1 to "bar" has incompatible type "List[Union[Literal[1], Literal[2], Literal[3]]]"; expected "Sequence[Union[Literal[1], Literal[2]]]" +[builtins fixtures/list.pyi] +[out] + +[case testLiteralRenamingDoesNotChangeTypeChecking] +from typing_extensions import Literal as Foo +from other_module import Bar1, Bar2, c + +def func(x: Foo[15]) -> None: pass + +a: Bar1 +b: Bar2 +func(a) +func(b) # E: Argument 1 to "func" has incompatible type "Literal[14]"; expected "Literal[15]" +func(c) + +[file other_module.py] +from typing_extensions import Literal + +Bar1 = Literal[15] +Bar2 = Literal[14] +c: Literal[15] + + +-- +-- Here are a few misc tests that deliberately do not work. +-- I'm including these as skipped tests partly because I wanted to +-- clarify the scope of what this diff did and did not do, and +-- partly because I already wrote these and would like to avoid having +-- to rewrite them in the future. +-- + +[case testLiteralInheritedMethodsInteractCorrectly-skip] +# TODO: fix this test. The method calls are not using the fallbacks. +from typing_extensions import Literal + +a: Literal[3] +b: int +c: Literal['foo'] + +reveal_type(a + a) # E: Revealed type is 'builtins.int' +reveal_type(a + b) # E: Revealed type is 'builtins.int' +reveal_type(b + a) # E: Revealed type is 'builtins.int' +reveal_type(c.strip()) # E: Revealed type is 'builtins.str' +[out] + +[case testLiteralActualAssignment-skip] +# TODO: fix this test. The 1 is currently always given a type of 'int' +from typing_extensions import Literal + +a: Literal[1] = 1 +[out] diff --git a/test-data/unit/check-newtype.test b/test-data/unit/check-newtype.test index 25485f627d97..c7f43017003a 100644 --- a/test-data/unit/check-newtype.test +++ b/test-data/unit/check-newtype.test @@ -269,12 +269,13 @@ tmp/m.py:14: error: Revealed type is 'builtins.int' from typing import NewType a = NewType('b', int) # E: String argument 1 'b' to NewType(...) does not match variable name 'a' -b = NewType('b', 3) # E: Argument 2 to NewType(...) must be a valid type -c = NewType(2, int) # E: Argument 1 to NewType(...) must be a string literal +b = NewType('b', 3) # E: Argument 2 to NewType(...) must be a valid type \ + # E: Invalid type: try using Literal[3] instead? +c = NewType(2, int) # E: Argument 1 to NewType(...) must be a string literal foo = "d" d = NewType(foo, int) # E: Argument 1 to NewType(...) must be a string literal -e = NewType(name='e', tp=int) # E: NewType(...) expects exactly two positional arguments -f = NewType('f', tp=int) # E: NewType(...) expects exactly two positional arguments +e = NewType(name='e', tp=int) # E: NewType(...) expects exactly two positional arguments +f = NewType('f', tp=int) # E: NewType(...) expects exactly two positional arguments [out] [case testNewTypeWithAnyFails] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index fc5bc3d3e1ed..96ece04686cc 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1141,7 +1141,7 @@ Point = TypedDict('Point', {int: int, int: int}) # E: Invalid TypedDict() field [case testCannotCreateTypedDictTypeWithInvalidItemType] from mypy_extensions import TypedDict -Point = TypedDict('Point', {'x': 1, 'y': 1}) # E: Invalid field type +Point = TypedDict('Point', {'x': 1, 'y': 1}) # E: Invalid type: try using Literal[1] instead? [builtins fixtures/dict.pyi] [case testCannotCreateTypedDictTypeWithInvalidName] diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 644a5a997562..2b75d3305ce2 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -7,3 +7,5 @@ def runtime(x: _T) -> _T: pass class Final: pass def final(x: _T) -> _T: pass + +class Literal: pass diff --git a/test-data/unit/parse-errors.test b/test-data/unit/parse-errors.test index 59886173ae50..56920af71917 100644 --- a/test-data/unit/parse-errors.test +++ b/test-data/unit/parse-errors.test @@ -121,20 +121,6 @@ def f(**x, y=x): [out] file:1: error: invalid syntax -[case testInvalidStringLiteralType] -def f(x: - 'A[' - ) -> None: pass -[out] -file:1: error: syntax error in type comment - -[case testInvalidStringLiteralType2] -def f(x: - 'A B' - ) -> None: pass -[out] -file:1: error: syntax error in type comment - [case testInvalidTypeComment] 0 x = 0 # type: A A @@ -159,11 +145,6 @@ x = 0 # type: * [out] file:2: error: syntax error in type comment -[case testInvalidMultilineLiteralType] -def f() -> "A\nB": pass -[out] -file:1: error: syntax error in type comment - [case testInvalidSignatureInComment1] def f(): # type: x pass diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index 342d0fa749ec..5ad34c274a47 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -877,7 +877,7 @@ A[TypeVar] # E: Invalid type "typing.TypeVar" from typing import TypeVar, Generic t = TypeVar('t') class A(Generic[t]): pass -A[1] # E: Type expected within [...] +A[1] # E: Invalid type: try using Literal[1] instead? [out] [case testVariableDeclWithInvalidNumberOfTypes] @@ -980,7 +980,7 @@ e = TypeVar('e', int, str, x=1) # E: Unexpected argument to TypeVar(): x f = TypeVar('f', (int, str), int) # E: Type expected g = TypeVar('g', int) # E: TypeVar cannot have only a single constraint h = TypeVar('h', x=(int, str)) # E: Unexpected argument to TypeVar(): x -i = TypeVar('i', bound=1) # E: TypeVar 'bound' must be a type +i = TypeVar('i', bound=1) # E: Invalid type: try using Literal[1] instead? [out] [case testMoreInvalidTypevarArguments] @@ -993,7 +993,7 @@ S = TypeVar('S', covariant=True, contravariant=True) \ [case testInvalidTypevarValues] from typing import TypeVar b = TypeVar('b', *[int]) # E: Unexpected argument to TypeVar() -c = TypeVar('c', int, 2) # E: Type expected +c = TypeVar('c', int, 2) # E: Invalid type: try using Literal[2] instead? [out] [case testObsoleteTypevarValuesSyntax] @@ -1056,8 +1056,16 @@ from typing import Generic as t # E: Name 't' already defined on line 2 def f(x: 'foo'): pass # E: Name 'foo' is not defined [out] -[case testInvalidStrLiteralType2] -def f(x: 'int['): pass # E: syntax error in type comment +[case testInvalidStrLiteralStrayBrace] +def f(x: 'int['): pass # E: Invalid type: syntax error in type comment +[out] + +[case testInvalidStrLiteralSpaces] +def f(x: 'A B'): pass # E: Invalid type: syntax error in type comment +[out] + +[case testInvalidMultilineLiteralType] +def f() -> "A\nB": pass # E: Invalid type: syntax error in type comment [out] [case testInconsistentOverload] diff --git a/test-data/unit/semenal-literal.test b/test-data/unit/semenal-literal.test new file mode 100644 index 000000000000..cab131b951a9 --- /dev/null +++ b/test-data/unit/semenal-literal.test @@ -0,0 +1,27 @@ +[case testLiteralSemanalBasicAssignment] +from typing_extensions import Literal +foo: Literal[3] +[out] +MypyFile:1( + ImportFrom:1(typing_extensions, [Literal]) + AssignmentStmt:2( + NameExpr(foo [__main__.foo]) + TempNode:-1( + Any) + Literal[3])) + +[case testLiteralSemenalInFunction] +from typing_extensions import Literal +def foo(a: Literal[1], b: Literal[" foo "]) -> Literal[True]: pass +[builtins fixtures/bool.pyi] +[out] +MypyFile:1( + ImportFrom:1(typing_extensions, [Literal]) + FuncDef:2( + foo + Args( + Var(a) + Var(b)) + def (a: Literal[1], b: Literal[' foo ']) -> Literal[True] + Block:2( + PassStmt:2())))