From 9fa1aa893e53bf3c3f65c2c5520a0a19b294c164 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 11 Apr 2019 20:59:17 -0700 Subject: [PATCH 1/4] Add basic support for enum literals This pull request adds in basic support for enum literals. Basically, you can construct them and use them as types, but not much else. (I originally said I was going to wait until the new semantic analyzer work was finished, but I'm bored.) Some notes: 1. I plan on submitting incremental and fine-grained-mode tests in a future PR. 2. I added two copies of each test -- one using the old semantic analyzer and one using the new one. Let me know if we prefer not doing this and if I should pick one over the other. 3. I wanted to add support for aliases to enums, but ran into some difficulty doing so. See https://github.com/python/mypy/issues/6667 for some analysis on the root cause. --- mypy/newsemanal/semanal.py | 4 +- mypy/newsemanal/typeanal.py | 38 +++- mypy/semanal.py | 6 +- mypy/typeanal.py | 47 ++++- mypy/types.py | 13 +- test-data/unit/check-literal.test | 303 ++++++++++++++++++++++++++++++ 6 files changed, 392 insertions(+), 19 deletions(-) diff --git a/mypy/newsemanal/semanal.py b/mypy/newsemanal/semanal.py index 87e8b147e311..313bf6668ecd 100644 --- a/mypy/newsemanal/semanal.py +++ b/mypy/newsemanal/semanal.py @@ -3989,10 +3989,10 @@ def lookup_fully_qualified(self, name: str) -> SymbolTableNode: module namespace is ignored. """ parts = name.split('.') - n = self.modules[parts[0]] + n = self.modules[parts[0]] # type: Union[MypyFile, TypeInfo] for i in range(1, len(parts) - 1): next_sym = n.names[parts[i]] - assert isinstance(next_sym.node, MypyFile) + assert isinstance(next_sym.node, (MypyFile, TypeInfo)) n = next_sym.node return n.names[parts[-1]] diff --git a/mypy/newsemanal/typeanal.py b/mypy/newsemanal/typeanal.py index 2a7cd1a4a863..6b3cfd98a7d5 100644 --- a/mypy/newsemanal/typeanal.py +++ b/mypy/newsemanal/typeanal.py @@ -151,15 +151,15 @@ def __init__(self, # Names of type aliases encountered while analysing a type will be collected here. self.aliases_used = set() # type: Set[str] - def visit_unbound_type(self, t: UnboundType) -> Type: - typ = self.visit_unbound_type_nonoptional(t) + def visit_unbound_type(self, t: UnboundType, defining_literal: bool = False) -> Type: + typ = self.visit_unbound_type_nonoptional(t, defining_literal) if t.optional: # We don't need to worry about double-wrapping Optionals or # wrapping Anys: Union simplification will take care of that. return make_optional_type(typ) return typ - def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: + def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) -> Type: sym = self.lookup_qualified(t.name, t, suppress_errors=self.third_pass) if sym is not None: node = sym.node @@ -217,7 +217,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: elif isinstance(node, TypeInfo): return self.analyze_type_with_type_info(node, t.args, t) else: - return self.analyze_unbound_type_without_type_info(t, sym) + return self.analyze_unbound_type_without_type_info(t, sym, defining_literal) else: # sym is None if self.third_pass: self.fail('Invalid type "{}"'.format(t.name), t) @@ -348,7 +348,8 @@ def analyze_type_with_type_info(self, info: TypeInfo, args: List[Type], ctx: Con fallback=instance) return instance - def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTableNode) -> Type: + def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTableNode, + defining_literal: bool) -> Type: """Figure out what an unbound type that doesn't refer to a TypeInfo node means. This is something unusual. We try our best to find out what it is. @@ -373,6 +374,27 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl if self.allow_unbound_tvars and unbound_tvar and not self.third_pass: return t + # Option 3: + # Enum value. Note: Enum values are not real types, so we return + # RawExpressionType only when this function is being called by + # one of the Literal[...] handlers -- when `defining_literal` is True. + # + # It's unsafe to return RawExpressionType in any other case, since + # the type would leak out of the semantic analysis phase. + if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum: + short_name = sym.node.name() + base_enum_name = sym.node.info.fullname() + if not defining_literal: + msg = "Invalid type: try using Literal[{}] instead?".format(name) + self.fail(msg, t) + return AnyType(TypeOfAny.from_error) + return RawExpressionType( + literal_value=short_name, + base_type_name=base_enum_name, + line=t.line, + column=t.column, + ) + # None of the above options worked, we give up. self.fail('Invalid type "{}"'.format(name), t) @@ -631,7 +653,11 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[L # 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) + self.nesting_level += 1 + try: + arg = self.visit_unbound_type(arg, defining_literal=True) + finally: + self.nesting_level -= 1 # Literal[...] cannot contain Any. Give up and add an error message # (if we haven't already). diff --git a/mypy/semanal.py b/mypy/semanal.py index 5826ad0a3f45..63c1da06103c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -73,7 +73,7 @@ from mypy.typeanal import ( TypeAnalyser, analyze_type_alias, no_subscript_builtin_alias, TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type, - check_for_explicit_any + check_for_explicit_any, expand_type_alias ) from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.sametypes import is_same_type @@ -3590,10 +3590,10 @@ def lookup_fully_qualified(self, name: str) -> SymbolTableNode: module namespace is ignored. """ parts = name.split('.') - n = self.modules[parts[0]] + n = self.modules[parts[0]] # type: Union[MypyFile, TypeInfo] for i in range(1, len(parts) - 1): next_sym = n.names[parts[i]] - assert isinstance(next_sym.node, MypyFile) + assert isinstance(next_sym.node, (MypyFile, TypeInfo)) n = next_sym.node return n.names[parts[-1]] diff --git a/mypy/typeanal.py b/mypy/typeanal.py index a3d788163e2d..58a781a029d5 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -187,15 +187,15 @@ def __init__(self, # Names of type aliases encountered while analysing a type will be collected here. self.aliases_used = set() # type: Set[str] - def visit_unbound_type(self, t: UnboundType) -> Type: - typ = self.visit_unbound_type_nonoptional(t) + def visit_unbound_type(self, t: UnboundType, defining_literal: bool = False) -> Type: + typ = self.visit_unbound_type_nonoptional(t, defining_literal) if t.optional: # We don't need to worry about double-wrapping Optionals or # wrapping Anys: Union simplification will take care of that. return make_optional_type(typ) return typ - def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: + def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) -> Type: sym = self.lookup(t.name, t, suppress_errors=self.third_pass) if '.' in t.name: # Handle indirect references to imported names. @@ -245,11 +245,14 @@ def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: all_vars = node.alias_tvars target = node.target an_args = self.anal_array(t.args) - return expand_type_alias(target, all_vars, an_args, self.fail, node.no_args, t) + out = expand_type_alias(target, all_vars, an_args, self.fail, node.no_args, t) + if 'RED' in str(t): + print('...') + return out elif isinstance(node, TypeInfo): return self.analyze_unbound_type_with_type_info(t, node) else: - return self.analyze_unbound_type_without_type_info(t, sym) + return self.analyze_unbound_type_without_type_info(t, sym, defining_literal) else: # sym is None if self.third_pass: self.fail('Invalid type "{}"'.format(t.name), t) @@ -368,7 +371,8 @@ def analyze_unbound_type_with_type_info(self, t: UnboundType, info: TypeInfo) -> fallback=instance) return instance - def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTableNode) -> Type: + def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTableNode, + defining_literal: bool) -> Type: """Figure out what an unbound type that doesn't refer to a TypeInfo node means. This is something unusual. We try our best to find out what it is. @@ -377,6 +381,7 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl if name is None: assert sym.node is not None name = sym.node.name() + # Option 1: # Something with an Any type -- make it an alias for Any in a type # context. This is slightly problematic as it allows using the type 'Any' @@ -385,6 +390,7 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl if isinstance(sym.node, Var) and isinstance(sym.node.type, AnyType): return AnyType(TypeOfAny.from_unimported_type, missing_import_name=sym.node.type.missing_import_name) + # Option 2: # Unbound type variable. Currently these may be still valid, # for example when defining a generic type alias. @@ -392,7 +398,29 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl (not self.tvar_scope or self.tvar_scope.get_binding(sym) is None)) if self.allow_unbound_tvars and unbound_tvar and not self.third_pass: return t + # Option 3: + # Enum value. Note: Enum values are not real types, so we return + # RawExpressionType only when this function is being called by + # one of the Literal[...] handlers -- when `defining_literal` is True. + # + # It's unsafe to return RawExpressionType in any other case, since + # the type would leak out of the semantic analysis phase. + if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum: + short_name = sym.node.name() + base_enum_name = sym.node.info.fullname() + if not defining_literal: + msg = "Invalid type: try using Literal[{}] instead?".format(name) + self.fail(msg, t) + return AnyType(TypeOfAny.from_error) + return RawExpressionType( + literal_value=short_name, + base_type_name=base_enum_name, + line=t.line, + column=t.column, + ) + + # Option 4: # If it is not something clearly bad (like a known function, variable, # type variable, or module), and it is still not too late, we try deferring # this type using a forward reference wrapper. It will be revisited in @@ -410,6 +438,7 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl self.fail('Unsupported forward reference to "{}"'.format(t.name), t) return AnyType(TypeOfAny.from_error) return ForwardRef(t) + # None of the above options worked, we give up. self.fail('Invalid type "{}"'.format(name), t) if self.third_pass and isinstance(sym.node, TypeVarExpr): @@ -657,7 +686,11 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[L # 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) + self.nesting_level += 1 + try: + arg = self.visit_unbound_type(arg, defining_literal=True) + finally: + self.nesting_level -= 1 # Literal[...] cannot contain Any. Give up and add an error message # (if we haven't already). diff --git a/mypy/types.py b/mypy/types.py index 87a56fc68f51..64955ee73d74 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1443,6 +1443,9 @@ class LiteralType(Type): For example, 'Literal[42]' is represented as 'LiteralType(value=42, fallback=instance_of_int)' + + As another example, `Literal[Color.RED]` (where Color is an enum) is + represented as `LiteralType(value="RED", fallback=instance_of_color)'. """ __slots__ = ('value', 'fallback') @@ -1464,15 +1467,23 @@ def __eq__(self, other: object) -> bool: else: return NotImplemented + def is_fallback_enum(self) -> bool: + return self.fallback.type.is_enum + def value_repr(self) -> str: """Returns the string representation of the underlying type. This function is almost equivalent to running `repr(self.value)`, except it includes some additional logic to correctly handle cases - where the value is a string, byte string, or a unicode string. + where the value is a string, byte string, a unicode string, or an enum. """ raw = repr(self.value) fallback_name = self.fallback.type.fullname() + + # If this is backed by an enum, + if self.is_fallback_enum(): + return '{}.{}'.format(fallback_name, self.value) + if fallback_name == 'builtins.bytes': # Note: 'builtins.bytes' only appears in Python 3, so we want to # explicitly prefix with a "b" diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index cc9e3cd38b5d..e349edc3ee6d 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2643,3 +2643,306 @@ c_wrap: Literal[4, c] # E: Invalid type "__main__.c" \ d_wrap: Literal[4, d] # E: Invalid type "__main__.d" \ # E: Parameter 2 of Literal[...] is invalid [out] + +-- +-- Tests for Literals and enums +-- + +[case testLiteralWithEnumsBasic] +from typing_extensions import Literal +from enum import Enum + +class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + def func(self) -> int: pass + +r: Literal[Color.RED] +g: Literal[Color.GREEN] +b: Literal[Color.BLUE] +bad: Literal[Color] # E: Parameter 1 of Literal[...] is invalid + +def expects_color(x: Color) -> None: pass +def expects_red(x: Literal[Color.RED]) -> None: pass +def bad_func(x: Color.RED) -> None: pass # E: Invalid type: try using Literal[__main__.Color.RED] instead? + +expects_color(r) +expects_color(g) +expects_color(b) +expects_red(r) +expects_red(g) # E: Argument 1 to "expects_red" has incompatible type "Literal[__main__.Color.GREEN]"; expected "Literal[__main__.Color.RED]" +expects_red(b) # E: Argument 1 to "expects_red" has incompatible type "Literal[__main__.Color.BLUE]"; expected "Literal[__main__.Color.RED]" + +reveal_type(expects_red) # E: Revealed type is 'def (x: Literal[__main__.Color.RED])' +reveal_type(r) # E: Revealed type is 'Literal[__main__.Color.RED]' +reveal_type(r.func()) # E: Revealed type is 'builtins.int' +[out] + +[case testLiteralWithEnumsBasicNewSemanticAnalyzer] +# flags: --new-semantic-analyzer +from typing_extensions import Literal +from enum import Enum + +class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + def func(self) -> int: pass + +r: Literal[Color.RED] +g: Literal[Color.GREEN] +b: Literal[Color.BLUE] +bad: Literal[Color] # E: Parameter 1 of Literal[...] is invalid + +def expects_color(x: Color) -> None: pass +def expects_red(x: Literal[Color.RED]) -> None: pass +def bad_func(x: Color.RED) -> None: pass # E: Invalid type: try using Literal[__main__.Color.RED] instead? + +expects_color(r) +expects_color(g) +expects_color(b) +expects_red(r) +expects_red(g) # E: Argument 1 to "expects_red" has incompatible type "Literal[__main__.Color.GREEN]"; expected "Literal[__main__.Color.RED]" +expects_red(b) # E: Argument 1 to "expects_red" has incompatible type "Literal[__main__.Color.BLUE]"; expected "Literal[__main__.Color.RED]" + +reveal_type(expects_red) # E: Revealed type is 'def (x: Literal[__main__.Color.RED])' +reveal_type(r) # E: Revealed type is 'Literal[__main__.Color.RED]' +reveal_type(r.func()) # E: Revealed type is 'builtins.int' +[out] + +[case testLiteralWithEnumsDefinedInClass] +from typing_extensions import Literal +from enum import Enum + +class Wrapper: + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + +def foo(x: Literal[Wrapper.Color.RED]) -> None: pass + +r: Literal[Wrapper.Color.RED] +g: Literal[Wrapper.Color.GREEN] + +foo(r) +foo(g) # E: Argument 1 to "foo" has incompatible type "Literal[__main__.Wrapper.Color.GREEN]"; expected "Literal[__main__.Wrapper.Color.RED]" + +reveal_type(foo) # E: Revealed type is 'def (x: Literal[__main__.Wrapper.Color.RED])' +reveal_type(r) # E: Revealed type is 'Literal[__main__.Wrapper.Color.RED]' +[out] + +[case testLiteralWithEnumsDefinedInClassNewSemanticAnalyzer] +# flags --new-semantic-analyzer +from typing_extensions import Literal +from enum import Enum + +class Wrapper: + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + +def foo(x: Literal[Wrapper.Color.RED]) -> None: pass + +r: Literal[Wrapper.Color.RED] +g: Literal[Wrapper.Color.GREEN] + +foo(r) +foo(g) # E: Argument 1 to "foo" has incompatible type "Literal[__main__.Wrapper.Color.GREEN]"; expected "Literal[__main__.Wrapper.Color.RED]" + +reveal_type(foo) # E: Revealed type is 'def (x: Literal[__main__.Wrapper.Color.RED])' +reveal_type(r) # E: Revealed type is 'Literal[__main__.Wrapper.Color.RED]' +[out] + +[case testLiteralWithEnumsSimilarDefinitions] +from typing_extensions import Literal +import mod_a +import mod_b + +def f(x: Literal[mod_a.Test.FOO]) -> None: pass + +a: Literal[mod_a.Test.FOO] +b: Literal[mod_a.Test2.FOO] +c: Literal[mod_b.Test.FOO] + +f(a) +f(b) # E: Argument 1 to "f" has incompatible type "Literal[mod_a.Test2.FOO]"; expected "Literal[mod_a.Test.FOO]" +f(c) # E: Argument 1 to "f" has incompatible type "Literal[mod_b.Test.FOO]"; expected "Literal[mod_a.Test.FOO]" + +[file mod_a.py] +from enum import Enum + +class Test(Enum): + FOO = 1 + BAR = 2 + +class Test2(Enum): + FOO = 1 + BAR = 2 + +[file mod_b.py] +from enum import Enum + +class Test(Enum): + FOO = 1 + BAR = 2 +[out] + +[case testLiteralWithEnumsSimilarDefinitionsNewSemanticAnalyzer] +# flags: --new-semantic-analyzer +from typing_extensions import Literal +import mod_a +import mod_b + +def f(x: Literal[mod_a.Test.FOO]) -> None: pass + +a: Literal[mod_a.Test.FOO] +b: Literal[mod_a.Test2.FOO] +c: Literal[mod_b.Test.FOO] + +f(a) +f(b) # E: Argument 1 to "f" has incompatible type "Literal[mod_a.Test2.FOO]"; expected "Literal[mod_a.Test.FOO]" +f(c) # E: Argument 1 to "f" has incompatible type "Literal[mod_b.Test.FOO]"; expected "Literal[mod_a.Test.FOO]" + +[file mod_a.py] +from enum import Enum + +class Test(Enum): + FOO = 1 + BAR = 2 + +class Test2(Enum): + FOO = 1 + BAR = 2 + +[file mod_b.py] +from enum import Enum + +class Test(Enum): + FOO = 1 + BAR = 2 +[out] + +[case testLiteralWithEnumsDeclaredUsingCallSyntax] +from typing_extensions import Literal +from enum import Enum + +A = Enum('A', 'FOO BAR') +B = Enum('B', ['FOO', 'BAR']) +C = Enum('C', [('FOO', 1), ('BAR', 2)]) +D = Enum('D', {'FOO': 1, 'BAR': 2}) + +a: Literal[A.FOO] +b: Literal[B.FOO] +c: Literal[C.FOO] +d: Literal[D.FOO] + +reveal_type(a) # E: Revealed type is 'Literal[__main__.A.FOO]' +reveal_type(b) # E: Revealed type is 'Literal[__main__.B.FOO]' +reveal_type(c) # E: Revealed type is 'Literal[__main__.C.FOO]' +reveal_type(d) # E: Revealed type is 'Literal[__main__.D.FOO]' +[builtins fixtures/dict.pyi] +[out] + +[case testLiteralWithEnumsDeclaredUsingCallSyntaxNewSemanticAnalyzer] +# flags: --new-semantic-analyzer +from typing_extensions import Literal +from enum import Enum + +A = Enum('A', 'FOO BAR') +B = Enum('B', ['FOO', 'BAR']) +C = Enum('C', [('FOO', 1), ('BAR', 2)]) +D = Enum('D', {'FOO': 1, 'BAR': 2}) + +a: Literal[A.FOO] +b: Literal[B.FOO] +c: Literal[C.FOO] +d: Literal[D.FOO] + +reveal_type(a) # E: Revealed type is 'Literal[__main__.A.FOO]' +reveal_type(b) # E: Revealed type is 'Literal[__main__.B.FOO]' +reveal_type(c) # E: Revealed type is 'Literal[__main__.C.FOO]' +reveal_type(d) # E: Revealed type is 'Literal[__main__.D.FOO]' +[builtins fixtures/dict.pyi] +[out] + +[case testLiteralWithEnumsDerivedEnums] +from typing_extensions import Literal +from enum import Enum, IntEnum, IntFlag, Flag + +def expects_int(x: int) -> None: pass + +class A(Enum): + FOO = 1 + +class B(IntEnum): + FOO = 1 + +class C(IntFlag): + FOO = 1 + +class D(Flag): + FOO = 1 + +a: Literal[A.FOO] +b: Literal[B.FOO] +c: Literal[C.FOO] +d: Literal[D.FOO] + +expects_int(a) # E: Argument 1 to "expects_int" has incompatible type "Literal[__main__.A.FOO]"; expected "int" +expects_int(b) +expects_int(c) +expects_int(d) # E: Argument 1 to "expects_int" has incompatible type "Literal[__main__.D.FOO]"; expected "int" +[out] + +[case testLiteralWithEnumsDerivedEnumsNewSemanticAnalyzer] +# flags: --new-semantic-analyzer +from typing_extensions import Literal +from enum import Enum, IntEnum, IntFlag, Flag + +def expects_int(x: int) -> None: pass + +class A(Enum): + FOO = 1 + +class B(IntEnum): + FOO = 1 + +class C(IntFlag): + FOO = 1 + +class D(Flag): + FOO = 1 + +a: Literal[A.FOO] +b: Literal[B.FOO] +c: Literal[C.FOO] +d: Literal[D.FOO] + +expects_int(a) # E: Argument 1 to "expects_int" has incompatible type "Literal[__main__.A.FOO]"; expected "int" +expects_int(b) +expects_int(c) +expects_int(d) # E: Argument 1 to "expects_int" has incompatible type "Literal[__main__.D.FOO]"; expected "int" +[out] + +[case testLiteralWithEnumsAliases-skip] +# TODO: Fix this test once https://github.com/python/mypy/issues/6667 is resolved. +# +# The linked issue is about nested classes, but the same root bug prevents aliases +# to enums from working. +from typing_extensions import Literal +from enum import Enum + +class Test(Enum): + FOO = 1 + BAR = 2 + +Alias = Test + +x: Literal[Alias.FOO] +reveal_type(x) # E: Revealed type is 'Literal[__main__.Test.FOO]' +[out] \ No newline at end of file From da79b0179ddd0bb01479e98ec9ff2a90b82ad33a Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 12 Apr 2019 14:17:24 -0700 Subject: [PATCH 2/4] Respond to code review --- mypy/message_registry.py | 5 + mypy/messages.py | 6 +- mypy/newsemanal/typeanal.py | 12 ++- mypy/semanal.py | 2 +- mypy/typeanal.py | 17 ++-- mypy/types.py | 4 +- test-data/unit/check-literal.test | 159 ++---------------------------- 7 files changed, 36 insertions(+), 169 deletions(-) diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 7279ea20dca5..9cfdfef940c9 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -11,6 +11,11 @@ from typing_extensions import Final +# Invalid types + +INVALID_TYPE_RAW_ENUM_VALUE = "Invalid type: try using Literal[{}.{}] instead?" # type: Final + + # Type checker error message constants -- NO_RETURN_VALUE_EXPECTED = 'No return value expected' # type: Final diff --git a/mypy/messages.py b/mypy/messages.py index 58ce7e2a042b..dd65d48c6a3b 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -238,7 +238,11 @@ def format_bare(self, typ: Type, verbosity: int = 0) -> str: s = 'TypedDict({{{}}})'.format(', '.join(items)) return s elif isinstance(typ, LiteralType): - return str(typ) + if typ.is_enum_literal(): + underlying_type = self.format_bare(typ.fallback, verbosity=verbosity) + return 'Literal[{}.{}]'.format(underlying_type, typ.value) + else: + 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/newsemanal/typeanal.py b/mypy/newsemanal/typeanal.py index 6b3cfd98a7d5..99bcb8007561 100644 --- a/mypy/newsemanal/typeanal.py +++ b/mypy/newsemanal/typeanal.py @@ -382,15 +382,17 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl # It's unsafe to return RawExpressionType in any other case, since # the type would leak out of the semantic analysis phase. if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum: - short_name = sym.node.name() - base_enum_name = sym.node.info.fullname() + value = sym.node.name() + base_enum_short_name = sym.node.info.name() + base_enum_qualified_name = sym.node.info.fullname() if not defining_literal: - msg = "Invalid type: try using Literal[{}] instead?".format(name) + msg = message_registry.INVALID_TYPE_RAW_ENUM_VALUE.format( + base_enum_short_name, value) self.fail(msg, t) return AnyType(TypeOfAny.from_error) return RawExpressionType( - literal_value=short_name, - base_type_name=base_enum_name, + literal_value=value, + base_type_name=base_enum_qualified_name, line=t.line, column=t.column, ) diff --git a/mypy/semanal.py b/mypy/semanal.py index 63c1da06103c..49ee106a4507 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -73,7 +73,7 @@ from mypy.typeanal import ( TypeAnalyser, analyze_type_alias, no_subscript_builtin_alias, TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type, - check_for_explicit_any, expand_type_alias + check_for_explicit_any ) from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.sametypes import is_same_type diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 58a781a029d5..9c9610f88755 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -245,10 +245,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) all_vars = node.alias_tvars target = node.target an_args = self.anal_array(t.args) - out = expand_type_alias(target, all_vars, an_args, self.fail, node.no_args, t) - if 'RED' in str(t): - print('...') - return out + return expand_type_alias(target, all_vars, an_args, self.fail, node.no_args, t) elif isinstance(node, TypeInfo): return self.analyze_unbound_type_with_type_info(t, node) else: @@ -407,15 +404,17 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl # It's unsafe to return RawExpressionType in any other case, since # the type would leak out of the semantic analysis phase. if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum: - short_name = sym.node.name() - base_enum_name = sym.node.info.fullname() + value = sym.node.name() + base_enum_short_name = sym.node.info.name() + base_enum_qualified_name = sym.node.info.fullname() if not defining_literal: - msg = "Invalid type: try using Literal[{}] instead?".format(name) + msg = message_registry.INVALID_TYPE_RAW_ENUM_VALUE.format( + base_enum_short_name, value) self.fail(msg, t) return AnyType(TypeOfAny.from_error) return RawExpressionType( - literal_value=short_name, - base_type_name=base_enum_name, + literal_value=value, + base_type_name=base_enum_qualified_name, line=t.line, column=t.column, ) diff --git a/mypy/types.py b/mypy/types.py index 64955ee73d74..d5e805365df5 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1467,7 +1467,7 @@ def __eq__(self, other: object) -> bool: else: return NotImplemented - def is_fallback_enum(self) -> bool: + def is_enum_literal(self) -> bool: return self.fallback.type.is_enum def value_repr(self) -> str: @@ -1481,7 +1481,7 @@ def value_repr(self) -> str: fallback_name = self.fallback.type.fullname() # If this is backed by an enum, - if self.is_fallback_enum(): + if self.is_enum_literal(): return '{}.{}'.format(fallback_name, self.value) if fallback_name == 'builtins.bytes': diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index e349edc3ee6d..35344b483d0b 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2666,47 +2666,14 @@ bad: Literal[Color] # E: Parameter 1 of Literal[...] is invalid def expects_color(x: Color) -> None: pass def expects_red(x: Literal[Color.RED]) -> None: pass -def bad_func(x: Color.RED) -> None: pass # E: Invalid type: try using Literal[__main__.Color.RED] instead? +def bad_func(x: Color.RED) -> None: pass # E: Invalid type: try using Literal[Color.RED] instead? expects_color(r) expects_color(g) expects_color(b) expects_red(r) -expects_red(g) # E: Argument 1 to "expects_red" has incompatible type "Literal[__main__.Color.GREEN]"; expected "Literal[__main__.Color.RED]" -expects_red(b) # E: Argument 1 to "expects_red" has incompatible type "Literal[__main__.Color.BLUE]"; expected "Literal[__main__.Color.RED]" - -reveal_type(expects_red) # E: Revealed type is 'def (x: Literal[__main__.Color.RED])' -reveal_type(r) # E: Revealed type is 'Literal[__main__.Color.RED]' -reveal_type(r.func()) # E: Revealed type is 'builtins.int' -[out] - -[case testLiteralWithEnumsBasicNewSemanticAnalyzer] -# flags: --new-semantic-analyzer -from typing_extensions import Literal -from enum import Enum - -class Color(Enum): - RED = 1 - GREEN = 2 - BLUE = 3 - - def func(self) -> int: pass - -r: Literal[Color.RED] -g: Literal[Color.GREEN] -b: Literal[Color.BLUE] -bad: Literal[Color] # E: Parameter 1 of Literal[...] is invalid - -def expects_color(x: Color) -> None: pass -def expects_red(x: Literal[Color.RED]) -> None: pass -def bad_func(x: Color.RED) -> None: pass # E: Invalid type: try using Literal[__main__.Color.RED] instead? - -expects_color(r) -expects_color(g) -expects_color(b) -expects_red(r) -expects_red(g) # E: Argument 1 to "expects_red" has incompatible type "Literal[__main__.Color.GREEN]"; expected "Literal[__main__.Color.RED]" -expects_red(b) # E: Argument 1 to "expects_red" has incompatible type "Literal[__main__.Color.BLUE]"; expected "Literal[__main__.Color.RED]" +expects_red(g) # E: Argument 1 to "expects_red" has incompatible type "Literal[Color.GREEN]"; expected "Literal[Color.RED]" +expects_red(b) # E: Argument 1 to "expects_red" has incompatible type "Literal[Color.BLUE]"; expected "Literal[Color.RED]" reveal_type(expects_red) # E: Revealed type is 'def (x: Literal[__main__.Color.RED])' reveal_type(r) # E: Revealed type is 'Literal[__main__.Color.RED]' @@ -2729,30 +2696,7 @@ r: Literal[Wrapper.Color.RED] g: Literal[Wrapper.Color.GREEN] foo(r) -foo(g) # E: Argument 1 to "foo" has incompatible type "Literal[__main__.Wrapper.Color.GREEN]"; expected "Literal[__main__.Wrapper.Color.RED]" - -reveal_type(foo) # E: Revealed type is 'def (x: Literal[__main__.Wrapper.Color.RED])' -reveal_type(r) # E: Revealed type is 'Literal[__main__.Wrapper.Color.RED]' -[out] - -[case testLiteralWithEnumsDefinedInClassNewSemanticAnalyzer] -# flags --new-semantic-analyzer -from typing_extensions import Literal -from enum import Enum - -class Wrapper: - class Color(Enum): - RED = 1 - GREEN = 2 - BLUE = 3 - -def foo(x: Literal[Wrapper.Color.RED]) -> None: pass - -r: Literal[Wrapper.Color.RED] -g: Literal[Wrapper.Color.GREEN] - -foo(r) -foo(g) # E: Argument 1 to "foo" has incompatible type "Literal[__main__.Wrapper.Color.GREEN]"; expected "Literal[__main__.Wrapper.Color.RED]" +foo(g) # E: Argument 1 to "foo" has incompatible type "Literal[Color.GREEN]"; expected "Literal[Color.RED]" reveal_type(foo) # E: Revealed type is 'def (x: Literal[__main__.Wrapper.Color.RED])' reveal_type(r) # E: Revealed type is 'Literal[__main__.Wrapper.Color.RED]' @@ -2770,42 +2714,7 @@ b: Literal[mod_a.Test2.FOO] c: Literal[mod_b.Test.FOO] f(a) -f(b) # E: Argument 1 to "f" has incompatible type "Literal[mod_a.Test2.FOO]"; expected "Literal[mod_a.Test.FOO]" -f(c) # E: Argument 1 to "f" has incompatible type "Literal[mod_b.Test.FOO]"; expected "Literal[mod_a.Test.FOO]" - -[file mod_a.py] -from enum import Enum - -class Test(Enum): - FOO = 1 - BAR = 2 - -class Test2(Enum): - FOO = 1 - BAR = 2 - -[file mod_b.py] -from enum import Enum - -class Test(Enum): - FOO = 1 - BAR = 2 -[out] - -[case testLiteralWithEnumsSimilarDefinitionsNewSemanticAnalyzer] -# flags: --new-semantic-analyzer -from typing_extensions import Literal -import mod_a -import mod_b - -def f(x: Literal[mod_a.Test.FOO]) -> None: pass - -a: Literal[mod_a.Test.FOO] -b: Literal[mod_a.Test2.FOO] -c: Literal[mod_b.Test.FOO] - -f(a) -f(b) # E: Argument 1 to "f" has incompatible type "Literal[mod_a.Test2.FOO]"; expected "Literal[mod_a.Test.FOO]" +f(b) # E: Argument 1 to "f" has incompatible type "Literal[Test2.FOO]"; expected "Literal[Test.FOO]" f(c) # E: Argument 1 to "f" has incompatible type "Literal[mod_b.Test.FOO]"; expected "Literal[mod_a.Test.FOO]" [file mod_a.py] @@ -2848,28 +2757,6 @@ reveal_type(d) # E: Revealed type is 'Literal[__main__.D.FOO]' [builtins fixtures/dict.pyi] [out] -[case testLiteralWithEnumsDeclaredUsingCallSyntaxNewSemanticAnalyzer] -# flags: --new-semantic-analyzer -from typing_extensions import Literal -from enum import Enum - -A = Enum('A', 'FOO BAR') -B = Enum('B', ['FOO', 'BAR']) -C = Enum('C', [('FOO', 1), ('BAR', 2)]) -D = Enum('D', {'FOO': 1, 'BAR': 2}) - -a: Literal[A.FOO] -b: Literal[B.FOO] -c: Literal[C.FOO] -d: Literal[D.FOO] - -reveal_type(a) # E: Revealed type is 'Literal[__main__.A.FOO]' -reveal_type(b) # E: Revealed type is 'Literal[__main__.B.FOO]' -reveal_type(c) # E: Revealed type is 'Literal[__main__.C.FOO]' -reveal_type(d) # E: Revealed type is 'Literal[__main__.D.FOO]' -[builtins fixtures/dict.pyi] -[out] - [case testLiteralWithEnumsDerivedEnums] from typing_extensions import Literal from enum import Enum, IntEnum, IntFlag, Flag @@ -2893,40 +2780,10 @@ b: Literal[B.FOO] c: Literal[C.FOO] d: Literal[D.FOO] -expects_int(a) # E: Argument 1 to "expects_int" has incompatible type "Literal[__main__.A.FOO]"; expected "int" +expects_int(a) # E: Argument 1 to "expects_int" has incompatible type "Literal[A.FOO]"; expected "int" expects_int(b) expects_int(c) -expects_int(d) # E: Argument 1 to "expects_int" has incompatible type "Literal[__main__.D.FOO]"; expected "int" -[out] - -[case testLiteralWithEnumsDerivedEnumsNewSemanticAnalyzer] -# flags: --new-semantic-analyzer -from typing_extensions import Literal -from enum import Enum, IntEnum, IntFlag, Flag - -def expects_int(x: int) -> None: pass - -class A(Enum): - FOO = 1 - -class B(IntEnum): - FOO = 1 - -class C(IntFlag): - FOO = 1 - -class D(Flag): - FOO = 1 - -a: Literal[A.FOO] -b: Literal[B.FOO] -c: Literal[C.FOO] -d: Literal[D.FOO] - -expects_int(a) # E: Argument 1 to "expects_int" has incompatible type "Literal[__main__.A.FOO]"; expected "int" -expects_int(b) -expects_int(c) -expects_int(d) # E: Argument 1 to "expects_int" has incompatible type "Literal[__main__.D.FOO]"; expected "int" +expects_int(d) # E: Argument 1 to "expects_int" has incompatible type "Literal[D.FOO]"; expected "int" [out] [case testLiteralWithEnumsAliases-skip] @@ -2945,4 +2802,4 @@ Alias = Test x: Literal[Alias.FOO] reveal_type(x) # E: Revealed type is 'Literal[__main__.Test.FOO]' -[out] \ No newline at end of file +[out] From 52f3d55524b2a72d4f8d8c9bb1a9fb1477f2ebf5 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 12 Apr 2019 14:23:15 -0700 Subject: [PATCH 3/4] Add test case for using function in enums --- test-data/unit/check-literal.test | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 35344b483d0b..05b9cff1fbf3 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2662,7 +2662,10 @@ class Color(Enum): r: Literal[Color.RED] g: Literal[Color.GREEN] b: Literal[Color.BLUE] -bad: Literal[Color] # E: Parameter 1 of Literal[...] is invalid +bad1: Literal[Color] # E: Parameter 1 of Literal[...] is invalid +bad2: Literal[Color.func] # E: Invalid type "__main__.Color.func" \ + # E: Parameter 1 of Literal[...] is invalid +bad3: Literal[Color.func()] # E: Invalid type: Literal[...] cannot contain arbitrary expressions def expects_color(x: Color) -> None: pass def expects_red(x: Literal[Color.RED]) -> None: pass From 0f08e69eeee8674399d41a7f432ba43b7f4c605b Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 12 Apr 2019 15:09:39 -0700 Subject: [PATCH 4/4] Respond to 2nd code review --- mypy/newsemanal/semanal.py | 4 ++-- mypy/newsemanal/typeanal.py | 19 ++++++++++--------- mypy/semanal.py | 4 ++-- mypy/typeanal.py | 21 +++++++++++---------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/mypy/newsemanal/semanal.py b/mypy/newsemanal/semanal.py index 313bf6668ecd..87e8b147e311 100644 --- a/mypy/newsemanal/semanal.py +++ b/mypy/newsemanal/semanal.py @@ -3989,10 +3989,10 @@ def lookup_fully_qualified(self, name: str) -> SymbolTableNode: module namespace is ignored. """ parts = name.split('.') - n = self.modules[parts[0]] # type: Union[MypyFile, TypeInfo] + n = self.modules[parts[0]] for i in range(1, len(parts) - 1): next_sym = n.names[parts[i]] - assert isinstance(next_sym.node, (MypyFile, TypeInfo)) + assert isinstance(next_sym.node, MypyFile) n = next_sym.node return n.names[parts[-1]] diff --git a/mypy/newsemanal/typeanal.py b/mypy/newsemanal/typeanal.py index 99bcb8007561..33a991e063d8 100644 --- a/mypy/newsemanal/typeanal.py +++ b/mypy/newsemanal/typeanal.py @@ -375,24 +375,25 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl return t # Option 3: - # Enum value. Note: Enum values are not real types, so we return - # RawExpressionType only when this function is being called by - # one of the Literal[...] handlers -- when `defining_literal` is True. + # Enum value. Note: we only want to return a LiteralType when + # we're using this enum value specifically within context of + # a "Literal[...]" type. So, if `defining_literal` is not set, + # we bail out early with an error. # - # It's unsafe to return RawExpressionType in any other case, since - # the type would leak out of the semantic analysis phase. + # If, in the distant future, we decide to permit things like + # `def foo(x: Color.RED) -> None: ...`, we can remove that + # check entirely. if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum: value = sym.node.name() base_enum_short_name = sym.node.info.name() - base_enum_qualified_name = sym.node.info.fullname() if not defining_literal: msg = message_registry.INVALID_TYPE_RAW_ENUM_VALUE.format( base_enum_short_name, value) self.fail(msg, t) return AnyType(TypeOfAny.from_error) - return RawExpressionType( - literal_value=value, - base_type_name=base_enum_qualified_name, + return LiteralType( + value=value, + fallback=Instance(sym.node.info, [], line=t.line, column=t.column), line=t.line, column=t.column, ) diff --git a/mypy/semanal.py b/mypy/semanal.py index 49ee106a4507..5826ad0a3f45 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3590,10 +3590,10 @@ def lookup_fully_qualified(self, name: str) -> SymbolTableNode: module namespace is ignored. """ parts = name.split('.') - n = self.modules[parts[0]] # type: Union[MypyFile, TypeInfo] + n = self.modules[parts[0]] for i in range(1, len(parts) - 1): next_sym = n.names[parts[i]] - assert isinstance(next_sym.node, (MypyFile, TypeInfo)) + assert isinstance(next_sym.node, MypyFile) n = next_sym.node return n.names[parts[-1]] diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 9c9610f88755..a4116147cf4f 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -397,24 +397,25 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl return t # Option 3: - # Enum value. Note: Enum values are not real types, so we return - # RawExpressionType only when this function is being called by - # one of the Literal[...] handlers -- when `defining_literal` is True. + # Enum value. Note: we only want to return a LiteralType when + # we're using this enum value specifically within context of + # a "Literal[...]" type. So, if `defining_literal` is not set, + # we bail out early with an error. # - # It's unsafe to return RawExpressionType in any other case, since - # the type would leak out of the semantic analysis phase. - if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum: + # If, in the distant future, we decide to permit things like + # `def foo(x: Color.RED) -> None: ...`, we can remove that + # check entirely. + if isinstance(sym.node, Var) and not t.args and sym.node.info and sym.node.info.is_enum: value = sym.node.name() base_enum_short_name = sym.node.info.name() - base_enum_qualified_name = sym.node.info.fullname() if not defining_literal: msg = message_registry.INVALID_TYPE_RAW_ENUM_VALUE.format( base_enum_short_name, value) self.fail(msg, t) return AnyType(TypeOfAny.from_error) - return RawExpressionType( - literal_value=value, - base_type_name=base_enum_qualified_name, + return LiteralType( + value=value, + fallback=Instance(sym.node.info, [], line=t.line, column=t.column), line=t.line, column=t.column, )