From 4c8e60c5b2f448d76c67a91d95228cc372799ef3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 6 Jun 2024 11:41:30 +0100 Subject: [PATCH 1/5] [PEP 695] Detect invalid number of constrained types At least two are required, according do PEP 695. Work on #15238. --- mypy/fastparse.py | 8 ++++++-- mypy/message_registry.py | 3 +++ test-data/unit/check-python312.test | 10 ++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 49f0a938b750..07b3179c9fef 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -1185,8 +1185,12 @@ def translate_type_params(self, type_params: list[Any]) -> list[TypeParam]: explicit_type_params.append(TypeParam(p.name, TYPE_VAR_TUPLE_KIND, None, [])) else: if isinstance(p.bound, ast3.Tuple): - conv = TypeConverter(self.errors, line=p.lineno) - values = [conv.visit(t) for t in p.bound.elts] + if len(p.bound.elts) < 2: + self.fail(message_registry.TYPE_VAR_TOO_FEW_CONSTRAINED_TYPES, + p.lineno, p.col_offset, blocker=False) + else: + conv = TypeConverter(self.errors, line=p.lineno) + values = [conv.visit(t) for t in p.bound.elts] elif p.bound is not None: bound = TypeConverter(self.errors, line=p.lineno).visit(p.bound) explicit_type_params.append(TypeParam(p.name, TYPE_VAR_KIND, bound, values)) diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 3852431f2290..775408ab581a 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -330,3 +330,6 @@ def with_additional_msg(self, info: str) -> ErrorMessage: NARROWED_TYPE_NOT_SUBTYPE: Final = ErrorMessage( "Narrowed type {} is not a subtype of input type {}", codes.NARROWED_TYPE_NOT_SUBTYPE ) +TYPE_VAR_TOO_FEW_CONSTRAINED_TYPES: Final = ErrorMessage( + "Type variable must have at least two constrained types", codes.MISC +) diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index a1c819667087..5c74401c5585 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1494,3 +1494,13 @@ reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" # flags: --enable-incomplete-feature=NewGenericSyntax def f[T](x: foobar, y: T) -> T: ... # E: Name "foobar" is not defined reveal_type(f) # N: Revealed type is "def [T] (x: Any, y: T`-1) -> T`-1" + +[case testPEP695WrongNumberOfConstrainedTypes] +# flags: --enable-incomplete-feature=NewGenericSyntax +type A[T: ()] = list[T] # E: Type variable must have at least two constrained types +a: A[int] +reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + +type B[T: (int,)] = list[T] # E: Type variable must have at least two constrained types +b: B[str] +reveal_type(b) # N: Revealed type is "builtins.list[builtins.str]" From b0ebf0d863cc1d6930c6370a72fb430c2608dd98 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 6 Jun 2024 12:13:39 +0100 Subject: [PATCH 2/5] [PEP 695] Add tests for various errors --- test-data/unit/check-python312.test | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 5c74401c5585..06c5bada1e92 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1504,3 +1504,24 @@ reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" type B[T: (int,)] = list[T] # E: Type variable must have at least two constrained types b: B[str] reveal_type(b) # N: Revealed type is "builtins.list[builtins.str]" + +[case testPEP695UsingTypeVariableInOwnBoundOrConstraint] +# flags: --enable-incomplete-feature=NewGenericSyntax +type A[T: list[T]] = str # E: Name "T" is not defined +type B[S: (list[S], str)] = str # E: Name "S" is not defined +type C[T, S: list[T]] = str # E: Name "T" is not defined + +def f[T: T](x: T) -> T: ... # E: Name "T" is not defined +class D[T: T]: # E: Name "T" is not defined + pass + +[case testPEP695InvalidType] +# flags: --enable-incomplete-feature=NewGenericSyntax +def f[T: 1](x: T) -> T: ... # E: Invalid type: try using Literal[1] instead? +class C[T: (int, (1 + 2))]: pass # E: Invalid type comment or annotation +type A = list[1] # E: Invalid type: try using Literal[1] instead? +type B = (1 + 2) # E: Invalid type alias: expression is not a valid type +a: A +reveal_type(a) # N: Revealed type is "builtins.list[Any]" +b: B +reveal_type(b) # N: Revealed type is "Any" From 516e1bb6eb5418fb2be5b774f53867620e03baab Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 6 Jun 2024 12:18:44 +0100 Subject: [PATCH 3/5] Black --- mypy/fastparse.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 07b3179c9fef..70afe9010583 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -1186,8 +1186,12 @@ def translate_type_params(self, type_params: list[Any]) -> list[TypeParam]: else: if isinstance(p.bound, ast3.Tuple): if len(p.bound.elts) < 2: - self.fail(message_registry.TYPE_VAR_TOO_FEW_CONSTRAINED_TYPES, - p.lineno, p.col_offset, blocker=False) + self.fail( + message_registry.TYPE_VAR_TOO_FEW_CONSTRAINED_TYPES, + p.lineno, + p.col_offset, + blocker=False, + ) else: conv = TypeConverter(self.errors, line=p.lineno) values = [conv.visit(t) for t in p.bound.elts] From ff1c839d1566edcb6af8ffdc2783cfe60c2368dd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 6 Jun 2024 13:05:00 +0100 Subject: [PATCH 4/5] Attempt to work around mypyc error --- mypy/errorcodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 7de796a70c8d..6e8763264ddd 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -271,7 +271,7 @@ def __hash__(self) -> int: del error_codes[FILE.code] # This is a catch-all for remaining uncategorized errors. -MISC: Final = ErrorCode("misc", "Miscellaneous other checks", "General") +MISC: Final[ErrorCode] = ErrorCode("misc", "Miscellaneous other checks", "General") OVERLOAD_OVERLAP: Final[ErrorCode] = ErrorCode( "overload-overlap", From 308ae713f921e17f54d44be22cd83a1da84bd256 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 14 Jun 2024 16:13:51 +0100 Subject: [PATCH 5/5] Make messages more consistent --- mypy/semanal.py | 9 +++++++-- test-data/unit/semanal-errors.test | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 98184ab41dd7..1eb2b9b890e8 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -59,6 +59,7 @@ from mypy.errorcodes import PROPERTY_DECORATOR, ErrorCode from mypy.errors import Errors, report_internal_error from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type +from mypy.message_registry import ErrorMessage from mypy.messages import ( SUGGESTED_TEST_FIXTURES, TYPES_FOR_UNIMPORTED_HINTS, @@ -4593,7 +4594,7 @@ def process_typevar_parameters( self.fail("TypeVar cannot be both covariant and contravariant", context) return None elif num_values == 1: - self.fail("TypeVar cannot have only a single constraint", context) + self.fail(message_registry.TYPE_VAR_TOO_FEW_CONSTRAINED_TYPES, context) return None elif covariant: variance = COVARIANT @@ -7009,7 +7010,7 @@ def in_checked_function(self) -> bool: def fail( self, - msg: str, + msg: str | ErrorMessage, ctx: Context, serious: bool = False, *, @@ -7020,6 +7021,10 @@ def fail( return # In case it's a bug and we don't really have context assert ctx is not None, msg + if isinstance(msg, ErrorMessage): + if code is None: + code = msg.code + msg = msg.value self.errors.report(ctx.line, ctx.column, msg, blocker=blocker, code=code) def note(self, msg: str, ctx: Context, code: ErrorCode | None = None) -> None: diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index 269536f868a4..33c8f9b80aa0 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -1046,7 +1046,7 @@ T = TypeVar(b'T') # E: TypeVar() expects a string literal as first argument d = TypeVar('D') # E: String argument 1 "D" to TypeVar(...) does not match variable name "d" e = TypeVar('e', int, str, x=1) # E: Unexpected argument to "TypeVar()": "x" f = TypeVar('f', (int, str), int) # E: Type expected -g = TypeVar('g', int) # E: TypeVar cannot have only a single constraint +g = TypeVar('g', int) # E: Type variable must have at least two constrained types h = TypeVar('h', x=(int, str)) # E: Unexpected argument to "TypeVar()": "x" i = TypeVar('i', bound=1) # E: TypeVar "bound" must be a type j = TypeVar('j', covariant=None) # E: TypeVar "covariant" may only be a literal bool