diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index faf88287e511..fcef5fb5f40d 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -764,6 +764,10 @@ of the above sections. Note that :option:`--strict-equality-for-none ` only works in combination with :option:`--strict-equality `. +.. option:: --strict-overload-subtyping + + Require subtype overload order to match supertype overload order. + .. option:: --strict-bytes By default, mypy treats ``bytearray`` and ``memoryview`` as subtypes of ``bytes`` which diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index e9a195e2da77..98c73e49153c 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -864,6 +864,13 @@ section of the command line docs. Include ``None`` in strict equality checks (requires :confval:`strict_equality` to be activated). +.. confval:: strict_overload_subtyping + + :type: boolean + :default: False + + Require subtype overload order to match supertype overload order. + .. confval:: strict_bytes :type: boolean diff --git a/mypy/checker.py b/mypy/checker.py index 58f34e8b126c..1978a194ddb4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2502,7 +2502,7 @@ def check_override( # Use boolean variable to clarify code. fail = False op_method_wider_note = False - if not is_subtype(override, original, ignore_pos_arg_names=True): + if not is_subtype(override, original, ignore_pos_arg_names=True, options=self.options): fail = True elif isinstance(override, Overloaded) and self.is_forward_op_method(name): # Operator method overrides cannot extend the domain, as diff --git a/mypy/main.py b/mypy/main.py index bb915c8583f8..a75909f31d50 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -933,6 +933,14 @@ def add_invertible_flag( group=strictness_group, ) + add_invertible_flag( + "--strict-overload-subtyping", + default=False, + strict_flag=False, + help="Require subtype overload order to match supertype overload order.", + group=strictness_group, + ) + add_invertible_flag( "--strict-bytes", default=False, diff --git a/mypy/options.py b/mypy/options.py index 86e57790dcfe..baaba30a481f 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -57,6 +57,7 @@ class BuildType: "strict_concatenate", "strict_equality", "strict_equality_for_none", + "strict_overload_subtyping", "strict_optional", "warn_no_return", "warn_return_any", @@ -244,6 +245,9 @@ def __init__(self) -> None: # Extend the logic of `strict_equality` to comparisons with `None`. self.strict_equality_for_none = False + # Enforce strict ordering for overloads. + self.strict_overload_subtyping = False + # Disable treating bytearray and memoryview as subtypes of bytes self.strict_bytes = False diff --git a/mypy/subtypes.py b/mypy/subtypes.py index b8e8d5e3b79d..0e9382e06d81 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1007,7 +1007,13 @@ def visit_overloaded(self, left: Overloaded) -> bool: # Order matters: we need to make sure that the index of # this item is at least the index of the previous one. - if subtype_match and previous_match_left_index <= left_index: + strict_overload_subtyping = ( + self.options.strict_overload_subtyping if self.options else False + ) + if subtype_match and ( + (not strict_overload_subtyping) + or (previous_match_left_index <= left_index) + ): previous_match_left_index = left_index found_match = True matched_overloads.add(left_index) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 85a2264c2088..3f7eb6d35bfc 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -363,6 +363,7 @@ y = [] # E: Need type annotation for "y" (hint: "y: list[] = ...") [var- [builtins fixtures/list.pyi] [case testErrorCodeBadOverride] +# flags: --strict-overload-subtyping from typing import overload class A: diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 8616a1b6d165..722421b9c681 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -985,7 +985,65 @@ A() + '' # E: No overload variant of "__add__" of "A" matches argument type "str # N: def __add__(self, A, /) -> int \ # N: def __add__(self, int, /) -> int +[case testAllowOverrideOverloadSwapped] +# https://github.com/python/mypy/issues/20720 +from foo import * +[file foo.pyi] +from typing import overload + +def test_mutually_exclusive_types() -> None: + # note: int and str are mutually exclusive types (@disjoint_base) + class Parent: + @overload + def f(self, x: int) -> int: ... + @overload + def f(self, x: str) -> str: ... + class Child(Parent): + @overload + def f(self, x: str) -> str: ... + @overload + def f(self, x: int) -> int: ... + +def test_mutually_exclusive_signatures() -> None: + # the overload call-signatures are mutually exclusive, + # so swapping is safe even if intersections exist + class X: ... + class Y: ... + class A: ... + class B: ... + + class Parent: + @overload + def f(self, *, x: X) -> A: ... + @overload + def f(self, *, y: Y) -> B: ... + class Child(Parent): + @overload + def f(self, *, y: Y) -> B: ... + @overload + def f(self, *, x: X) -> A: ... + +def test_same_signature_and_return() -> None: + # swapping is safe if the return types are the same, even + # even if argument types overlap + + class X: ... + class Y: ... + + class Parent: + @overload + def f(self, x: X, /) -> None: ... + @overload + def f(self, y: Y, /) -> None: ... + class Child(Parent): + @overload + def f(self, y: Y, /) -> None: ... + @overload + def f(self, x: X, /) -> None: ... + + [case testOverrideOverloadSwapped] +# flags: --strict-overload-subtyping from foo import * [file foo.pyi] from typing import overload @@ -1003,6 +1061,7 @@ class Child(Parent): def f(self, x: int) -> int: ... [case testOverrideOverloadSwappedWithExtraVariants] +# flags: --strict-overload-subtyping from foo import * [file foo.pyi] from typing import overload @@ -1040,6 +1099,7 @@ class Child3(Parent): def f(self, x: bool) -> bool: ... [case testOverrideOverloadSwappedWithAdjustedVariants] +# flags: --strict-overload-subtyping from foo import * [file foo.pyi] from typing import overload