diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 85c8d437a856..ad73bc999f00 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -1149,6 +1149,34 @@ types you expect. See :ref:`overloading ` for more explanation. + +.. _code-overload-cannot-match: + +Check for overload signatures that cannot match [overload-cannot-match] +-------------------------------------------------------------------------- + +Warn if an ``@overload`` variant can never be matched, because an earlier +overload has a wider signature. For example, this can happen if the two +overloads accept the same parameters and each parameter on the first overload +has the same type or a wider type than the corresponding parameter on the second +overload. + +Example: + +.. code-block:: python + + from typing import overload, Union + + @overload + def process(response1: object, response2: object) -> object: + ... + @overload + def process(response1: int, response2: int) -> int: # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match] + ... + + def process(response1: object, response2: object) -> object: + return response1 + response2 + .. _code-annotation-unchecked: Notify about an annotation in an unchecked function [annotation-unchecked] diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 6e8763264ddd..ad061b161af1 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -273,6 +273,14 @@ def __hash__(self) -> int: # This is a catch-all for remaining uncategorized errors. MISC: Final[ErrorCode] = ErrorCode("misc", "Miscellaneous other checks", "General") +OVERLOAD_CANNOT_MATCH: Final[ErrorCode] = ErrorCode( + "overload-cannot-match", + "Warn if an @overload signature can never be matched", + "General", + sub_code_of=MISC, +) + + OVERLOAD_OVERLAP: Final[ErrorCode] = ErrorCode( "overload-overlap", "Warn if multiple @overload variants overlap in unsafe ways", diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 92316d11926d..c7df851668be 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -2,12 +2,15 @@ from __future__ import annotations +from typing import Callable + from mypy.fastparse import parse_type_string from mypy.nodes import ( MISSING_FALLBACK, BytesExpr, CallExpr, ComplexExpr, + Context, DictExpr, EllipsisExpr, Expression, @@ -21,6 +24,7 @@ RefExpr, StarExpr, StrExpr, + SymbolTableNode, TupleExpr, UnaryExpr, get_member_expr_fullname, @@ -63,12 +67,16 @@ def expr_to_unanalyzed_type( allow_new_syntax: bool = False, _parent: Expression | None = None, allow_unpack: bool = False, + lookup_qualified: Callable[[str, Context], SymbolTableNode | None] | None = None, ) -> ProperType: """Translate an expression to the corresponding type. The result is not semantically analyzed. It can be UnboundType or TypeList. Raise TypeTranslationError if the expression cannot represent a type. + If lookup_qualified is not provided, the expression is expected to be semantically + analyzed. + If allow_new_syntax is True, allow all type syntax independent of the target Python version (used in stubs). @@ -101,19 +109,26 @@ def expr_to_unanalyzed_type( else: args = [expr.index] - if isinstance(expr.base, RefExpr) and expr.base.fullname in ANNOTATED_TYPE_NAMES: - # TODO: this is not the optimal solution as we are basically getting rid - # of the Annotation definition and only returning the type information, - # losing all the annotations. + if isinstance(expr.base, RefExpr): + # Check if the type is Annotated[...]. For this we need the fullname, + # which must be looked up if the expression hasn't been semantically analyzed. + base_fullname = None + if lookup_qualified is not None: + sym = lookup_qualified(base.name, expr) + if sym and sym.node: + base_fullname = sym.node.fullname + else: + base_fullname = expr.base.fullname - return expr_to_unanalyzed_type(args[0], options, allow_new_syntax, expr) - else: - base.args = tuple( - expr_to_unanalyzed_type( - arg, options, allow_new_syntax, expr, allow_unpack=True - ) - for arg in args - ) + if base_fullname is not None and base_fullname in ANNOTATED_TYPE_NAMES: + # TODO: this is not the optimal solution as we are basically getting rid + # of the Annotation definition and only returning the type information, + # losing all the annotations. + return expr_to_unanalyzed_type(args[0], options, allow_new_syntax, expr) + base.args = tuple( + expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr, allow_unpack=True) + for arg in args + ) if not base.args: base.empty_tuple_index = True return base diff --git a/mypy/messages.py b/mypy/messages.py index d5fa21273d6c..736871d235e2 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1653,6 +1653,7 @@ def overloaded_signature_will_never_match( index1=index1, index2=index2 ), context, + code=codes.OVERLOAD_CANNOT_MATCH, ) def overloaded_signatures_typevar_specific(self, index: int, context: Context) -> None: diff --git a/mypy/semanal.py b/mypy/semanal.py index d0dc901a49a2..a6fbdd21f74c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -824,6 +824,11 @@ def file_context( self.num_incomplete_refs = 0 if active_type: + enclosing_fullname = active_type.fullname.rsplit(".", 1)[0] + if "." in enclosing_fullname: + enclosing_node = self.lookup_fully_qualified_or_none(enclosing_fullname) + if enclosing_node and isinstance(enclosing_node.node, TypeInfo): + self._type = enclosing_node.node self.push_type_args(active_type.defn.type_args, active_type.defn) self.incomplete_type_stack.append(False) scope.enter_class(active_type) @@ -3744,7 +3749,9 @@ def analyze_alias( dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic()) global_scope = not self.type and not self.function_stack try: - typ = expr_to_unanalyzed_type(rvalue, self.options, self.is_stub_file) + typ = expr_to_unanalyzed_type( + rvalue, self.options, self.is_stub_file, lookup_qualified=self.lookup_qualified + ) except TypeTranslationError: self.fail( "Invalid type alias: expression is not a valid type", rvalue, code=codes.VALID_TYPE diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 649cbae4c831..5c4471cc5b62 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2004,18 +2004,25 @@ def infer_variance(info: TypeInfo, i: int) -> bool: tvar = info.defn.type_vars[i] self_type = fill_typevars(info) for member in all_non_object_members(info): - if member in ("__init__", "__new__"): + # __mypy-replace is an implementation detail of the dataclass plugin + if member in ("__init__", "__new__", "__mypy-replace"): continue - node = info[member].node - if isinstance(node, Var) and node.type is None: - tv.variance = VARIANCE_NOT_READY - return False + if isinstance(self_type, TupleType): self_type = mypy.typeops.tuple_fallback(self_type) - flags = get_member_flags(member, self_type) - typ = find_member(member, self_type, self_type) settable = IS_SETTABLE in flags + + node = info[member].node + if isinstance(node, Var): + if node.type is None: + tv.variance = VARIANCE_NOT_READY + return False + if has_underscore_prefix(member): + # Special case to avoid false positives (and to pass conformance tests) + settable = False + + typ = find_member(member, self_type, self_type) if typ: typ2 = expand_type(typ, {tvar.id: object_type}) if not is_subtype(typ, typ2): @@ -2036,6 +2043,10 @@ def infer_variance(info: TypeInfo, i: int) -> bool: return True +def has_underscore_prefix(name: str) -> bool: + return name.startswith("_") and not (name.startswith("__") and name.endswith("__")) + + def infer_class_variances(info: TypeInfo) -> bool: if not info.defn.type_args: return True diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index cca13347dd86..10cc145d0c70 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1222,3 +1222,17 @@ def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of inpu pass [builtins fixtures/tuple.pyi] + + +[case testOverloadedFunctionSignature] +from typing import overload, Union + +@overload +def process(response1: float,response2: float) -> float: + ... +@overload +def process(response1: int,response2: int) -> int: # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match] + ... + +def process(response1,response2)-> Union[float,int]: + return response1 + response2 diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index a3f4c87120cd..d0a39f7e56a6 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -249,6 +249,25 @@ if int(): if int(): f = e +[case testPEP695InferVarianceInFrozenDataclass] +# flags: --enable-incomplete-feature=NewGenericSyntax +from dataclasses import dataclass + +@dataclass(frozen=True) +class Covariant[T]: + x: T + +cov1: Covariant[float] = Covariant[int](1) +cov2: Covariant[int] = Covariant[float](1) # E: Incompatible types in assignment (expression has type "Covariant[float]", variable has type "Covariant[int]") + +@dataclass(frozen=True) +class Invariant[T]: + x: list[T] + +inv1: Invariant[float] = Invariant[int]([1]) # E: Incompatible types in assignment (expression has type "Invariant[int]", variable has type "Invariant[float]") +inv2: Invariant[int] = Invariant[float]([1]) # E: Incompatible types in assignment (expression has type "Invariant[float]", variable has type "Invariant[int]") +[builtins fixtures/tuple.pyi] + [case testPEP695InferVarianceCalculateOnDemand] # flags: --enable-incomplete-feature=NewGenericSyntax @@ -342,6 +361,69 @@ class Invariant[T]: reveal_type(c(a1, a2)) # N: Revealed type is "Never" +[case testPEP695InferVarianceUnderscorePrefix] +# flags: --enable-incomplete-feature=NewGenericSyntax + +class Covariant1[T]: + def __init__(self, x: T) -> None: + self._x = x + + @property + def x(self) -> T: + return self._x + +co1_1: Covariant1[float] = Covariant1[int](1) +co1_2: Covariant1[int] = Covariant1[float](1) # E: Incompatible types in assignment (expression has type "Covariant1[float]", variable has type "Covariant1[int]") + +class Covariant2[T]: + def __init__(self, x: T) -> None: + self.__foo_bar = x + + @property + def x(self) -> T: + return self.__foo_bar + +co2_1: Covariant2[float] = Covariant2[int](1) +co2_2: Covariant2[int] = Covariant2[float](1) # E: Incompatible types in assignment (expression has type "Covariant2[float]", variable has type "Covariant2[int]") + +class Invariant1[T]: + def __init__(self, x: T) -> None: + self._x = x + + # Methods behave differently from attributes + def _f(self, x: T) -> None: ... + + @property + def x(self) -> T: + return self._x + +inv1_1: Invariant1[float] = Invariant1[int](1) # E: Incompatible types in assignment (expression has type "Invariant1[int]", variable has type "Invariant1[float]") +inv1_2: Invariant1[int] = Invariant1[float](1) # E: Incompatible types in assignment (expression has type "Invariant1[float]", variable has type "Invariant1[int]") + +class Invariant2[T]: + def __init__(self, x: T) -> None: + # Dunders are special + self.__x__ = x + + @property + def x(self) -> T: + return self.__x__ + +inv2_1: Invariant2[float] = Invariant2[int](1) # E: Incompatible types in assignment (expression has type "Invariant2[int]", variable has type "Invariant2[float]") +inv2_2: Invariant2[int] = Invariant2[float](1) # E: Incompatible types in assignment (expression has type "Invariant2[float]", variable has type "Invariant2[int]") + +class Invariant3[T]: + def __init__(self, x: T) -> None: + self._x = Invariant1(x) + + @property + def x(self) -> T: + return self._x._x + +inv3_1: Invariant3[float] = Invariant3[int](1) # E: Incompatible types in assignment (expression has type "Invariant3[int]", variable has type "Invariant3[float]") +inv3_2: Invariant3[int] = Invariant3[float](1) # E: Incompatible types in assignment (expression has type "Invariant3[float]", variable has type "Invariant3[int]") +[builtins fixtures/property.pyi] + [case testPEP695InheritInvariant] # flags: --enable-incomplete-feature=NewGenericSyntax @@ -1713,3 +1795,91 @@ type XNested = (1 + (yield 1)) # E: Yield expression cannot be used within a ty 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 + +[case testPEP695TypeAliasAndAnnotated] +# flags: --enable-incomplete-feature=NewGenericSyntax +from typing_extensions import Annotated, Annotated as _Annotated +import typing_extensions as t + +def ann(*args): ... + +type A = Annotated[int, ann()] +type B = Annotated[int | str, ann((1, 2))] +type C = _Annotated[int, ann()] +type D = t.Annotated[str, ann()] + +x: A +y: B +z: C +zz: D +reveal_type(x) # N: Revealed type is "builtins.int" +reveal_type(y) # N: Revealed type is "Union[builtins.int, builtins.str]" +reveal_type(z) # N: Revealed type is "builtins.int" +reveal_type(zz) # N: Revealed type is "builtins.str" +[builtins fixtures/tuple.pyi] + +[case testPEP695NestedGenericClass1] +# flags: --enable-incomplete-feature=NewGenericSyntax +class C[T]: + def f(self) -> T: ... + +class A: + class B[Q]: + def __init__(self, a: Q) -> None: + self.a = a + + def f(self) -> Q: + return self.a + + def g(self, x: Q) -> None: ... + + b: B[str] + +x: A.B[int] +x.g("x") # E: Argument 1 to "g" of "B" has incompatible type "str"; expected "int" +reveal_type(x.a) # N: Revealed type is "builtins.int" +reveal_type(x) # N: Revealed type is "__main__.A.B[builtins.int]" +reveal_type(A.b) # N: Revealed type is "__main__.A.B[builtins.str]" + +[case testPEP695NestedGenericClass2] +# flags: --enable-incomplete-feature=NewGenericSyntax +class A: + def m(self) -> None: + class B[T]: + def f(self) -> T: ... + x: B[int] + reveal_type(x.f()) # N: Revealed type is "builtins.int" + self.a = B[str]() + +reveal_type(A().a) # N: Revealed type is "__main__.B@4[builtins.str]" +reveal_type(A().a.f()) # N: Revealed type is "builtins.str" + +[case testPEP695NestedGenericClass3] +# flags: --enable-incomplete-feature=NewGenericSyntax +class C[T]: + def f(self) -> T: ... + class D[S]: + x: T # E: Name "T" is not defined + def g(self) -> S: ... + +a: C[int] +reveal_type(a.f()) # N: Revealed type is "builtins.int" +b: C.D[str] +reveal_type(b.g()) # N: Revealed type is "builtins.str" + +class E[T]: + class F[T]: # E: "T" already defined as a type parameter + x: T + +c: E.F[int] + +[case testPEP695NestedGenericClass4] +# flags: --enable-incomplete-feature=NewGenericSyntax +class A: + class B[T]: + def __get__(self, instance: A, owner: type[A]) -> T: + return None # E: Incompatible return value type (got "None", expected "T") + f = B[int]() + +a = A() +v = a.f diff --git a/test-data/unit/fine-grained-python312.test b/test-data/unit/fine-grained-python312.test index 3970c8cacfbf..80e4e4ca3bd8 100644 --- a/test-data/unit/fine-grained-python312.test +++ b/test-data/unit/fine-grained-python312.test @@ -80,3 +80,23 @@ from builtins import tuple as B == main:4: error: Incompatible types in assignment (expression has type "int", variable has type "tuple[int, str]") main:5: error: Incompatible types in assignment (expression has type "str", variable has type "tuple[int, str]") + +[case testPEP695NestedGenericClassMethodUpdated] +# flags: --enable-incomplete-feature=NewGenericSyntax +from a import f + +class C: + class D[T]: + x: T + def m(self) -> T: + f() + return self.x + +[file a.py] +def f() -> None: pass + +[file a.py.2] +def f(x: int) -> None: pass +[out] +== +main:8: error: Missing positional argument "x" in call to "f"