Skip to content

Commit e99dbc8

Browse files
author
Pablo Galindo Salgado
committed
Fix overload resolution for type aliases to Any
Aliases like `Incomplete: TypeAlias = Any` were being downgraded to TypeOfAny.special_form, the same flavor used for NewType-style higher-kinded types. has_any_type ignores special_form Anys when checking for overload ambiguity, which meant a 2-arg call to os.environ.get with an alias-Any default would silently match the `default: None = None` overload and return `str | None` instead of Any. Add a dedicated TypeOfAny.from_alias_target flavor that suppresses --disallow-any-explicit at use sites (the original reason for the downgrade) but still counts as a real Any everywhere else. Stats reporting and the Literal[...] arg check are extended to keep their existing behavior for the new flavor.
1 parent da21dae commit e99dbc8

7 files changed

Lines changed: 187 additions & 3 deletions

File tree

mypy/semanal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8269,7 +8269,7 @@ def make_any_non_explicit(t: Type) -> Type:
82698269
class MakeAnyNonExplicit(TrivialSyntheticTypeTranslator):
82708270
def visit_any(self, t: AnyType) -> Type:
82718271
if t.type_of_any == TypeOfAny.explicit:
8272-
return t.copy_modified(TypeOfAny.special_form)
8272+
return t.copy_modified(TypeOfAny.from_alias_target)
82738273
return t
82748274

82758275
def visit_type_alias_type(self, t: TypeAliasType) -> Type:

mypy/stats.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,10 @@ def is_complex(t: Type) -> bool:
483483

484484

485485
def is_special_form_any(t: AnyType) -> bool:
486-
return get_original_any(t).type_of_any == TypeOfAny.special_form
486+
return get_original_any(t).type_of_any in (
487+
TypeOfAny.special_form,
488+
TypeOfAny.from_alias_target,
489+
)
487490

488491

489492
def get_original_any(t: AnyType) -> AnyType:

