Skip to content

Commit

Permalink
[PEP 695] Detect errors related to mixing old and new style features (#…
Browse files Browse the repository at this point in the history
…17269)

`Generic[...]` or `Protocol[...]` shouldn't be used with new-style
syntax.

Generic functions and classes using the new syntax shouldn't mix
new-style and
old-style type parameters.

Work on #15238.
  • Loading branch information
JukkaL committed May 30, 2024
1 parent f60f458 commit 7032f8c
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 9 deletions.
4 changes: 4 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2421,6 +2421,10 @@ def annotation_in_unchecked_function(self, context: Context) -> None:
code=codes.ANNOTATION_UNCHECKED,
)

def type_parameters_should_be_declared(self, undeclared: list[str], context: Context) -> None:
names = ", ".join('"' + n + '"' for n in undeclared)
self.fail(f"All type parameters should be declared ({names} not declared)", context)


def quote_type_string(type_string: str) -> str:
"""Quotes a type representation for use in messages."""
Expand Down
40 changes: 31 additions & 9 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,14 @@ def update_function_type_variables(self, fun_type: CallableType, defn: FuncItem)
fun_type.variables, has_self_type = a.bind_function_type_variables(fun_type, defn)
if has_self_type and self.type is not None:
self.setup_self_type()
if defn.type_args:
bound_fullnames = {v.fullname for v in fun_type.variables}
declared_fullnames = {self.qualified_name(p.name) for p in defn.type_args}
extra = sorted(bound_fullnames - declared_fullnames)
if extra:
self.msg.type_parameters_should_be_declared(
[n.split(".")[-1] for n in extra], defn
)
return has_self_type

def setup_self_type(self) -> None:
Expand Down Expand Up @@ -2076,11 +2084,19 @@ class Foo(Bar, Generic[T]): ...
continue
result = self.analyze_class_typevar_declaration(base)
if result is not None:
if declared_tvars:
self.fail("Only single Generic[...] or Protocol[...] can be in bases", context)
removed.append(i)
tvars = result[0]
is_protocol |= result[1]
if declared_tvars:
if defn.type_args:
if is_protocol:
self.fail('No arguments expected for "Protocol" base class', context)
else:
self.fail("Generic[...] base class is redundant", context)
else:
self.fail(
"Only single Generic[...] or Protocol[...] can be in bases", context
)
removed.append(i)
declared_tvars.extend(tvars)
if isinstance(base, UnboundType):
sym = self.lookup_qualified(base.name, base)
Expand All @@ -2092,15 +2108,21 @@ class Foo(Bar, Generic[T]): ...

all_tvars = self.get_all_bases_tvars(base_type_exprs, removed)
if declared_tvars:
if len(remove_dups(declared_tvars)) < len(declared_tvars):
if len(remove_dups(declared_tvars)) < len(declared_tvars) and not defn.type_args:
self.fail("Duplicate type variables in Generic[...] or Protocol[...]", context)
declared_tvars = remove_dups(declared_tvars)
if not set(all_tvars).issubset(set(declared_tvars)):
self.fail(
"If Generic[...] or Protocol[...] is present"
" it should list all type variables",
context,
)
if defn.type_args:
undeclared = sorted(set(all_tvars) - set(declared_tvars))
self.msg.type_parameters_should_be_declared(
[tv[0] for tv in undeclared], context
)
else:
self.fail(
"If Generic[...] or Protocol[...] is present"
" it should list all type variables",
context,
)
# In case of error, Generic tvars will go first
declared_tvars = remove_dups(declared_tvars + all_tvars)
else:
Expand Down
40 changes: 40 additions & 0 deletions test-data/unit/check-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -1161,3 +1161,43 @@ def decorator(x: str) -> Any: ...
@decorator(T) # E: Argument 1 to "decorator" has incompatible type "int"; expected "str"
class C[T]:
pass

[case testPEP695InvalidGenericOrProtocolBaseClass]
# mypy: enable-incomplete-feature=NewGenericSyntax
from typing import Generic, Protocol, TypeVar

S = TypeVar("S")

class C[T](Generic[T]): # E: Generic[...] base class is redundant
pass
class C2[T](Generic[S]): # E: Generic[...] base class is redundant
pass

a: C[int]
b: C2[int, str]

class P[T](Protocol[T]): # E: No arguments expected for "Protocol" base class
pass
class P2[T](Protocol[S]): # E: No arguments expected for "Protocol" base class
pass

[case testPEP695MixNewAndOldStyleGenerics]
# mypy: enable-incomplete-feature=NewGenericSyntax
from typing import TypeVar

S = TypeVar("S")
U = TypeVar("U")

def f[T](x: T, y: S) -> T | S: ... # E: All type parameters should be declared ("S" not declared)
def g[T](x: S, y: U) -> T | S | U: ... # E: All type parameters should be declared ("S", "U" not declared)

def h[S: int](x: S) -> S:
a: int = x
return x

class C[T]:
def m[X, S](self, x: S, y: U) -> X | S | U: ... # E: All type parameters should be declared ("U" not declared)
def m2(self, x: T, y: S) -> T | S: ...

class D[T](C[S]): # E: All type parameters should be declared ("S" not declared)
pass

0 comments on commit 7032f8c

Please sign in to comment.