From d9ea9920d68918be18666a0b8d2888bf8cb6c127 Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Wed, 29 Dec 2021 16:46:36 +0300
Subject: [PATCH 1/5] Allows to use `type[T]` in stubs

---
 mypy/typeanal.py                        | 23 +++++++++++++----------
 test-data/unit/check-generic-alias.test |  3 +++
 2 files changed, 16 insertions(+), 10 deletions(-)

diff --git a/mypy/typeanal.py b/mypy/typeanal.py
index e29ccfd3c928..5c9259651f47 100644
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -163,6 +163,14 @@ def __init__(self,
         # Names of type aliases encountered while analysing a type will be collected here.
         self.aliases_used: Set[str] = set()
 
+    @property
+    def is_future_annotations(self) -> bool:
+        return (
+            self.options.python_version >= (3, 9)
+            or self.api.is_future_flag_set('annotations')
+            or self.allow_new_syntax  # basically tells us if this is a stub
+        )
+
     def visit_unbound_type(self, t: UnboundType, defining_literal: bool = False) -> Type:
         typ = self.visit_unbound_type_nonoptional(t, defining_literal)
         if t.optional:
@@ -203,8 +211,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
                 return hook(AnalyzeTypeContext(t, t, self))
             if (fullname in get_nongen_builtins(self.options.python_version)
                     and t.args and
-                    not self.allow_new_syntax and
-                    not self.api.is_future_flag_set("annotations")):
+                    not self.is_future_annotations):
                 self.fail(no_subscript_builtin_alias(fullname,
                                                      propose_alt=not self.defining_alias), t)
             tvar_def = self.tvar_scope.get_binding(sym)
@@ -291,9 +298,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
                       " in a variable annotation", t)
             return AnyType(TypeOfAny.from_error)
         elif (fullname == 'typing.Tuple' or
-             (fullname == 'builtins.tuple' and (self.options.python_version >= (3, 9) or
-                                                self.api.is_future_flag_set('annotations') or
-                                                self.allow_new_syntax))):
+             (fullname == 'builtins.tuple' and self.is_future_annotations)):
             # Tuple is special because it is involved in builtin import cycle
             # and may be not ready when used.
             sym = self.api.lookup_fully_qualified_or_none('builtins.tuple')
@@ -326,8 +331,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
         elif fullname == 'typing.Callable':
             return self.analyze_callable_type(t)
         elif (fullname == 'typing.Type' or
-             (fullname == 'builtins.type' and (self.options.python_version >= (3, 9) or
-                                               self.api.is_future_flag_set('annotations')))):
+             (fullname == 'builtins.type' and self.is_future_annotations)):
             if len(t.args) == 0:
                 if fullname == 'typing.Type':
                     any_type = self.get_omitted_any(t)
@@ -405,6 +409,7 @@ def analyze_type_with_type_info(
                             ctx.line, ctx.column)
         # Check type argument count.
         if len(instance.args) != len(info.type_vars) and not self.defining_alias:
+            # raise ValueError(instance)
             fix_instance(instance, self.fail, self.note,
                          disallow_any=self.options.disallow_any_generics and
                          not self.is_typeshed_stub,
@@ -704,9 +709,7 @@ def visit_star_type(self, t: StarType) -> Type:
     def visit_union_type(self, t: UnionType) -> Type:
         if (t.uses_pep604_syntax is True
                 and t.is_evaluated is True
-                and self.api.is_stub_file is False
-                and self.options.python_version < (3, 10)
-                and self.api.is_future_flag_set('annotations') is False):
+                and not self.is_future_annotations):
             self.fail("X | Y syntax for unions requires Python 3.10", t)
         return UnionType(self.anal_array(t.items), t.line)
 
