diff --git a/CHANGELOG.md b/CHANGELOG.md index 8883ce517da74..365ad2fce3e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ - No longer provide mypyc-accelerated wheels for macOS x86_64 [mypyc-wheels #119](https://github.com/mypyc/mypy_mypyc-wheels/pull/119) +### Documentation Updates + +- Clarify that nested `Any` in an overload argument is ambiguous, the same as a top-level `Any` (PR [21897](https://github.com/python/mypy/pull/21897)) + +### Other Notable Fixes and Improvements + +- Treat type aliases that expand to `Any` (or a type containing `Any`) as ambiguous in overload matching, matching a written `Any` (PR [21897](https://github.com/python/mypy/pull/21897)) + ## Mypy 2.3 We've just uploaded mypy 2.3.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 444753757aad9..3a24e4a5f2d63 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -394,8 +394,10 @@ will have an inferred type of ``float``. The implementer is responsible for making sure ``summarize`` breaks ties in the same way at runtime. However, there are two exceptions to the "pick the first match" rule. -First, if multiple variants match due to an argument being of type -``Any``, mypy will make the inferred type also be ``Any``: +First, if multiple variants match because an argument has type ``Any`` +or *contains* ``Any`` (for example ``list[Any]`` or ``Sequence[Any]``, +including via a type alias), mypy will make the inferred type also be +``Any``: .. code-block:: python @@ -404,6 +406,18 @@ First, if multiple variants match due to an argument being of type # output2 is of type 'Any' output2 = summarize(dynamic_var) + nested_any: list[Any] = some_dynamic_function() + + # output2_nested is also 'Any'. A list[Any] is not "really" a list[int]: + # it can hold strings, and list[str] selects the other overload. The + # call is therefore ambiguous in the same way as a top-level Any. + output2_nested = summarize(nested_any) + + Nested = list[Any] + aliased: Nested = some_dynamic_function() + # output2_alias is also 'Any': a type alias does not hide the nested Any. + output2_alias = summarize(aliased) + Second, if multiple variants match due to one or more of the arguments being a union, mypy will make the inferred type be the union of the matching variant returns: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 172d44555b946..0c64ab270d65b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6638,9 +6638,23 @@ class HasAnyType(types.BoolTypeQuery): def __init__(self, ignore_in_type_obj: bool) -> None: super().__init__(types.ANY_STRATEGY) self.ignore_in_type_obj = ignore_in_type_obj + # Alias targets store a written `Any` as TypeOfAny.special_form. + # Count those as real Any while expanding aliases, so Nested = Sequence[Any] + # matches a direct Sequence[Any] for overload ambiguity. + self.count_alias_special_form_any = False def visit_any(self, t: AnyType) -> bool: - return t.type_of_any != TypeOfAny.special_form # special forms are not real Any types + if t.type_of_any == TypeOfAny.special_form: + return self.count_alias_special_form_any + return True + + def visit_type_alias_type(self, t: TypeAliasType) -> bool: + old = self.count_alias_special_form_any + self.count_alias_special_form_any = True + try: + return super().visit_type_alias_type(t) + finally: + self.count_alias_special_form_any = old def visit_callable_type(self, t: CallableType) -> bool: if self.ignore_in_type_obj and t.is_type_obj(): @@ -6855,6 +6869,13 @@ def any_causes_overload_ambiguity( ) -> bool: """May an argument containing 'Any' cause ambiguous result type on call to overloaded function? + This includes nested Any, such as Sequence[Any] or list[Any], not just a + top-level Any, and the same types reached through a type alias. A nested + Any can materialize as different concrete types, so if several overloads + remain and their return types differ, the result is Any. + (If every materialization matched an earlier overload, the spec would drop + later ones; this helper over-approximates that case as ambiguous.) + Note that this sometimes returns True even if there is no ambiguity, since a correct implementation would be complex (and the call would be imprecisely typed due to Any types anyway). diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 721ca52f59eed..2067d98938ddb 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -1906,6 +1906,240 @@ a: Any reveal_type(f(a)) # N: Revealed type is "def (*Any, **Any) -> Any" reveal_type(f(a)(a)) # N: Revealed type is "Any" +[case testOverloadNestedAnyInGenericArgumentIsAmbiguous] +# https://github.com/python/mypy/issues/21768 +# Nested Any is treated like top-level Any for overload matching. +# Sequence[Any] can materialize as Sequence[int] (first overload) or as +# another sequence type (only the object overload), so the result is Any. +from typing import Any, Mapping, Sequence, overload + +@overload +def f(s: Sequence[int]) -> int: ... +@overload +def f(s: object) -> object: ... +def f(s): pass + +concrete: Sequence[int] +nested_any: Sequence[Any] +top_level_any: Any +reveal_type(f(concrete)) # N: Revealed type is "builtins.int" +reveal_type(f(nested_any)) # N: Revealed type is "Any" +reveal_type(f(top_level_any)) # N: Revealed type is "Any" + +@overload +def g(s: Mapping[str, int]) -> int: ... +@overload +def g(s: object) -> object: ... +def g(s): pass + +mapping_any: Mapping[str, Any] +reveal_type(g(mapping_any)) # N: Revealed type is "Any" + +[case testOverloadNestedAnyUnambiguousWhenReturnsMatch] +# If remaining overloads agree on the return type, nested Any is not ambiguous. +from typing import Any, Sequence, overload + +@overload +def f(s: Sequence[int]) -> str: ... +@overload +def f(s: object) -> str: ... +def f(s): pass + +nested_any: Sequence[Any] +reveal_type(f(nested_any)) # N: Revealed type is "builtins.str" + +[case testOverloadNestedAnyDoesNotForceAnyWhenOtherArgSelects] +# Nested Any does not collapse the result when it does not itself cause +# multiple overloads with differing returns to remain. +from typing import Any, Sequence, overload + +@overload +def f(x: Sequence[int], y: int) -> int: ... +@overload +def f(x: Sequence[int], y: str) -> str: ... +def f(x, y): pass + +nested_any: Sequence[Any] +reveal_type(f(nested_any, 1)) # N: Revealed type is "builtins.int" +reveal_type(f(nested_any, "x")) # N: Revealed type is "builtins.str" +a: Any +reveal_type(f(nested_any, a)) # N: Revealed type is "Any" + +[case testOverloadNestedAnyWithIncompatibleOverlappingReturns] +# Issue 21768's original pair also has unsafely overlapping returns. +# The overlap error is separate; the call-site result is still Any. +from typing import Any, Sequence, overload + +@overload +def f(s: Sequence[int]) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(s: object) -> int: ... +def f(s): pass + +concrete: Sequence[int] +nested_any: Sequence[Any] +reveal_type(f(concrete)) # N: Revealed type is "builtins.str" +reveal_type(f(nested_any)) # N: Revealed type is "Any" + +[case testOverloadNestedAnyInhabitationCounterexample] +# Picking the Sequence[int] overload for Sequence[Any] is unsound: +# a Sequence[str] inhabits Sequence[Any], and that call takes the object +# overload (int), not str. See typing spec overload call evaluation step 5. +from typing import Any, Sequence, overload + +@overload +def f(s: Sequence[int]) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(s: object) -> int: ... +def f(s): pass + +as_str: Sequence[str] +nested_any: Sequence[Any] = as_str +reveal_type(f(as_str)) # N: Revealed type is "builtins.int" +reveal_type(f(nested_any)) # N: Revealed type is "Any" + +[case testOverloadNestedAnyOnlyOneOverloadMatches] +# Nested Any is not a blunt "always Any" switch. If only one overload +# is compatible, that overload wins even when the argument contains Any. +from typing import Any, Sequence, overload + +@overload +def f(s: Sequence[int]) -> str: ... +@overload +def f(s: str) -> int: ... +def f(s): pass + +nested_any: Sequence[Any] +reveal_type(f(nested_any)) # N: Revealed type is "builtins.str" +reveal_type(f("x")) # N: Revealed type is "builtins.int" + +[case testOverloadNestedAnySpecExampleThreeOverloads] +# Spec step-5 example: every materialization of list[Any] matches the +# first or second overload, so the Any fallback is dropped. The first +# two remain with different returns, so the call is still Any. +# Picking the first overload here would be the #21768 "fix" and is wrong. +from typing import Any, overload + +@overload +def example(x: list[int]) -> int: ... +@overload +def example(x: list[Any]) -> str: ... +@overload +def example(x: Any) -> bytes: ... +def example(x): pass + +a: list[Any] +reveal_type(example(a)) # N: Revealed type is "Any" + +[case testOverloadNestedAnyMatchesExistingListMatrix] +# Already locked in by testOverloadsUsingAny: invariant list[int]/list[str] +# with a list[Any] argument is Any. Repeated here next to the #21768 cases +# so that matrix cannot drift from the Sequence[Any] rule. +from typing import Any, List, overload + +@overload +def foo(x: List[int]) -> int: ... +@overload +def foo(x: List[str]) -> str: ... +def foo(x): pass + +c: List[Any] +reveal_type(foo(c)) # N: Revealed type is "Any" +[builtins fixtures/list.pyi] + +[case testOverloadNestedAnyInDictValues] +from typing import Any, overload + +@overload +def f(s: dict[str, int]) -> int: ... +@overload +def f(s: object) -> object: ... +def f(s): pass + +d: dict[str, Any] +reveal_type(f(d)) # N: Revealed type is "Any" +[builtins fixtures/dict.pyi] + +[case testOverloadNestedAnyInFixedTuple] +from typing import Any, overload + +@overload +def g(s: tuple[int, int]) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def g(s: object) -> int: ... +def g(s): pass + +t: tuple[Any, int] +reveal_type(g(t)) # N: Revealed type is "Any" +[builtins fixtures/tuple.pyi] + +[case testOverloadNestedAnyOnMethod] +from typing import Any, Sequence, overload + +class Box: + @overload + def get(self, key: Sequence[int]) -> int: ... + @overload + def get(self, key: object) -> object: ... + def get(self, key): pass + +nested_any: Sequence[Any] +reveal_type(Box().get(nested_any)) # N: Revealed type is "Any" + +[case testOverloadNestedAnyViaTypeAliasIsAmbiguous] +# A type alias must not hide nested Any from overload matching. Alias targets +# store written Any as a special form; that is still Any (#15630, #21768). +from typing import Any, Sequence, overload + +Nested = Sequence[Any] + +@overload +def f(s: Sequence[int]) -> int: ... +@overload +def f(s: object) -> object: ... +def f(s): pass + +direct: Sequence[Any] +aliased: Nested +reveal_type(f(direct)) # N: Revealed type is "Any" +reveal_type(f(aliased)) # N: Revealed type is "Any" + +[case testOverloadBareAnyTypeAliasIsAmbiguous] +# https://github.com/python/mypy/issues/15630 +from typing import Any, overload + +TypeSpec = Any + +@overload +def f(x: int) -> int: ... +@overload +def f(x: object) -> object: ... +def f(x): pass + +a: Any +b: TypeSpec +reveal_type(f(a)) # N: Revealed type is "Any" +reveal_type(f(b)) # N: Revealed type is "Any" + +[case testOverloadBareAnyTypeAliasDoesNotPickFirstForIdentity] +# The #15630 reproducer: an alias to Any must not pick the first overload. +from typing import Any, Union, overload + +@overload +def my_origin(x: type) -> type: ... +@overload +def my_origin(x: Any) -> Any: ... +def my_origin(x): + return x + +TypeSpec = Any + +def call_any(spec: Any) -> bool: + return my_origin(spec) is Union + +def call_alias(spec: TypeSpec) -> bool: + return my_origin(spec) is Union + [case testOverloadOnOverloadWithType] from typing import Any, Type, TypeVar, overload from mod import MyInt diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 8c37e48d7c332..9dc6f311745b0 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1346,6 +1346,42 @@ def print_custom_table() -> None: [out] _testLoadsOfOverloads.py:24: note: Revealed type is "str" +[case testOverloadNestedAnyWithFullStubs] +# Full-stub counterpart of #21768: collections.abc.Sequence, dict, tuple. +from collections.abc import Sequence +from typing import Any, overload + +@overload +def f(s: Sequence[int]) -> int: ... +@overload +def f(s: object) -> object: ... +def f(s: object) -> object: + return 0 + +concrete: Sequence[int] = [1] +nested: Sequence[Any] = [1] +as_str: Sequence[str] = ["x"] +print(f(concrete), f(nested), f(as_str)) +reveal_type(f(concrete)) +reveal_type(f(nested)) +reveal_type(f(as_str)) + +@overload +def g(s: dict[str, int]) -> int: ... +@overload +def g(s: object) -> object: ... +def g(s: object) -> object: + return 0 + +d: dict[str, Any] = {"a": 1} +print(g(d)) +reveal_type(g(d)) +[out] +_testOverloadNestedAnyWithFullStubs.py:16: note: Revealed type is "int" +_testOverloadNestedAnyWithFullStubs.py:17: note: Revealed type is "Any" +_testOverloadNestedAnyWithFullStubs.py:18: note: Revealed type is "object" +_testOverloadNestedAnyWithFullStubs.py:29: note: Revealed type is "Any" + [case testReduceWithAnyInstance] from typing import Iterable from functools import reduce