Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NoMatch for invalid signatures in overloads #18344

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,16 @@ def check_call_expr_with_callee_type(
proper_callee = get_proper_type(callee_type)
if isinstance(e.callee, (NameExpr, MemberExpr)):
self.chk.warn_deprecated_overload_item(e.callee.node, e, target=callee_type)
if (
isinstance((p_type := get_proper_type(ret_type)), AnyType)
and p_type.type_of_any == TypeOfAny.no_match
and isinstance(proper_callee, CallableType)
):
self.chk.fail(
f'No matching overload found for "{proper_callee.name}"',
context=e,
code=codes.CALL_OVERLOAD,
)
if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType):
# Cache it for find_isinstance_check()
if proper_callee.type_guard is not None:
Expand Down
18 changes: 17 additions & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
FINAL_TYPE_NAMES,
LITERAL_TYPE_NAMES,
NEVER_NAMES,
NO_MATCH_NAMES,
TYPE_ALIAS_NAMES,
AnyType,
BoolTypeQuery,
Expand Down Expand Up @@ -227,6 +228,7 @@ def __init__(
allow_typed_dict_special_forms: bool = False,
allow_param_spec_literals: bool = False,
allow_unpack: bool = False,
allow_no_match: bool = False,
report_invalid_types: bool = True,
prohibit_self_type: str | None = None,
prohibit_special_class_field_types: str | None = None,
Expand Down Expand Up @@ -282,6 +284,7 @@ def __init__(
self.allow_type_any = allow_type_any
self.allow_type_var_tuple = False
self.allow_unpack = allow_unpack
self.allow_no_match = allow_no_match

def lookup_qualified(
self, name: str, ctx: Context, suppress_errors: bool = False
Expand Down Expand Up @@ -694,6 +697,15 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
return self.anal_type(t.args[0])
elif fullname in NEVER_NAMES:
return UninhabitedType()
elif fullname in NO_MATCH_NAMES:
if not self.allow_no_match:
self.fail(
"NoMatch can only be used as an overload return annotation",
t,
code=codes.VALID_TYPE,
)
return AnyType(TypeOfAny.from_error)
return AnyType(TypeOfAny.no_match)
elif fullname in LITERAL_TYPE_NAMES:
return self.analyze_literal_type(t)
elif fullname in ANNOTATED_TYPE_NAMES:
Expand Down Expand Up @@ -1158,7 +1170,7 @@ def visit_callable_type(
arg_types=arg_types,
arg_kinds=arg_kinds,
arg_names=arg_names,
ret_type=self.anal_type(t.ret_type, nested=nested),
ret_type=self.anal_type(t.ret_type, nested=nested, allow_no_match=True),
# If the fallback isn't filled in yet,
# its type will be the falsey FakeInfo
fallback=(t.fallback if t.fallback.type else self.named_type("builtins.function")),
Expand Down Expand Up @@ -1877,6 +1889,7 @@ def anal_type(
allow_unpack: bool = False,
allow_ellipsis: bool = False,
allow_typed_dict_special_forms: bool = False,
allow_no_match: bool = False,
) -> Type:
if nested:
self.nesting_level += 1
Expand All @@ -1886,6 +1899,8 @@ def anal_type(
self.allow_ellipsis = allow_ellipsis
old_allow_unpack = self.allow_unpack
self.allow_unpack = allow_unpack
old_no_match = self.allow_no_match
self.allow_no_match = allow_no_match
try:
analyzed = t.accept(self)
finally:
Expand All @@ -1894,6 +1909,7 @@ def anal_type(
self.allow_typed_dict_special_forms = old_allow_typed_dict_special_forms
self.allow_ellipsis = old_allow_ellipsis
self.allow_unpack = old_allow_unpack
self.allow_no_match = old_no_match
if (
not allow_param_spec
and isinstance(analyzed, ParamSpecType)
Expand Down
4 changes: 4 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@
# Supported @override decorator names.
OVERRIDE_DECORATOR_NAMES: Final = ("typing.override", "typing_extensions.override")

NO_MATCH_NAMES: Final = ("typing.NoMatch", "typing_extensions.NoMatch")

# A placeholder used for Bogus[...] parameters
_dummy: Final[Any] = object()

Expand Down Expand Up @@ -209,6 +211,8 @@ class TypeOfAny:
# used to ignore Anys inserted by the suggestion engine when
# generating constraints.
suggestion_engine: Final = 9
# Does this Any come from NoMatch overload return annotation?
no_match: Final = 10


def deserialize_type(data: JsonDict | str) -> Type:
Expand Down
2 changes: 2 additions & 0 deletions mypy/typeshed/stdlib/typing_extensions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,8 @@ else:
ReadOnly: _SpecialForm
TypeIs: _SpecialForm

NoMatch: _SpecialForm

class Doc:
documentation: str
def __init__(self, documentation: str, /) -> None: ...
Expand Down
18 changes: 18 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -6768,3 +6768,21 @@ class D(Generic[T]):
a: D[str] # E: Type argument "str" of "D" must be a subtype of "C"
reveal_type(a.f(1)) # N: Revealed type is "builtins.int"
reveal_type(a.f("x")) # N: Revealed type is "builtins.str"

[case testOverloadNoMatch]
from typing import Sequence, overload
from typing_extensions import NoMatch

@overload
def f1(args: str) -> NoMatch: ...
@overload
def f1(arg: Sequence[str]) -> int: ...
def f1(arg): ...

reveal_type(f1(["Hello", "World"])) # N: Revealed type is "builtins.int"
reveal_type(f1("Hello World")) # E: No matching overload found for "f1" \
# N: Revealed type is "Any"

y: NoMatch # E: NoMatch can only be used as an overload return annotation
def f2(arg: NoMatch) -> int: ... # E: NoMatch can only be used as an overload return annotation
[builtins fixtures/tuple.pyi]
2 changes: 2 additions & 0 deletions test-data/unit/lib-stub/typing_extensions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ ReadOnly: _SpecialForm

Self: _SpecialForm

NoMatch: _SpecialForm

@final
class TypeAliasType:
def __init__(
Expand Down
Loading