diff --git a/test-data/unit/check-generic-alias.test b/test-data/unit/check-generic-alias.test
index b6060f3922fe..bfef0f5c5086 100644
--- a/test-data/unit/check-generic-alias.test
+++ b/test-data/unit/check-generic-alias.test
@@ -273,6 +273,8 @@ b: B
 import m
 reveal_type(m.a)  # N: Revealed type is "builtins.list[builtins.int]"
 reveal_type(m.b)  # N: Revealed type is "builtins.list[builtins.list[builtins.int]]"
+m.C  # has complex representation, ignored
+reveal_type(m.d)  # N: Revealed type is "Type[builtins.str]"
 
 [file m.pyi]
 A = list[int]
@@ -281,4 +283,5 @@ B = list[list[int]]
 b: B
 class C(list[int]):
     pass
+d: type[str]
 [builtins fixtures/list.pyi]

From f79f3e4f0ebc7bc4c5e0e5b9839f421d2981e1b4 Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Wed, 29 Dec 2021 16:48:43 +0300
Subject: [PATCH 2/5] Removes unused comment

---
 mypy/typeanal.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/mypy/typeanal.py b/mypy/typeanal.py
index 5c9259651f47..a707141e3e36 100644
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -409,7 +409,6 @@ def analyze_type_with_type_info(
                             ctx.line, ctx.column)
         # Check type argument count.
         if len(instance.args) != len(info.type_vars) and not self.defining_alias:
-            # raise ValueError(instance)
             fix_instance(instance, self.fail, self.note,
                          disallow_any=self.options.disallow_any_generics and
                          not self.is_typeshed_stub,

From f35e9ceeea78f1960f2bc9c8278a287f497bebde Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Wed, 29 Dec 2021 17:21:39 +0300
Subject: [PATCH 3/5] Fixes CI

---
 mypy/typeanal.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/mypy/typeanal.py b/mypy/typeanal.py
index a707141e3e36..6800e0ded148 100644
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -163,10 +163,10 @@ def __init__(self,
         # Names of type aliases encountered while analysing a type will be collected here.
         self.aliases_used: Set[str] = set()
 
-    @property
-    def is_future_annotations(self) -> bool:
+    def is_future_annotations(self, target_version: Optional[Tuple[int, int]] = None) -> bool:
         return (
-            self.options.python_version >= (3, 9)
+            (self.options.python_version >= target_version
+                if target_version is not None else False)
             or self.api.is_future_flag_set('annotations')
             or self.allow_new_syntax  # basically tells us if this is a stub
         )
@@ -211,7 +211,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
                 return hook(AnalyzeTypeContext(t, t, self))
             if (fullname in get_nongen_builtins(self.options.python_version)
                     and t.args and
-                    not self.is_future_annotations):
+                    not self.is_future_annotations()):
                 self.fail(no_subscript_builtin_alias(fullname,
                                                      propose_alt=not self.defining_alias), t)
             tvar_def = self.tvar_scope.get_binding(sym)
@@ -298,7 +298,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
                       " in a variable annotation", t)
             return AnyType(TypeOfAny.from_error)
         elif (fullname == 'typing.Tuple' or
-             (fullname == 'builtins.tuple' and self.is_future_annotations)):
+             (fullname == 'builtins.tuple' and self.is_future_annotations((3, 9)))):
             # Tuple is special because it is involved in builtin import cycle
             # and may be not ready when used.
             sym = self.api.lookup_fully_qualified_or_none('builtins.tuple')
@@ -331,7 +331,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
         elif fullname == 'typing.Callable':
             return self.analyze_callable_type(t)
         elif (fullname == 'typing.Type' or
-             (fullname == 'builtins.type' and self.is_future_annotations)):
+             (fullname == 'builtins.type' and self.is_future_annotations((3, 9)))):
             if len(t.args) == 0:
                 if fullname == 'typing.Type':
                     any_type = self.get_omitted_any(t)