mypy/typeanal.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1760,7 +1760,11 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type]
17601760
#
17611761
# TODO: Once we start adding support for enums, make sure we report a custom
17621762
# error for case 2 as well.
1763-
if arg.type_of_any not in (TypeOfAny.from_error, TypeOfAny.special_form):
1763+
if arg.type_of_any not in (
1764+
TypeOfAny.from_error,
1765+
TypeOfAny.special_form,
1766+
TypeOfAny.from_alias_target,
1767+
):
17641768
self.fail(
17651769
f'Parameter {idx} of Literal[...] cannot be of type "Any"',
17661770
ctx,

mypy/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ class TypeOfAny:
237237
# used to ignore Anys inserted by the suggestion engine when
238238
# generating constraints.
239239
suggestion_engine: Final = 9
240+
# Does this Any come from a type alias whose target is Any (e.g.
241+
# `Incomplete: TypeAlias = Any`)? It is a real Any (unlike special_form),
242+
# but tagged separately so use sites don't trigger --disallow-any-explicit.
243+
from_alias_target: Final = 10
240244

241245

242246
def deserialize_type(data: JsonDict | str) -> Type:

test-data/unit/check-literal.test

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,20 @@ reveal_type(b) # N: Revealed type is "Any"
562562
[builtins fixtures/tuple.pyi]
563563
[out]
564564

565+
[case testLiteralAliasToAnyAllowed]
566+
# Aliases to Any (e.g. typeshed's `Incomplete: TypeAlias = Any`) must not
567+
# trigger the Literal[...] cannot-be-Any error, matching the behavior of
568+
# unfollowed-import aliases.
569+
from typing import Any, Literal
570+
from typing_extensions import TypeAlias
571+
572+
Incomplete: TypeAlias = Any
573+
574+
a: Literal[Incomplete]
575+
reveal_type(a) # N: Revealed type is "Any"
576+
[builtins fixtures/tuple.pyi]
577+
[out]
578+
565579
[case testLiteralDisallowActualTypes]
566580
from typing import Literal
567581

test-data/unit/check-overloading.test

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6856,3 +6856,145 @@ if isinstance(headers, dict):
68566856

68576857
reveal_type(headers) # N: Revealed type is "__main__.Headers | typing.Iterable[tuple[builtins.bytes, builtins.bytes]]"
68586858
[builtins fixtures/isinstancelist.pyi]
6859+
6860+
[case testOverloadAliasToAnyArgWithNoneDefault]
6861+
# An alias `Incomplete = Any` used as an overload arg must not silently match
6862+
# the `default: None = None` overload. It should be treated as Any like a bare
6863+
# Any value, triggering the overload-ambiguity fallback.
6864+
from typing import Any, TypeVar, overload
6865+
from typing_extensions import TypeAlias
6866+
6867+
T = TypeVar("T")
6868+
Incomplete: TypeAlias = Any
6869+
6870+
class M:
6871+
@overload
6872+
def get(self, key: str, default: None = None) -> str | None: ...
6873+
@overload
6874+
def get(self, key: str, default: str) -> str: ...
6875+
@overload
6876+
def get(self, key: str, default: T) -> str | T: ...
6877+
def get(self, key, default=None): ...
6878+
6879+
a: Incomplete
6880+
b: Any
6881+
reveal_type(M().get("k", a)) # N: Revealed type is "Any"
6882+
reveal_type(M().get("k", b)) # N: Revealed type is "Any"
6883+
[builtins fixtures/tuple.pyi]
6884+
6885+
[case testOverloadAliasToAnyArgEquivalentToBareAny]
6886+
# Alias-to-Any and bare Any should produce the same overload results.
6887+
from typing import Any, overload
6888+
from typing_extensions import TypeAlias
6889+
6890+
Incomplete: TypeAlias = Any
6891+
6892+
@overload
6893+
def f(x: int) -> int: ...
6894+
@overload
6895+
def f(x: str) -> str: ...
6896+
def f(x): ...
6897+
6898+
a: Incomplete
6899+
b: Any
6900+
reveal_type(f(a)) # N: Revealed type is "Any"
6901+
reveal_type(f(b)) # N: Revealed type is "Any"
6902+
[builtins fixtures/tuple.pyi]
6903+
6904+
[case testOverloadAliasToAnyArgChainedAlias]
6905+
# A chained alias `Outer = Inner` where `Inner = Any` should still behave
6906+
# like Any, both at the inner and outer level.
6907+
from typing import Any, overload
6908+
from typing_extensions import TypeAlias
6909+
6910+
Inner: TypeAlias = Any
6911+
Outer: TypeAlias = Inner
6912+
6913+
@overload
6914+
def f(x: int) -> int: ...
6915+
@overload
6916+
def f(x: str) -> str: ...
6917+
def f(x): ...
6918+
6919+
inner: Inner
6920+
outer: Outer
6921+
reveal_type(f(inner)) # N: Revealed type is "Any"
6922+
reveal_type(f(outer)) # N: Revealed type is "Any"
6923+
[builtins fixtures/tuple.pyi]
6924+
6925+
[case testOverloadAliasToAnyArgImplicitAlias]
6926+
# `MyAlias = Any` without an explicit TypeAlias annotation also gets the
6927+
# alias-Any treatment and must engage the overload-ambiguity check.
6928+
from typing import Any, overload
6929+
6930+
MyAlias = Any
6931+
6932+
@overload
6933+
def f(x: int) -> int: ...
6934+
@overload
6935+
def f(x: str) -> str: ...
6936+
def f(x): ...
6937+
6938+
a: MyAlias
6939+
reveal_type(f(a)) # N: Revealed type is "Any"
6940+
[builtins fixtures/tuple.pyi]
6941+
6942+
[case testOverloadAliasToAnyArgImported]
6943+
# Alias-to-Any imported from another module must behave the same way.
6944+
from typing import overload
6945+
from other import Incomplete
6946+
6947+
@overload
6948+
def f(x: int) -> int: ...
6949+
@overload
6950+
def f(x: str) -> str: ...
6951+
def f(x): ...
6952+
6953+
a: Incomplete
6954+
reveal_type(f(a)) # N: Revealed type is "Any"
6955+
6956+
[file other.py]
6957+
from typing import Any
6958+
from typing_extensions import TypeAlias
6959+
Incomplete: TypeAlias = Any
6960+
[builtins fixtures/tuple.pyi]
6961+
6962+
[case testOverloadAliasToAnyArgUnambiguous]
6963+
# When overloads are unambiguous, alias-to-Any should pick that overload's
6964+
# return type just like bare Any does (no false ambiguity).
6965+
from typing import Any, overload
6966+
from typing_extensions import TypeAlias
6967+
6968+
Incomplete: TypeAlias = Any
6969+
6970+
@overload
6971+
def f(x: int, y: int, z: str) -> int: ...
6972+
@overload
6973+
def f(x: object, y: int, z: str) -> object: ...
6974+
def f(x, y, z): ...
6975+
6976+
a: Incomplete
6977+
b: Any
6978+
# Ambiguous on x: returns Any
6979+
reveal_type(f(a, 1, '')) # N: Revealed type is "Any"
6980+
reveal_type(f(b, 1, '')) # N: Revealed type is "Any"
6981+
# Unambiguous: x is concrete, only y/z are Any
6982+
reveal_type(f(1, a, '')) # N: Revealed type is "builtins.int"
6983+
reveal_type(f(1, b, '')) # N: Revealed type is "builtins.int"
6984+
[builtins fixtures/tuple.pyi]
6985+
6986+
[case testAliasToAnyDoesNotTriggerDisallowAnyExplicitAtUseSite]
6987+
# --disallow-any-explicit must NOT fire at use sites of an `Alias = Any`
6988+
# (the original purpose of make_any_non_explicit).
6989+
# flags: --disallow-any-explicit --show-error-codes
6990+
from typing import Any
6991+
from typing_extensions import TypeAlias
6992+
6993+
Incomplete: TypeAlias = Any # E: Explicit "Any" is not allowed [explicit-any]
6994+
6995+
def f(x: Incomplete) -> Incomplete: # no error
6996+
return x
6997+
6998+
a: Incomplete = 1 # no error
6999+
[builtins fixtures/tuple.pyi]
7000+

test-data/unit/check-python312.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,3 +2242,20 @@ class D[*Ts](Generic[Unpack[Us]]): # E: Generic[...] base class is redundant \
22422242
# E: Can only use one type var tuple in a class def
22432243
pass
22442244
[builtins fixtures/tuple.pyi]
2245+
2246+
[case testPEP695AliasToAnyOverloadAmbiguity]
2247+
# A PEP 695 `type Incomplete = Any` alias used as an overload arg should
2248+
# engage the overload-ambiguity fallback, just like a bare Any value.
2249+
from typing import Any, overload
2250+
2251+
type Incomplete = Any
2252+
2253+
@overload
2254+
def f(x: int) -> int: ...
2255+
@overload
2256+
def f(x: str) -> str: ...
2257+
def f(x): ...
2258+
2259+
a: Incomplete
2260+
reveal_type(f(a)) # N: Revealed type is "Any"
2261+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)