Skip to content

Commit

Permalink
Merge branch 'python:master' into c-a
Browse files Browse the repository at this point in the history
  • Loading branch information
clintaire authored Sep 19, 2024
2 parents e453262 + 4554bd0 commit d0be99c
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 20 deletions.
28 changes: 28 additions & 0 deletions docs/source/error_code_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,34 @@ types you expect.

See :ref:`overloading <function-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]
Expand Down
8 changes: 8 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 27 additions & 12 deletions mypy/exprtotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,6 +24,7 @@
RefExpr,
StarExpr,
StrExpr,
SymbolTableNode,
TupleExpr,
UnaryExpr,
get_member_expr_fullname,
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
25 changes: 18 additions & 7 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit d0be99c

Please sign in to comment.