From f2cee90937e96603b7b28083de966f587fba757d Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Wed, 24 Jul 2024 00:27:56 +0300 Subject: [PATCH] PEP 695: Add detection and error reporting for the use of incorrect expressions within the scope of a type parameter and a type alias (#17560) This fixes part of #15238: > Add detection and error reporting for use of assignment expressions, yield, yield from, and await expressions within the type parameter scope; see [this section of PEP](https://peps.python.org/pep-0695/#type-parameter-scopes) for details --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: sobolevn --- mypy/fastparse.py | 47 +++++++++++++++++++++++++++++ mypy/message_registry.py | 24 +++++++++++++++ test-data/unit/check-python312.test | 46 ++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 75c4bd46550c..363fc8375259 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -181,10 +181,12 @@ def ast3_parse( if sys.version_info >= (3, 12): ast_TypeAlias = ast3.TypeAlias ast_ParamSpec = ast3.ParamSpec + ast_TypeVar = ast3.TypeVar ast_TypeVarTuple = ast3.TypeVarTuple else: ast_TypeAlias = Any ast_ParamSpec = Any + ast_TypeVar = Any ast_TypeVarTuple = Any N = TypeVar("N", bound=Node) @@ -345,6 +347,15 @@ def is_no_type_check_decorator(expr: ast3.expr) -> bool: return False +def find_disallowed_expression_in_annotation_scope(expr: ast3.expr | None) -> ast3.expr | None: + if expr is None: + return None + for node in ast3.walk(expr): + if isinstance(node, (ast3.Yield, ast3.YieldFrom, ast3.NamedExpr, ast3.Await)): + return node + return None + + class ASTConverter: def __init__( self, @@ -1180,6 +1191,29 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef: self.class_and_function_stack.pop() return cdef + def validate_type_param(self, type_param: ast_TypeVar) -> None: + incorrect_expr = find_disallowed_expression_in_annotation_scope(type_param.bound) + if incorrect_expr is None: + return + if isinstance(incorrect_expr, (ast3.Yield, ast3.YieldFrom)): + self.fail( + message_registry.TYPE_VAR_YIELD_EXPRESSION_IN_BOUND, + type_param.lineno, + type_param.col_offset, + ) + if isinstance(incorrect_expr, ast3.NamedExpr): + self.fail( + message_registry.TYPE_VAR_NAMED_EXPRESSION_IN_BOUND, + type_param.lineno, + type_param.col_offset, + ) + if isinstance(incorrect_expr, ast3.Await): + self.fail( + message_registry.TYPE_VAR_AWAIT_EXPRESSION_IN_BOUND, + type_param.lineno, + type_param.col_offset, + ) + def translate_type_params(self, type_params: list[Any]) -> list[TypeParam]: explicit_type_params = [] for p in type_params: @@ -1202,6 +1236,7 @@ def translate_type_params(self, type_params: list[Any]) -> list[TypeParam]: conv = TypeConverter(self.errors, line=p.lineno) values = [conv.visit(t) for t in p.bound.elts] elif p.bound is not None: + self.validate_type_param(p) bound = TypeConverter(self.errors, line=p.lineno).visit(p.bound) explicit_type_params.append(TypeParam(p.name, TYPE_VAR_KIND, bound, values)) return explicit_type_params @@ -1791,11 +1826,23 @@ def visit_MatchOr(self, n: MatchOr) -> OrPattern: node = OrPattern([self.visit(pattern) for pattern in n.patterns]) return self.set_line(node, n) + def validate_type_alias(self, n: ast_TypeAlias) -> None: + incorrect_expr = find_disallowed_expression_in_annotation_scope(n.value) + if incorrect_expr is None: + return + if isinstance(incorrect_expr, (ast3.Yield, ast3.YieldFrom)): + self.fail(message_registry.TYPE_ALIAS_WITH_YIELD_EXPRESSION, n.lineno, n.col_offset) + if isinstance(incorrect_expr, ast3.NamedExpr): + self.fail(message_registry.TYPE_ALIAS_WITH_NAMED_EXPRESSION, n.lineno, n.col_offset) + if isinstance(incorrect_expr, ast3.Await): + self.fail(message_registry.TYPE_ALIAS_WITH_AWAIT_EXPRESSION, n.lineno, n.col_offset) + # TypeAlias(identifier name, type_param* type_params, expr value) def visit_TypeAlias(self, n: ast_TypeAlias) -> TypeAliasStmt | AssignmentStmt: node: TypeAliasStmt | AssignmentStmt if NEW_GENERIC_SYNTAX in self.options.enable_incomplete_feature: type_params = self.translate_type_params(n.type_params) + self.validate_type_alias(n) value = self.visit(n.value) # Since the value is evaluated lazily, wrap the value inside a lambda. # This helps mypyc. diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 06199e70d6b4..29d539faaed6 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -338,3 +338,27 @@ def with_additional_msg(self, info: str) -> ErrorMessage: TYPE_VAR_TOO_FEW_CONSTRAINED_TYPES: Final = ErrorMessage( "Type variable must have at least two constrained types", codes.MISC ) + +TYPE_VAR_YIELD_EXPRESSION_IN_BOUND: Final = ErrorMessage( + "Yield expression cannot be used as a type variable bound", codes.SYNTAX +) + +TYPE_VAR_NAMED_EXPRESSION_IN_BOUND: Final = ErrorMessage( + "Named expression cannot be used as a type variable bound", codes.SYNTAX +) + +TYPE_VAR_AWAIT_EXPRESSION_IN_BOUND: Final = ErrorMessage( + "Await expression cannot be used as a type variable bound", codes.SYNTAX +) + +TYPE_ALIAS_WITH_YIELD_EXPRESSION: Final = ErrorMessage( + "Yield expression cannot be used within a type alias", codes.SYNTAX +) + +TYPE_ALIAS_WITH_NAMED_EXPRESSION: Final = ErrorMessage( + "Named expression cannot be used within a type alias", codes.SYNTAX +) + +TYPE_ALIAS_WITH_AWAIT_EXPRESSION: Final = ErrorMessage( + "Await expression cannot be used within a type alias", codes.SYNTAX +) diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 073ef7f4bdec..a3f4c87120cd 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1667,3 +1667,49 @@ if x["other"] is not None: type Y[T] = {"item": T, **Y[T]} # E: Overwriting TypedDict field "item" while merging [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] + +[case testPEP695UsingIncorrectExpressionsInTypeVariableBound] +# flags: --enable-incomplete-feature=NewGenericSyntax + +type X[T: (yield 1)] = Any # E: Yield expression cannot be used as a type variable bound +type Y[T: (yield from [])] = Any # E: Yield expression cannot be used as a type variable bound +type Z[T: (a := 1)] = Any # E: Named expression cannot be used as a type variable bound +type K[T: (await 1)] = Any # E: Await expression cannot be used as a type variable bound + +type XNested[T: (1 + (yield 1))] = Any # E: Yield expression cannot be used as a type variable bound +type YNested[T: (1 + (yield from []))] = Any # E: Yield expression cannot be used as a type variable bound +type ZNested[T: (1 + (a := 1))] = Any # E: Named expression cannot be used as a type variable bound +type KNested[T: (1 + (await 1))] = Any # E: Await expression cannot be used as a type variable bound + +class FooX[T: (yield 1)]: pass # E: Yield expression cannot be used as a type variable bound +class FooY[T: (yield from [])]: pass # E: Yield expression cannot be used as a type variable bound +class FooZ[T: (a := 1)]: pass # E: Named expression cannot be used as a type variable bound +class FooK[T: (await 1)]: pass # E: Await expression cannot be used as a type variable bound + +class FooXNested[T: (1 + (yield 1))]: pass # E: Yield expression cannot be used as a type variable bound +class FooYNested[T: (1 + (yield from []))]: pass # E: Yield expression cannot be used as a type variable bound +class FooZNested[T: (1 + (a := 1))]: pass # E: Named expression cannot be used as a type variable bound +class FooKNested[T: (1 + (await 1))]: pass # E: Await expression cannot be used as a type variable bound + +def foox[T: (yield 1)](): pass # E: Yield expression cannot be used as a type variable bound +def fooy[T: (yield from [])](): pass # E: Yield expression cannot be used as a type variable bound +def fooz[T: (a := 1)](): pass # E: Named expression cannot be used as a type variable bound +def fook[T: (await 1)](): pass # E: Await expression cannot be used as a type variable bound + +def foox_nested[T: (1 + (yield 1))](): pass # E: Yield expression cannot be used as a type variable bound +def fooy_nested[T: (1 + (yield from []))](): pass # E: Yield expression cannot be used as a type variable bound +def fooz_nested[T: (1 + (a := 1))](): pass # E: Named expression cannot be used as a type variable bound +def fook_nested[T: (1 +(await 1))](): pass # E: Await expression cannot be used as a type variable bound + +[case testPEP695UsingIncorrectExpressionsInTypeAlias] +# flags: --enable-incomplete-feature=NewGenericSyntax + +type X = (yield 1) # E: Yield expression cannot be used within a type alias +type Y = (yield from []) # E: Yield expression cannot be used within a type alias +type Z = (a := 1) # E: Named expression cannot be used within a type alias +type K = (await 1) # E: Await expression cannot be used within a type alias + +type XNested = (1 + (yield 1)) # E: Yield expression cannot be used within a type alias +type YNested = (1 + (yield from [])) # E: Yield expression cannot be used within a type alias +type ZNested = (1 + (a := 1)) # E: Named expression cannot be used within a type alias +type KNested = (1 + (await 1)) # E: Await expression cannot be used within a type alias