From f9d8f3ae9e4454777e0dd44380ba57bff7ef8ca2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 20 Jun 2024 18:57:31 +0100 Subject: [PATCH] Fix self-referential upper bound in new-style type variables (#17407) Fixes https://github.com/python/mypy/issues/17347 This copies old-style `TypeVar` logic 1:1 (I know it is ugly, but I don't think there is anything better now). Also while I am touching this code, I am removing `third_pass` argument (third pass is not a thing for ~5 years now). --- mypy/plugin.py | 1 - mypy/semanal.py | 13 ++++++------- test-data/unit/check-python312.test | 13 +++++++++++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 38016191de8f..858795addb7f 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -328,7 +328,6 @@ def anal_type( allow_tuple_literal: bool = False, allow_unbound_tvars: bool = False, report_invalid_types: bool = True, - third_pass: bool = False, ) -> Type | None: """Analyze an unbound type. diff --git a/mypy/semanal.py b/mypy/semanal.py index c7a22d20aac6..f857c3e73381 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1738,10 +1738,12 @@ def analyze_type_param( ) -> TypeVarLikeExpr | None: fullname = self.qualified_name(type_param.name) if type_param.upper_bound: - upper_bound = self.anal_type(type_param.upper_bound) + upper_bound = self.anal_type(type_param.upper_bound, allow_placeholder=True) # TODO: we should validate the upper bound is valid for a given kind. if upper_bound is None: - return None + # This and below copies special-casing for old-style type variables, that + # is equally necessary for new-style classes to break a vicious circle. + upper_bound = PlaceholderType(None, [], context.line) else: if type_param.kind == TYPE_VAR_TUPLE_KIND: upper_bound = self.named_type("builtins.tuple", [self.object_type()]) @@ -1752,9 +1754,9 @@ def analyze_type_param( values = [] if type_param.values: for value in type_param.values: - analyzed = self.anal_type(value) + analyzed = self.anal_type(value, allow_placeholder=True) if analyzed is None: - return None + analyzed = PlaceholderType(None, [], context.line) values.append(analyzed) return TypeVarExpr( name=type_param.name, @@ -7192,7 +7194,6 @@ def anal_type( report_invalid_types: bool = True, prohibit_self_type: str | None = None, allow_type_any: bool = False, - third_pass: bool = False, ) -> Type | None: """Semantically analyze a type. @@ -7200,8 +7201,6 @@ def anal_type( typ: Type to analyze (if already analyzed, this is a no-op) allow_placeholder: If True, may return PlaceholderType if encountering an incomplete definition - third_pass: Unused; only for compatibility with old semantic - analyzer Return None only if some part of the type couldn't be bound *and* it referred to an incomplete namespace or definition. In this case also diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 7c3d565b1b44..27027d30a684 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1592,6 +1592,19 @@ d: E[int] # E: Type argument "int" of "E" must be a subtype of "str" [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] +[case testCurrentClassWorksAsBound] +# flags: --enable-incomplete-feature=NewGenericSyntax +from typing import Protocol + +class Comparable[T: Comparable](Protocol): + def compare(self, other: T) -> bool: ... + +class Good: + def compare(self, other: Good) -> bool: ... + +x: Comparable[Good] +y: Comparable[int] # E: Type argument "int" of "Comparable" must be a subtype of "Comparable[Any]" + [case testPEP695TypeAliasWithDifferentTargetTypes] # flags: --enable-incomplete-feature=NewGenericSyntax import types # We need GenericAlias from here, and test stubs don't bring in 'types'