diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index b5081f113f91..6c1343a6e232 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -746,6 +746,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 77f952471007..ff3b3303192b 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -844,6 +844,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 d2633f0b2632..a1a74525f571 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2499,7 +2499,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 926e72515d95..94e099f709ef 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -929,6 +929,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 9bfbc5f68af8..bd74465162ac 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -55,6 +55,7 @@ class BuildType: "strict_concatenate", "strict_equality", "strict_equality_for_none", + "strict_overload_subtyping", "strict_optional", "warn_no_return", "warn_return_any", @@ -235,6 +236,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 350d57a7e4ad..74e6a3dcd914 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1002,7 +1002,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 8533dbc6c56c..e1396220a773 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -290,6 +290,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 5a445c818218..20da9f2e245d 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