From 0147a76e54149f9387713d147395d0ccab80bc5b Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 23 Nov 2018 15:02:17 -0800 Subject: [PATCH 01/10] Add basic end-to-end support for Literal types This pull request primarily adds support for string, int, bool, and None Literal types in the parsing and semantic analysis layer. It also adds a small smidge of logic to the subtyping and meet parts of the typechecking layer -- just enough so we can start using Literal types end-to-end and write some tests. Basically, my previous diff made a bunch of gross changes in a horizontal way to lay out a foundation; this diff makes a bunch of gross changes in a vertical way to create a proof-of-concept of the proof-of-concept. I'll probably need to submit a few follow-up PRs cleaning up some stuff in the semantic analysis layer, but my hope is to switch focus on fleshing out the type checking layer shortly after this PR lands. Specific changes made: 1. I added a new 'RawLiteralType' synthetic type meant to represent any literal expression that appears in the earliest stages of semantic analysis. If these 'RawLiteralTypes' appear in a "Literal[...]" context, they transformed into regular 'LiteralTypes' during phase 2 of semantic analysis (turning UnboundTypes into actual types). If they appear outside of the correct context, we report an error instead. I also added a string field to UnboundType to keep track of similar information. Basically, if we have `Foo["bar"]`, we don't actually know immediately whether or not that "bar" is meant to be string literal vs a forward reference. (Foo could be a regular class or an alias for 'Literal'). Note: I wanted to avoid having to introduce yet another type, so looked into perhaps attaching even more metadata to UnboundType or perhaps to LiteralType. Both of those approaches ended up looking pretty messy though, so I went with this. 2. As a consequence, some of the syntax checking logic had to move from the parsing layer to the semantic analysis layer, and some of the existing error messages changed slightly. 3. The PEP, at some point, provisionally rejected having Literals contain other Literal types. For example, something like this: RegularIds = Literal[1, 2, 3, 4] AdminIds = Literal[100, 101, 102] AllIds = Literal[RegularIds, AdminIds, 30, 32] This was mainly because I thought this would be challenging to implement, but it turned out to be easier then expected -- it happened almost by accident while I was implementing support for 'Literal[1, 2, 3]'. I can excise this part out if we think supporting this kind of thing is a fundamentally bad idea. 4. I also penciled in some minimal code to the subtyping and meet logic. --- mypy/exprtotype.py | 29 +- mypy/fastparse.py | 41 +- mypy/indirection.py | 3 + mypy/meet.py | 7 + mypy/messages.py | 4 +- mypy/semanal_pass3.py | 9 +- mypy/server/astmerge.py | 4 + mypy/subtypes.py | 12 +- mypy/test/testcheck.py | 1 + mypy/test/testsemanal.py | 2 + mypy/type_visitor.py | 9 +- mypy/typeanal.py | 90 +++- mypy/types.py | 96 +++- test-data/unit/check-fastparse.test | 7 - test-data/unit/check-functions.test | 6 +- test-data/unit/check-inference.test | 2 +- test-data/unit/check-literal.test | 497 ++++++++++++++++++ test-data/unit/check-newtype.test | 9 +- test-data/unit/check-typeddict.test | 2 +- test-data/unit/lib-stub/typing_extensions.pyi | 2 + test-data/unit/parse-errors.test | 19 - test-data/unit/semanal-errors.test | 18 +- test-data/unit/semenal-literal.test | 27 + 23 files changed, 827 insertions(+), 69 deletions(-) create mode 100644 test-data/unit/check-literal.test create mode 100644 test-data/unit/semenal-literal.test diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 9146c303811b..a0e91b6c4182 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, 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,22 @@ 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, EllipsisExpr): return EllipsisType(expr.line) else: diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 563e5d069828..0dc1522137aa 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,42 @@ 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) + 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..b8ee97906ae4 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]: + return set() + 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_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..a9f755ce84d2 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: + pass + 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/testcheck.py b/mypy/test/testcheck.py index 36c1d7f24465..ab5655c2f9fa 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', ] diff --git a/mypy/test/testsemanal.py b/mypy/test/testsemanal.py index b07f31fbf5d9..321f6d4d459c 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('_') 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..cddf5d02707c 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -16,7 +16,7 @@ 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.nodes import ( @@ -283,6 +283,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 +462,21 @@ 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 change + # this method so it generates and returns an actual LiteralType + # instead. + self.fail("Invalid type. Try using Literal[{}] instead?".format(repr(t.value)), 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 +577,64 @@ 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_simplified_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): + if arg.type_of_any != TypeOfAny.from_error: + self.fail('Parameter {} of Literal[...] is of type Any'.format(idx), ctx) + return None + elif isinstance(arg, RawLiteralType): + # A raw literal. Convert it directly into a literal. + 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): + # TODO: Figure out if just including this in the union is ok + 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 +832,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..c966e2838c3f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -5,7 +5,7 @@ from collections import OrderedDict from typing import ( Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Optional, Union, Iterable, NamedTuple, - Callable, Sequence, Iterator, + Callable, Sequence, Iterator, NewType, ) MYPY = False @@ -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..d81b4761125c 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..8a16b682c58e 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..5871f7d39d47 --- /dev/null +++ b/test-data/unit/check-literal.test @@ -0,0 +1,497 @@ +-- +-- Check to see how we handle raw types, error handling, and other +-- semantic analysis shenanigans +-- + +[case testLiteralInvalidString1] +from typing_extensions import Literal +def f(x: 'A[') -> None: pass # E: Invalid type. Try using Literal['A['] instead? +def g(x: Literal['A[']) -> None: pass +reveal_type(g) # E: Revealed type is 'def (x: Literal['A['])' +[out] + +[case testLiteralInvalidString2] +from typing_extensions import Literal +def f(x: 'A B') -> None: pass # E: Invalid type. Try using Literal['A B'] instead? +def g(x: Literal['A B']) -> None: pass +reveal_type(g) # 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. Try using Literal['A['] instead? + # type: ("A[") -> None + pass + +def g(x): + # type: (Literal["A["]) -> None + pass +[out] + +[case testLiteralParsingPython2] +# flags: --python-version 2.7 +from typing import Optional +from typing_extensions import Literal + +def f(x): # E: Invalid type. Try using Literal['A['] instead? + # 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]] +[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(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(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(y) # E: Revealed type is 'Union[Tuple[Literal[2]], None]' +reveal_type(bar) # E: Revealed type is 'def (x: Tuple[Literal[2]])' +[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 + +a: Literal[4] +b: Literal[0x2a] +c: Literal[-300] + +reveal_type(a) # E: Revealed type is 'Literal[4]' +reveal_type(b) # E: Revealed type is 'Literal[42]' +reveal_type(c) # E: Revealed type is 'Literal[-300]' +[out] + +[case testLiteralBasicIntUsageTypeAlias] +from typing_extensions import Literal + +at = Literal[4] +bt = Literal[0x2a] +ct = Literal[-300] +a: at +b: bt +c: ct + +reveal_type(a) # E: Revealed type is 'Literal[4]' +reveal_type(b) # E: Revealed type is 'Literal[42]' +reveal_type(c) # E: Revealed type is 'Literal[-300]' +[out] + +[case testLiteralBasicBoolUsage] +from typing_extensions import Literal + +a: Literal[True] +b: Literal[False] + +reveal_type(a) # E: Revealed type is 'Literal[True]' +reveal_type(b) # E: Revealed type is 'Literal[False]' +[builtins fixtures/bool.pyi] +[out] + +[case testLiteralBasicBoolUsageTypeAlias] +from typing_extensions import Literal + +at = Literal[True] +bt = Literal[False] +a: at +b: bt + +reveal_type(a) # E: Revealed type is 'Literal[True]' +reveal_type(b) # E: Revealed type is 'Literal[False]' +[builtins fixtures/bool.pyi] +[out] + +[case testLiteralBasicStrUsage] +from typing_extensions import Literal + +a: Literal[""] +b: Literal[r"foo\nbar"] +c: Literal["foo\nbar"] +d: Literal[" foo bar "] +e: Literal[' foo bar '] + +reveal_type(a) # E: Revealed type is 'Literal['']' +reveal_type(b) # E: Revealed type is 'Literal['foo//nbar']' +reveal_type(c) # E: Revealed type is 'Literal['foo/nbar']' +reveal_type(d) # E: Revealed type is 'Literal[' foo bar ']' +reveal_type(e) # E: Revealed type is 'Literal[' foo bar ']' +[out] + +[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: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) + +a: Literal[Any] # E: Parameter 1 of Literal[...] is of type Any +b: Literal[BadAlias] # E: Parameter 1 of Literal[...] is of type Any +[out] + +[case testLiteralDisallowFloats] +from typing_extensions import Literal +a: Literal[3.14] # E: invalid type comment or annotation +b: 3.14 # E: invalid type comment or annotation +[out] + +[case testLiteralDisallowFloatsTypeAlias] +from typing_extensions import Literal +at = Literal[3.14] # E: Invalid type alias \ + # E: The type "Type[Literal]" is not generic and not indexable +bt = 3.14 + +a: at # E: Invalid type "__main__.at" +b: bt # E: Invalid type "__main__.bt" +[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 +[out] + +[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 +[out] + +[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]' +reveal_type(d) # E: Revealed type is 'Literal[1]' +reveal_type(e) # E: Revealed type is '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 + +# Note: the initial plan was to keep this kind of behavior provisional +# and decide whether we want to include this in the PEP at a later date. +# However, implementing this ended up being easier then expected (it required +# only a few trivial tweaks to typeanal.py), so maybe we should just go ahead +# and make it non-provisional now? +# +# TODO: make a decision here + +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-skip] +from typing_extensions import Literal +# TODO: Currently, this test case causes a crash. Fix the crash then re-enable. +# (We currently aren't handling forward references + type aliases very gracefully, +# if at all.) + +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 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] + + +-- +-- 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..704ae4321057 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 subclassable (got "Any") \ + # 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..892dfea18d45 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..204e24f2cd36 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 \ No newline at end of file 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 f3588e36148b..8588353f1740 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. Try using Literal['int['] instead? +[out] + +[case testInvalidStrLiteralSpaces] +def f(x: 'A B'): pass # E: Invalid type. Try using Literal['A B'] instead? +[out] + +[case testInvalidMultilineLiteralType] +def f() -> "A\nB": pass # E: Invalid type. Try using Literal['A/nB'] instead? [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()))) From 4ff4294b882b12180d906c69a8dc9185a88d29ac Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 23 Nov 2018 20:56:54 -0800 Subject: [PATCH 02/10] Correct error strings to what they *should* be --- test-data/unit/check-literal.test | 4 ++-- test-data/unit/semanal-errors.test | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 5871f7d39d47..baa321343aea 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -170,8 +170,8 @@ d: Literal[" foo bar "] e: Literal[' foo bar '] reveal_type(a) # E: Revealed type is 'Literal['']' -reveal_type(b) # E: Revealed type is 'Literal['foo//nbar']' -reveal_type(c) # E: Revealed type is 'Literal['foo/nbar']' +reveal_type(b) # E: Revealed type is 'Literal['foo\\nbar']' +reveal_type(c) # E: Revealed type is 'Literal['foo\nbar']' reveal_type(d) # E: Revealed type is 'Literal[' foo bar ']' reveal_type(e) # E: Revealed type is 'Literal[' foo bar ']' [out] diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index 8588353f1740..87b557ca136a 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -1065,7 +1065,7 @@ def f(x: 'A B'): pass # E: Invalid type. Try using Literal['A B'] instead? [out] [case testInvalidMultilineLiteralType] -def f() -> "A\nB": pass # E: Invalid type. Try using Literal['A/nB'] instead? +def f() -> "A\nB": pass # E: Invalid type. Try using Literal['A\nB'] instead? [out] [case testInconsistentOverload] From 4b62235ecd41936f4e293e097397d12a6cd3e35f Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 23 Nov 2018 21:15:59 -0800 Subject: [PATCH 03/10] Remove lingering NewType import --- mypy/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/types.py b/mypy/types.py index c966e2838c3f..201c40b30045 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -5,7 +5,7 @@ from collections import OrderedDict from typing import ( Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Optional, Union, Iterable, NamedTuple, - Callable, Sequence, Iterator, NewType, + Callable, Sequence, Iterator, ) MYPY = False From 5d436dca41f36082f0c2b4816e97eda6f7d6e3de Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Sat, 24 Nov 2018 20:31:31 -0800 Subject: [PATCH 04/10] Add test case arg that skips path normalization This commit lets us specify we want to skip path normalization on some given test case, side-stepping the path-related errors we were seeing. Specifically, the problem was that mypy attempts to normalize paths by replacing all instances of '\' with '/' so that the output when running the tests on Windows matches the specified errors. This is what we want to most of the time, except for a few tests with Literals containing slashes. I thought about maybe changing the output of mypy in general so it always uses '/' for paths in error outputs, even on Windows: this would mean we would no longer have to do path normalization. However, I wasn't convinced this was the right thing to do: using '\' on Windows technically *is* the right thing to do, and I didn't want to complicate the codebase by forcing us to keep track of when to use os.sep vs '/'. I don't think I'll add too many of these test cases, so I decided to just go with a localized solution instead of changing mypy's error output. --- mypy/test/data.py | 11 ++++++++++- mypy/test/testcheck.py | 3 ++- mypy/test/testcmdline.py | 12 ++++++++---- mypy/test/testmerge.py | 3 ++- mypy/test/testsemanal.py | 7 +++++-- mypy/test/testtransform.py | 3 ++- test-data/unit/check-literal.test | 24 ++++++++++++++++-------- test-data/unit/semanal-errors.test | 5 +++-- 8 files changed, 48 insertions(+), 20 deletions(-) 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 ab5655c2f9fa..642ce9355522 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -178,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..92970c2c2d08 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -85,14 +85,18 @@ 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 321f6d4d459c..e86f645ed87e 100644 --- a/mypy/test/testsemanal.py +++ b/mypy/test/testsemanal.py @@ -88,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, @@ -118,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/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index baa321343aea..ef86918eef32 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -164,18 +164,26 @@ reveal_type(b) # E: Revealed type is 'Literal[False]' from typing_extensions import Literal a: Literal[""] -b: Literal[r"foo\nbar"] -c: Literal["foo\nbar"] -d: Literal[" foo bar "] -e: Literal[' foo bar '] +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\\nbar']' -reveal_type(c) # E: Revealed type is 'Literal['foo\nbar']' -reveal_type(d) # E: Revealed type is 'Literal[' foo bar ']' -reveal_type(e) # E: Revealed type is 'Literal[' foo bar ']' +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] diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index 87b557ca136a..3ad0c69d39f9 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -1065,8 +1065,9 @@ def f(x: 'A B'): pass # E: Invalid type. Try using Literal['A B'] instead? [out] [case testInvalidMultilineLiteralType] -def f() -> "A\nB": pass # E: Invalid type. Try using Literal['A\nB'] instead? -[out] +def f() -> "A\nB": pass +[out skip-path-normalization] +main:1: error: Invalid type. Try using Literal['A\nB'] instead? [case testInconsistentOverload] from typing import overload From 75891b91f008cb543eb1171b7a6520f9a4fa65af Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Sat, 24 Nov 2018 21:22:59 -0800 Subject: [PATCH 05/10] Fix lint error --- mypy/test/testcmdline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index 92970c2c2d08..aeca03684d4d 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -89,7 +89,8 @@ def test_python_cmdline(testcase: DataDrivenTestCase) -> None: # 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 = [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( From 9271810bc5371f077e378b2e4232af1062b265c3 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Sun, 25 Nov 2018 12:18:43 -0800 Subject: [PATCH 06/10] Add a test case for when 'Literal' is imported with a different name --- test-data/unit/check-literal.test | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index ef86918eef32..1fd3829bfaf8 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -100,6 +100,38 @@ reveal_type(y) # E: Revealed type is 'Union[Tuple[Literal[2 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 @@ -474,6 +506,25 @@ foo(d) [builtins fixtures/ops.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. From c206a1b0a23aa63f6a142896355f3ddfb401de12 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 29 Nov 2018 00:30:14 -0800 Subject: [PATCH 07/10] Respond to feedback regarding tests This commit makes a variety of changes addressing feedback given regarding the test cases. It does *not* address the "maybe we can remove RawLiteraType from the Type hierarchy" feedback: I'll take a closer look at that tomorrow. --- mypy/exprtotype.py | 6 +- mypy/fastparse.py | 4 + mypy/semanal_newtype.py | 12 +- mypy/test/testtypes.py | 33 ++- mypy/typeanal.py | 30 ++- test-data/unit/check-functions.test | 4 +- test-data/unit/check-inference.test | 2 +- test-data/unit/check-literal.test | 250 ++++++++++++------ test-data/unit/check-newtype.test | 4 +- test-data/unit/check-typeddict.test | 2 +- test-data/unit/lib-stub/typing_extensions.pyi | 2 +- test-data/unit/semanal-errors.test | 15 +- 12 files changed, 260 insertions(+), 104 deletions(-) diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index a0e91b6c4182..b9974b9c361e 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -1,7 +1,7 @@ """Translate an Expression to a Type value.""" from mypy.nodes import ( - Expression, NameExpr, MemberExpr, IndexExpr, TupleExpr, IntExpr, UnaryExpr, + Expression, NameExpr, MemberExpr, IndexExpr, TupleExpr, IntExpr, FloatExpr, UnaryExpr, ListExpr, StrExpr, BytesExpr, UnicodeExpr, EllipsisExpr, CallExpr, get_member_expr_fullname ) @@ -130,6 +130,10 @@ def expr_to_unanalyzed_type(expr: Expression, _parent: Optional[Expression] = No raise TypeTranslationError() 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 0dc1522137aa..ac1d28e98043 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -1164,6 +1164,10 @@ def visit_Num(self, n: Num) -> Type: 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) 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/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/typeanal.py b/mypy/typeanal.py index cddf5d02707c..a4a99a0e7def 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -472,7 +472,18 @@ def visit_raw_literal_type(self, t: RawLiteralType) -> Type: # make signatures like "foo(x: 20) -> None" legal, we change # this method so it generates and returns an actual LiteralType # instead. - self.fail("Invalid type. Try using Literal[{}] instead?".format(repr(t.value)), t) + 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: floats are not valid types", 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: syntax error in type hint", t) + return AnyType(TypeOfAny.from_error) def visit_literal_type(self, t: LiteralType) -> Type: @@ -589,7 +600,7 @@ def analyze_literal_type(self, t: UnboundType) -> Type: return AnyType(TypeOfAny.from_error) else: output.extend(analyzed_types) - return UnionType.make_simplified_union(output, line=t.line) + 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. @@ -609,11 +620,23 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[L # 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: + # if the user attempts use an explicit Any as a parameter, 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', or 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. if arg.type_of_any != TypeOfAny.from_error: - self.fail('Parameter {} of Literal[...] is of type Any'.format(idx), ctx) + self.fail('Parameter {} of Literal[...] is 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[...] is 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)] @@ -629,7 +652,6 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[L out.extend(union_result) return out elif isinstance(arg, ForwardRef): - # TODO: Figure out if just including this in the union is ok return [arg] else: self.fail('Parameter {} of Literal[...] is invalid'.format(idx), ctx) diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index d81b4761125c..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. Try using Literal[1] instead? +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 @@ -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. Try using Literal[1] instead? +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 8a16b682c58e..4bfcf192897c 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -1974,7 +1974,7 @@ class C(Sequence[T], Generic[T]): pass C[0] = 0 [out] main:4: error: Unsupported target for indexed assignment -main:4: error: Invalid type. Try using Literal[0] instead? +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 index 1fd3829bfaf8..44b7abd4ff4e 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -3,18 +3,17 @@ -- semantic analysis shenanigans -- -[case testLiteralInvalidString1] +[case testLiteralInvalidString] from typing_extensions import Literal -def f(x: 'A[') -> None: pass # E: Invalid type. Try using Literal['A['] instead? -def g(x: Literal['A[']) -> None: pass -reveal_type(g) # E: Revealed type is 'def (x: Literal['A['])' -[out] +def f1(x: 'A[') -> None: pass # E: Invalid type: syntax error in type hint +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['])' -[case testLiteralInvalidString2] -from typing_extensions import Literal -def f(x: 'A B') -> None: pass # E: Invalid type. Try using Literal['A B'] instead? -def g(x: Literal['A B']) -> None: pass -reveal_type(g) # E: Revealed type is 'def (x: Literal['A B'])' +def f2(x: 'A B') -> None: pass # E: Invalid type: syntax error in type hint +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] @@ -25,13 +24,16 @@ def f(x): # E: syntax error in type comment [case testLiteralInvalidTypeComment2] from typing_extensions import Literal -def f(x): # E: Invalid type. Try using Literal['A['] instead? +def f(x): # E: Invalid type: syntax error in type hint # 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] @@ -39,7 +41,7 @@ def g(x): from typing import Optional from typing_extensions import Literal -def f(x): # E: Invalid type. Try using Literal['A['] instead? +def f(x): # E: Invalid type: syntax error in type hint # type: ("A[") -> None pass @@ -47,19 +49,23 @@ def g(x): # type: (Literal["A["]) -> None pass -x = None # type: Optional[1] # E: Invalid type. Try using Literal[1] instead? +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? +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] @@ -69,8 +75,8 @@ reveal_type(bar) # E: Revealed type is 'def (x: Tuple[Literal 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? +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 @@ -78,6 +84,7 @@ 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] @@ -87,8 +94,8 @@ reveal_type(bar) # E: Revealed type is 'def (x: Tuple[Literal 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? +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 @@ -96,6 +103,7 @@ 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] @@ -144,51 +152,42 @@ y: Foo[Foo] # E: Literal[...] must have at least one parameter [case testLiteralBasicIntUsage] from typing_extensions import Literal -a: Literal[4] -b: Literal[0x2a] -c: Literal[-300] - -reveal_type(a) # E: Revealed type is 'Literal[4]' -reveal_type(b) # E: Revealed type is 'Literal[42]' -reveal_type(c) # E: Revealed type is 'Literal[-300]' -[out] +a1: Literal[4] +b1: Literal[0x2a] +c1: Literal[-300] -[case testLiteralBasicIntUsageTypeAlias] -from typing_extensions import Literal +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]' -at = Literal[4] -bt = Literal[0x2a] -ct = Literal[-300] -a: at -b: bt -c: ct +a2t = Literal[4] +b2t = Literal[0x2a] +c2t = Literal[-300] +a2: a2t +b2: b2t +c2: c2t -reveal_type(a) # E: Revealed type is 'Literal[4]' -reveal_type(b) # E: Revealed type is 'Literal[42]' -reveal_type(c) # E: Revealed type is 'Literal[-300]' +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 -a: Literal[True] -b: Literal[False] +a1: Literal[True] +b1: Literal[False] -reveal_type(a) # E: Revealed type is 'Literal[True]' -reveal_type(b) # E: Revealed type is 'Literal[False]' -[builtins fixtures/bool.pyi] -[out] +reveal_type(a1) # E: Revealed type is 'Literal[True]' +reveal_type(b1) # E: Revealed type is 'Literal[False]' -[case testLiteralBasicBoolUsageTypeAlias] -from typing_extensions import Literal - -at = Literal[True] -bt = Literal[False] -a: at -b: bt +a2t = Literal[True] +b2t = Literal[False] +a2: a2t +b2: b2t -reveal_type(a) # E: Revealed type is 'Literal[True]' -reveal_type(b) # E: Revealed type is 'Literal[False]' +reveal_type(a2) # E: Revealed type is 'Literal[True]' +reveal_type(b2) # E: Revealed type is 'Literal[False]' [builtins fixtures/bool.pyi] [out] @@ -229,24 +228,22 @@ from typing_extensions import Literal from missing_module import BadAlias # E: Cannot find module named 'missing_module' \ # N: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) -a: Literal[Any] # E: Parameter 1 of Literal[...] is of type Any -b: Literal[BadAlias] # E: Parameter 1 of Literal[...] is of type Any +a: Literal[Any] # E: Parameter 1 of Literal[...] is of type "Any" +b: Literal[BadAlias] # E: Parameter 1 of Literal[...] is of type "Any" [out] [case testLiteralDisallowFloats] from typing_extensions import Literal -a: Literal[3.14] # E: invalid type comment or annotation -b: 3.14 # E: invalid type comment or annotation -[out] +a1: Literal[3.14] # E: Parameter 1 of Literal[...] is of type "float" +b1: 3.14 # E: Invalid type: floats are not valid types -[case testLiteralDisallowFloatsTypeAlias] -from typing_extensions import Literal -at = Literal[3.14] # E: Invalid type alias \ - # E: The type "Type[Literal]" is not generic and not indexable -bt = 3.14 +a2t = Literal[3.14] # E: Parameter 1 of Literal[...] is of type "float" +b2t = 3.14 + +a2: a2t +reveal_type(a2) # E: Revealed type is 'Any' +b2: b2t # E: Invalid type "__main__.b2t" -a: at # E: Invalid type "__main__.at" -b: bt # E: Invalid type "__main__.bt" [out] [case testLiteralDisallowComplexNumbers] @@ -255,7 +252,6 @@ 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 -[out] [case testLiteralDisallowComplexNumbersTypeAlias] from typing_extensions import Literal @@ -279,7 +275,6 @@ 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 -[out] [case testLiteralDisallowCollections2] from typing_extensions import Literal @@ -325,8 +320,11 @@ 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]' -reveal_type(d) # E: Revealed type is 'Literal[1]' -reveal_type(e) # E: Revealed type is '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] @@ -343,14 +341,6 @@ reveal_type(b) # E: Revealed type is 'Union[Literal[1], Literal[2], Literal[3]] [case testLiteralNestedUsage] # flags: --strict-optional -# Note: the initial plan was to keep this kind of behavior provisional -# and decide whether we want to include this in the PEP at a later date. -# However, implementing this ended up being easier then expected (it required -# only a few trivial tweaks to typeanal.py), so maybe we should just go ahead -# and make it non-provisional now? -# -# TODO: make a decision here - 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']]' @@ -386,11 +376,8 @@ reveal_type(d) # E: Revealed type is 'Literal['Foo']' class Foo: pass [out] -[case testLiteralBiasTowardsAssumingForwardReferenceForTypeAliases-skip] +[case testLiteralBiasTowardsAssumingForwardReferenceForTypeAliases] from typing_extensions import Literal -# TODO: Currently, this test case causes a crash. Fix the crash then re-enable. -# (We currently aren't handling forward references + type aliases very gracefully, -# if at all.) a: "Foo" reveal_type(a) # E: Revealed type is 'Literal[5]' @@ -473,6 +460,57 @@ 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 @@ -506,6 +544,56 @@ 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 diff --git a/test-data/unit/check-newtype.test b/test-data/unit/check-newtype.test index 704ae4321057..c7f43017003a 100644 --- a/test-data/unit/check-newtype.test +++ b/test-data/unit/check-newtype.test @@ -269,8 +269,8 @@ 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 subclassable (got "Any") \ - # E: Invalid type. Try using Literal[3] instead? +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 diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 892dfea18d45..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 type. Try using Literal[1] instead? +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 204e24f2cd36..2b75d3305ce2 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -8,4 +8,4 @@ def runtime(x: _T) -> _T: pass class Final: pass def final(x: _T) -> _T: pass -class Literal: pass \ No newline at end of file +class Literal: pass diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index 3ad0c69d39f9..bdcf9e40e604 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: Invalid type. Try using Literal[1] instead? +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: Invalid type. Try using Literal[1] instead? +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: Invalid type. Try using Literal[2] instead? +c = TypeVar('c', int, 2) # E: Invalid type: try using Literal[2] instead? [out] [case testObsoleteTypevarValuesSyntax] @@ -1057,17 +1057,16 @@ def f(x: 'foo'): pass # E: Name 'foo' is not defined [out] [case testInvalidStrLiteralStrayBrace] -def f(x: 'int['): pass # E: Invalid type. Try using Literal['int['] instead? +def f(x: 'int['): pass # E: Invalid type: syntax error in type hint [out] [case testInvalidStrLiteralSpaces] -def f(x: 'A B'): pass # E: Invalid type. Try using Literal['A B'] instead? +def f(x: 'A B'): pass # E: Invalid type: syntax error in type hint [out] [case testInvalidMultilineLiteralType] -def f() -> "A\nB": pass -[out skip-path-normalization] -main:1: error: Invalid type. Try using Literal['A\nB'] instead? +def f() -> "A\nB": pass # E: Invalid type: syntax error in type hint +[out] [case testInconsistentOverload] from typing import overload From fc34c6a3e02299126cdd2c8980c84cf7d2f0c7af Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 29 Nov 2018 01:48:27 -0800 Subject: [PATCH 08/10] Hoisted by my own petard --- test-data/unit/check-literal.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 44b7abd4ff4e..acf16b803d19 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -226,7 +226,7 @@ reveal_type(a) # E: Revealed type is 'None' from typing import Any from typing_extensions import Literal from missing_module import BadAlias # E: Cannot find module named 'missing_module' \ - # N: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) + # N: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports a: Literal[Any] # E: Parameter 1 of Literal[...] is of type "Any" b: Literal[BadAlias] # E: Parameter 1 of Literal[...] is of type "Any" From 6aca386cf1c9e5745599ad3c0015b5cb9f7f286a Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Sun, 2 Dec 2018 15:35:46 -0800 Subject: [PATCH 09/10] Respond to next wave of code review --- mypy/indirection.py | 2 +- mypy/server/astmerge.py | 2 +- mypy/typeanal.py | 25 ++++++++++++++++--------- test-data/unit/check-literal.test | 21 ++++++++++++--------- test-data/unit/semanal-errors.test | 6 +++--- 5 files changed, 33 insertions(+), 23 deletions(-) diff --git a/mypy/indirection.py b/mypy/indirection.py index b8ee97906ae4..4e3390a65e3c 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -91,7 +91,7 @@ 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]: - return set() + 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/server/astmerge.py b/mypy/server/astmerge.py index a9f755ce84d2..edfdc076e7d7 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -393,7 +393,7 @@ def visit_typeddict_type(self, typ: TypedDictType) -> None: typ.fallback.accept(self) def visit_raw_literal_type(self, t: RawLiteralType) -> None: - pass + assert False, "Unexpected RawLiteralType after semantic analysis phase" def visit_literal_type(self, typ: LiteralType) -> None: typ.fallback.accept(self) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index a4a99a0e7def..646bb4e05b0d 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -18,6 +18,7 @@ CallableArgument, get_type_vars, TypeQuery, union_items, TypeOfAny, ForwardRef, Overloaded, LiteralType, RawLiteralType, ) +from mypy.fastparse import TYPE_COMMENT_SYNTAX_ERROR from mypy.nodes import ( TVAR, MODULE_REF, UNBOUND_IMPORTED, TypeInfo, Context, SymbolTableNode, Var, Expression, @@ -469,7 +470,7 @@ def visit_raw_literal_type(self, t: RawLiteralType) -> Type: # corresponding to 'Literal'. # # Note: if at some point in the distant future, we decide to - # make signatures like "foo(x: 20) -> None" legal, we change + # 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': @@ -477,12 +478,12 @@ def visit_raw_literal_type(self, t: RawLiteralType) -> Type: # 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: floats are not valid types", t) + 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: syntax error in type hint", t) + self.fail('Invalid type: ' + TYPE_COMMENT_SYNTAX_ERROR, t) return AnyType(TypeOfAny.from_error) @@ -621,20 +622,26 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[L # (if we haven't already). if isinstance(arg, AnyType): # Note: We can encounter Literals containing 'Any' under three circumstances: - # if the user attempts use an explicit Any as a parameter, 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', or if there's some other underlying - # problem with the parameter. + # + # 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[...] is of type "Any"'.format(idx), ctx) + 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[...] is of type "float"'.format(idx), ctx) + self.fail( + 'Parameter {} of Literal[...] cannot be of type "float"'.format(idx), + ctx) return None fallback = self.named_type(arg.base_type_name) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index acf16b803d19..961b5a493ee6 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -5,12 +5,12 @@ [case testLiteralInvalidString] from typing_extensions import Literal -def f1(x: 'A[') -> None: pass # E: Invalid type: syntax error in type hint +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 hint +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'])' @@ -24,7 +24,7 @@ def f(x): # E: syntax error in type comment [case testLiteralInvalidTypeComment2] from typing_extensions import Literal -def f(x): # E: Invalid type: syntax error in type hint +def f(x): # E: Invalid type: syntax error in type comment # type: ("A[") -> None pass @@ -41,7 +41,7 @@ reveal_type(g) # E: Revealed type is 'def (x: Literal['A['])' from typing import Optional from typing_extensions import Literal -def f(x): # E: Invalid type: syntax error in type hint +def f(x): # E: Invalid type: syntax error in type comment # type: ("A[") -> None pass @@ -228,16 +228,19 @@ 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[...] is of type "Any" -b: Literal[BadAlias] # E: Parameter 1 of Literal[...] is of type "Any" +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 testLiteralDisallowFloats] from typing_extensions import Literal -a1: Literal[3.14] # E: Parameter 1 of Literal[...] is of type "float" -b1: 3.14 # E: Invalid type: floats are not valid types +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[...] is of type "float" +a2t = Literal[3.14] # E: Parameter 1 of Literal[...] cannot be of type "float" b2t = 3.14 a2: a2t diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index b34272a0b257..5ad34c274a47 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -1057,15 +1057,15 @@ def f(x: 'foo'): pass # E: Name 'foo' is not defined [out] [case testInvalidStrLiteralStrayBrace] -def f(x: 'int['): pass # E: Invalid type: syntax error in type hint +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 hint +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 hint +def f() -> "A\nB": pass # E: Invalid type: syntax error in type comment [out] [case testInconsistentOverload] From 73e058a6fb4d25a720e72bd71ba4c4e17a4f68a6 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Sun, 2 Dec 2018 15:44:42 -0800 Subject: [PATCH 10/10] Add an extra error check --- test-data/unit/check-literal.test | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 961b5a493ee6..2fab35cc10bb 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -235,6 +235,21 @@ 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"