Skip to content

Commit ff95659

Browse files
relax overload subtyping
1 parent e63e6e5 commit ff95659

File tree

8 files changed

+92
-2
lines changed

8 files changed

+92
-2
lines changed

docs/source/command_line.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,10 @@ of the above sections.
746746
Note that :option:`--strict-equality-for-none <mypy --strict-equality-for-none>`
747747
only works in combination with :option:`--strict-equality <mypy --strict-equality>`.
748748

749+
.. option:: --strict-overload-subtyping
750+
751+
Require subtype overload order to match supertype overload order.
752+
749753
.. option:: --strict-bytes
750754

751755
By default, mypy treats ``bytearray`` and ``memoryview`` as subtypes of ``bytes`` which

docs/source/config_file.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,13 @@ section of the command line docs.
844844
Include ``None`` in strict equality checks (requires :confval:`strict_equality`
845845
to be activated).
846846

847+
.. confval:: strict_overload_subtyping
848+
849+
:type: boolean
850+
:default: False
851+
852+
Require subtype overload order to match supertype overload order.
853+
847854
.. confval:: strict_bytes
848855

849856
:type: boolean

mypy/checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2499,7 +2499,7 @@ def check_override(
24992499
# Use boolean variable to clarify code.
25002500
fail = False
25012501
op_method_wider_note = False
2502-
if not is_subtype(override, original, ignore_pos_arg_names=True):
2502+
if not is_subtype(override, original, ignore_pos_arg_names=True, options=self.options):
25032503
fail = True
25042504
elif isinstance(override, Overloaded) and self.is_forward_op_method(name):
25052505
# Operator method overrides cannot extend the domain, as

mypy/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,14 @@ def add_invertible_flag(
929929
group=strictness_group,
930930
)
931931

932+
add_invertible_flag(
933+
"--strict-overload-subtyping",
934+
default=False,
935+
strict_flag=False,
936+
help="Require subtype overload order to match supertype overload order.",
937+
group=strictness_group,
938+
)
939+
932940
add_invertible_flag(
933941
"--strict-bytes",
934942
default=False,

mypy/options.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class BuildType:
5555
"strict_concatenate",
5656
"strict_equality",
5757
"strict_equality_for_none",
58+
"strict_overload_subtyping",
5859
"strict_optional",
5960
"warn_no_return",
6061
"warn_return_any",
@@ -235,6 +236,9 @@ def __init__(self) -> None:
235236
# Extend the logic of `strict_equality` to comparisons with `None`.
236237
self.strict_equality_for_none = False
237238

239+
# Enforce strict ordering for overloads.
240+
self.strict_overload_subtyping = False
241+
238242
# Disable treating bytearray and memoryview as subtypes of bytes
239243
self.strict_bytes = False
240244

mypy/subtypes.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,13 @@ def visit_overloaded(self, left: Overloaded) -> bool:
10021002

10031003
# Order matters: we need to make sure that the index of
10041004
# this item is at least the index of the previous one.
1005-
if subtype_match and previous_match_left_index <= left_index:
1005+
strict_overload_subtyping = (
1006+
self.options.strict_overload_subtyping if self.options else False
1007+
)
1008+
if subtype_match and (
1009+
(not strict_overload_subtyping)
1010+
or (previous_match_left_index <= left_index)
1011+
):
10061012
previous_match_left_index = left_index
10071013
found_match = True
10081014
matched_overloads.add(left_index)

test-data/unit/check-errorcodes.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ y = [] # E: Need type annotation for "y" (hint: "y: list[<type>] = ...") [var-
290290
[builtins fixtures/list.pyi]
291291

292292
[case testErrorCodeBadOverride]
293+
# flags: --strict-overload-subtyping
293294
from typing import overload
294295

295296
class A:

test-data/unit/check-overloading.test

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,65 @@ A() + '' # E: No overload variant of "__add__" of "A" matches argument type "str
985985
# N: def __add__(self, A, /) -> int \
986986
# N: def __add__(self, int, /) -> int
987987

988+
[case testAllowOverrideOverloadSwapped]
989+
# https://github.com/python/mypy/issues/20720
990+
from foo import *
991+
[file foo.pyi]
992+
from typing import overload
993+
994+
def test_mutually_exclusive_types() -> None:
995+
# note: int and str are mutually exclusive types (@disjoint_base)
996+
class Parent:
997+
@overload
998+
def f(self, x: int) -> int: ...
999+
@overload
1000+
def f(self, x: str) -> str: ...
1001+
class Child(Parent):
1002+
@overload
1003+
def f(self, x: str) -> str: ...
1004+
@overload
1005+
def f(self, x: int) -> int: ...
1006+
1007+
def test_mutually_exclusive_signatures() -> None:
1008+
# the overload call-signatures are mutually exclusive,
1009+
# so swapping is safe even if intersections exist
1010+
class X: ...
1011+
class Y: ...
1012+
class A: ...
1013+
class B: ...
1014+
1015+
class Parent:
1016+
@overload
1017+
def f(self, *, x: X) -> A: ...
1018+
@overload
1019+
def f(self, *, y: Y) -> B: ...
1020+
class Child(Parent):
1021+
@overload
1022+
def f(self, *, y: Y) -> B: ...
1023+
@overload
1024+
def f(self, *, x: X) -> A: ...
1025+
1026+
def test_same_signature_and_return() -> None:
1027+
# swapping is safe if the return types are the same, even
1028+
# even if argument types overlap
1029+
1030+
class X: ...
1031+
class Y: ...
1032+
1033+
class Parent:
1034+
@overload
1035+
def f(self, x: X, /) -> None: ...
1036+
@overload
1037+
def f(self, y: Y, /) -> None: ...
1038+
class Child(Parent):
1039+
@overload
1040+
def f(self, y: Y, /) -> None: ...
1041+
@overload
1042+
def f(self, x: X, /) -> None: ...
1043+
1044+
9881045
[case testOverrideOverloadSwapped]
1046+
# flags: --strict-overload-subtyping
9891047
from foo import *
9901048
[file foo.pyi]
9911049
from typing import overload
@@ -1003,6 +1061,7 @@ class Child(Parent):
10031061
def f(self, x: int) -> int: ...
10041062

10051063
[case testOverrideOverloadSwappedWithExtraVariants]
1064+
# flags: --strict-overload-subtyping
10061065
from foo import *
10071066
[file foo.pyi]
10081067
from typing import overload
@@ -1040,6 +1099,7 @@ class Child3(Parent):
10401099
def f(self, x: bool) -> bool: ...
10411100

10421101
[case testOverrideOverloadSwappedWithAdjustedVariants]
1102+
# flags: --strict-overload-subtyping
10431103
from foo import *
10441104
[file foo.pyi]
10451105
from typing import overload

0 commit comments

Comments
 (0)