Repository: python/mypy
Pull request: Support type alias as Literal
resolves #7996

I'm not very sure about how effective the current testcase is, suggestions are welcomed.


Problem statement:
Variant of alias for enum type cannot be used as Literal
```python
class Foo(Enum):
    A = 1

F = Foo

# works
x: Literal[Foo.A]
# name-defined error
y: Literal[F.A]
```

Is there a simple workaround here? I also tried `F: Final = Foo` which doesn't work either

Hints from issue discussion:
This looks like a bug. I'm not sure if there's any general workaround right now.

cc @Michael0x2a (literal types) and @ilevkivskyi (type aliases)
This looks like a missing case in `lookup_qualified()`, this doesn't work either:
```python
class Out:
    class In:
        ...
O = Out
z: O.In  # Same name not defined error
```
Interestingly, `visit_member_expr()` has an `elif` branch for this, so for example this works (on master):
```python
f: Final = F.A
reveal_type(f)  # Revealed type is 'Literal[__main__.Foo.A]?'
```
Should be easy to fix.

Commit message: remove redundant temporal var and a more meaningful testcase name

Patch to review:
diff --git a/mypy/semanal.py b/mypy/semanal.py
index 718da41937af..14cb7fe02f85 100644
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -3621,7 +3621,6 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
                         type_info = self.type
             elif isinstance(base.node, TypeAlias) and base.node.no_args:
                 assert isinstance(base.node.target, ProperType)
-                # TODO: support chained aliases.
                 if isinstance(base.node.target, Instance):
                     type_info = base.node.target.type
 
@@ -3969,6 +3968,10 @@ def lookup_qualified(self, name: str, ctx: Context,
                     namespace = node.fullname
                 elif isinstance(node, PlaceholderNode):
                     return sym
+                elif isinstance(node, TypeAlias) and node.no_args:
+                    assert isinstance(node.target, ProperType)
+                    if isinstance(node.target, Instance):
+                        nextsym = node.target.type.get(part)
                 else:
                     if isinstance(node, Var):
                         typ = get_proper_type(node.type)
diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test
index da6f5dfd25db..f5445429b709 100644
--- a/test-data/unit/check-literal.test
+++ b/test-data/unit/check-literal.test
@@ -3149,3 +3149,40 @@ err_code = -TWO
 if bool():
     err_code = -THREE
 [builtins fixtures/float.pyi]
+
+[case testNestedClassOnAliasAsType]
+from typing_extensions import Literal
+
+class Out:
+    class In:
+        class Inner:
+            pass
+
+O = Out
+I = Out.In
+OI = O.In
+A = Out
+B = A
+
+w: O.In
+x: I.Inner
+y: OI.Inner
+z: B.In
+reveal_type(w)  # N: Revealed type is '__main__.Out.In'
+reveal_type(x)  # N: Revealed type is '__main__.Out.In.Inner'
+reveal_type(y)  # N: Revealed type is '__main__.Out.In.Inner'
+reveal_type(z)  # N: Revealed type is '__main__.Out.In'
+
+[case testAliasForEnumTypeAsLiteral]
+from typing_extensions import Literal
+from enum import Enum
+
+class Foo(Enum):
+    A = 1
+
+F = Foo
+
+x: Literal[Foo.A]
+y: Literal[F.A]
+reveal_type(x)  # N: Revealed type is 'Literal[__main__.Foo.A]'
+reveal_type(y)  # N: Revealed type is 'Literal[__main__.Foo.A]'


Code review comments:
diff_hunk: @@ -3149,3 +3149,40 @@ err_code = -TWO
 if bool():
     err_code = -THREE
 [builtins fixtures/float.pyi]
+
+[case testNestedClassOnAliasAsType]
+from typing_extensions import Literal
path: test-data/unit/check-literal.test
text: @user1:
Actually one more thing, sorry for not noticing this before. This import is unused, and the test case can be moved to `check-type-aliases.test`.

@author:
Wow, I didn't notice that unused Literal import either, great catch!