@@ -708,7 +708,7 @@ def visit_star_type(self, t: StarType) -> Type:
     def visit_union_type(self, t: UnionType) -> Type:
         if (t.uses_pep604_syntax is True
                 and t.is_evaluated is True
-                and not self.is_future_annotations):
+                and not self.is_future_annotations((3, 10))):
             self.fail("X | Y syntax for unions requires Python 3.10", t)
         return UnionType(self.anal_array(t.items), t.line)
 

From a4a040dc2660442c400ca574745549d67e9e18f7 Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Thu, 30 Dec 2021 12:49:05 +0300
Subject: [PATCH 4/5] Addresses review

---
 mypy/semanal.py  |  4 +---
 mypy/typeanal.py | 36 ++++++++++++++++--------------------
 2 files changed, 17 insertions(+), 23 deletions(-)

diff --git a/mypy/semanal.py b/mypy/semanal.py
index a9226d2cdd0c..2b3956612eab 100644
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -2629,7 +2629,6 @@ def analyze_alias(self, rvalue: Expression,
                                  self.plugin,
                                  self.options,
                                  self.is_typeshed_stub_file,
-                                 allow_new_syntax=self.is_stub_file,
                                  allow_placeholder=allow_placeholder,
                                  in_dynamic_func=dynamic,
                                  global_scope=global_scope)
@@ -5183,8 +5182,7 @@ def type_analyzer(self, *,
                             allow_tuple_literal=allow_tuple_literal,
                             report_invalid_types=report_invalid_types,
                             allow_placeholder=allow_placeholder,
-                            allow_required=allow_required,
-                            allow_new_syntax=self.is_stub_file)
+                            allow_required=allow_required)
         tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic())
         tpan.global_scope = not self.type and not self.function_stack
         return tpan
diff --git a/mypy/typeanal.py b/mypy/typeanal.py
index 6800e0ded148..0f7b87853dc3 100644
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -70,7 +70,6 @@ def analyze_type_alias(node: Expression,
                        plugin: Plugin,
                        options: Options,
                        is_typeshed_stub: bool,
-                       allow_new_syntax: bool = False,
                        allow_placeholder: bool = False,
                        in_dynamic_func: bool = False,
                        global_scope: bool = True) -> Optional[Tuple[Type, Set[str]]]:
@@ -80,13 +79,13 @@ def analyze_type_alias(node: Expression,
     full names of type aliases it depends on (directly or indirectly).
     Return None otherwise. 'node' must have been semantically analyzed.
     """
-    try:
-        type = expr_to_unanalyzed_type(node, options, allow_new_syntax)
+    try:  # TODO: refactor `expr_to_type` to work with `__future__` import
+        type = expr_to_unanalyzed_type(node, options, api.is_stub_file)
     except TypeTranslationError:
         api.fail('Invalid type alias: expression is not a valid type', node)
         return None
     analyzer = TypeAnalyser(api, tvar_scope, plugin, options, is_typeshed_stub,
-                            allow_new_syntax=allow_new_syntax, defining_alias=True,
+                            defining_alias=True,
                             allow_placeholder=allow_placeholder)
     analyzer.in_dynamic_func = in_dynamic_func
     analyzer.global_scope = global_scope
@@ -127,7 +126,6 @@ def __init__(self,
                  is_typeshed_stub: bool, *,
                  defining_alias: bool = False,
                  allow_tuple_literal: bool = False,
-                 allow_new_syntax: bool = False,
                  allow_unbound_tvars: bool = False,
                  allow_placeholder: bool = False,
                  allow_required: bool = False,
@@ -144,8 +142,11 @@ def __init__(self,
         # Positive if we are analyzing arguments of another (outer) type
         self.nesting_level = 0
         # Should we allow new type syntax when targeting older Python versions
-        # like 'list[int]' or 'X | Y' (allowed in stubs)?
-        self.allow_new_syntax = allow_new_syntax
+        # like 'list[int]' or 'X | Y' (allowed in stubs and with `__future__` import)?
+        self.always_allow_new_syntax = (
+            self.api.is_stub_file
+            or self.api.is_future_flag_set('annotations')
+        )
         # Should we accept unbound type variables (always OK in aliases)?
         self.allow_unbound_tvars = allow_unbound_tvars or defining_alias
         # If false, record incomplete ref if we generate PlaceholderType.
@@ -163,14 +164,6 @@ def __init__(self,
         # Names of type aliases encountered while analysing a type will be collected here.
         self.aliases_used: Set[str] = set()
 
-    def is_future_annotations(self, target_version: Optional[Tuple[int, int]] = None) -> bool:
-        return (
-            (self.options.python_version >= target_version
-                if target_version is not None else False)
-            or self.api.is_future_flag_set('annotations')
-            or self.allow_new_syntax  # basically tells us if this is a stub
-        )
-
     def visit_unbound_type(self, t: UnboundType, defining_literal: bool = False) -> Type:
         typ = self.visit_unbound_type_nonoptional(t, defining_literal)
         if t.optional:
@@ -210,8 +203,8 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
             if hook is not None:
                 return hook(AnalyzeTypeContext(t, t, self))
             if (fullname in get_nongen_builtins(self.options.python_version)
-                    and t.args and
-                    not self.is_future_annotations()):
+                    and t.args
+                    and not self.always_allow_new_syntax):
                 self.fail(no_subscript_builtin_alias(fullname,
                                                      propose_alt=not self.defining_alias), t)
             tvar_def = self.tvar_scope.get_binding(sym)
@@ -298,7 +291,8 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
                       " in a variable annotation", t)
             return AnyType(TypeOfAny.from_error)
         elif (fullname == 'typing.Tuple' or
-             (fullname == 'builtins.tuple' and self.is_future_annotations((3, 9)))):
+             (fullname == 'builtins.tuple'
+                and (self.always_allow_new_syntax or self.options.python_version >= (3, 9)))):
             # Tuple is special because it is involved in builtin import cycle
             # and may be not ready when used.
             sym = self.api.lookup_fully_qualified_or_none('builtins.tuple')
@@ -331,7 +325,8 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
         elif fullname == 'typing.Callable':
             return self.analyze_callable_type(t)
         elif (fullname == 'typing.Type' or
-             (fullname == 'builtins.type' and self.is_future_annotations((3, 9)))):
+             (fullname == 'builtins.type'
+                and (self.always_allow_new_syntax or self.options.python_version >= (3, 9)))):
             if len(t.args) == 0:
                 if fullname == 'typing.Type':
                     any_type = self.get_omitted_any(t)
@@ -708,7 +703,8 @@ def visit_star_type(self, t: StarType) -> Type:
     def visit_union_type(self, t: UnionType) -> Type:
         if (t.uses_pep604_syntax is True
                 and t.is_evaluated is True
-                and not self.is_future_annotations((3, 10))):
+                and not self.always_allow_new_syntax
+                and not self.options.python_version >= (3, 10)):
             self.fail("X | Y syntax for unions requires Python 3.10", t)
         return UnionType(self.anal_array(t.items), t.line)
 

From 3c0fe262ac71262a4c1dddcb781b6802e0642e5a Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Thu, 30 Dec 2021 12:56:47 +0300
Subject: [PATCH 5/5] Remove TODO

---
 mypy/typeanal.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mypy/typeanal.py b/mypy/typeanal.py
index 0f7b87853dc3..969c756cb8b5 100644
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -79,7 +79,7 @@ def analyze_type_alias(node: Expression,
     full names of type aliases it depends on (directly or indirectly).
     Return None otherwise. 'node' must have been semantically analyzed.
     """
-    try:  # TODO: refactor `expr_to_type` to work with `__future__` import
+    try:
         type = expr_to_unanalyzed_type(node, options, api.is_stub_file)
     except TypeTranslationError:
         api.fail('Invalid type alias: expression is not a valid type', node)