Skip to content

Commit 2dbddab

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 2dbddab

6 files changed

Lines changed: 186 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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6856,3 +6856,161 @@ 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+
# Chained alias `B = A` where `A = Any` should still behave like Any.
6906+
from typing import Any, overload
6907+
from typing_extensions import TypeAlias
6908+
6909+
A: TypeAlias = Any
6910+
B: TypeAlias = A
6911+
6912+
@overload
6913+
def f(x: int) -> int: ...
6914+
@overload
6915+
def f(x: str) -> str: ...
6916+
def f(x): ...
6917+
6918+
a: A
6919+
b: B
6920+
reveal_type(f(a)) # N: Revealed type is "Any"
6921+
reveal_type(f(b)) # N: Revealed type is "Any"
6922+
[builtins fixtures/tuple.pyi]
6923+
6924+
[case testOverloadAliasToAnyArgImplicitAlias]
6925+
# `MyAlias = Any` without an explicit TypeAlias annotation also gets the
6926+
# alias-Any treatment and must engage the overload-ambiguity check.
6927+
from typing import Any, overload
6928+
6929+
MyAlias = Any
6930+
6931+
@overload
6932+
def f(x: int) -> int: ...
6933+
@overload
6934+
def f(x: str) -> str: ...
6935+
def f(x): ...
6936+
6937+
a: MyAlias
6938+
reveal_type(f(a)) # N: Revealed type is "Any"
6939+
[builtins fixtures/tuple.pyi]
6940+
6941+
[case testOverloadAliasToAnyArgImported]
6942+
# Alias-to-Any imported from another module must behave the same way.
6943+
from typing import overload
6944+
from other import Incomplete
6945+
6946+
@overload
6947+
def f(x: int) -> int: ...
6948+
@overload
6949+
def f(x: str) -> str: ...
6950+
def f(x): ...
6951+
6952+
a: Incomplete
6953+
reveal_type(f(a)) # N: Revealed type is "Any"
6954+
6955+
[file other.py]
6956+
from typing import Any
6957+
from typing_extensions import TypeAlias
6958+
Incomplete: TypeAlias = Any
6959+
[builtins fixtures/tuple.pyi]
6960+
6961+
[case testOverloadAliasToAnyArgPep695]
6962+
# PEP 695 `type Incomplete = Any` should also engage overload ambiguity.
6963+
# flags: --python-version 3.12
6964+
from typing import Any, overload
6965+
6966+
type Incomplete = Any
6967+
6968+
@overload
6969+
def f(x: int) -> int: ...
6970+
@overload
6971+
def f(x: str) -> str: ...
6972+
def f(x): ...
6973+
6974+
a: Incomplete
6975+
reveal_type(f(a)) # N: Revealed type is "Any"
6976+
[builtins fixtures/tuple.pyi]
6977+
6978+
[case testOverloadAliasToAnyArgUnambiguous]
6979+
# When overloads are unambiguous, alias-to-Any should pick that overload's
6980+
# return type just like bare Any does (no false ambiguity).
6981+
from typing import Any, overload
6982+
from typing_extensions import TypeAlias
6983+
6984+
Incomplete: TypeAlias = Any
6985+
6986+
@overload
6987+
def f(x: int, y: int, z: str) -> int: ...
6988+
@overload
6989+
def f(x: object, y: int, z: str) -> object: ...
6990+
def f(x, y, z): ...
6991+
6992+
a: Incomplete
6993+
b: Any
6994+
# Ambiguous on x: returns Any
6995+
reveal_type(f(a, 1, '')) # N: Revealed type is "Any"
6996+
reveal_type(f(b, 1, '')) # N: Revealed type is "Any"
6997+
# Unambiguous: x is concrete, only y/z are Any
6998+
reveal_type(f(1, a, '')) # N: Revealed type is "builtins.int"
6999+
reveal_type(f(1, b, '')) # N: Revealed type is "builtins.int"
7000+
[builtins fixtures/tuple.pyi]
7001+
7002+
[case testAliasToAnyDoesNotTriggerDisallowAnyExplicitAtUseSite]
7003+
# --disallow-any-explicit must NOT fire at use sites of an `Alias = Any`
7004+
# (the original purpose of make_any_non_explicit).
7005+
# flags: --disallow-any-explicit --show-error-codes
7006+
from typing import Any
7007+
from typing_extensions import TypeAlias
7008+
7009+
Incomplete: TypeAlias = Any # E: Explicit "Any" is not allowed [explicit-any]
7010+
7011+
def f(x: Incomplete) -> Incomplete: # no error
7012+
return x
7013+
7014+
a: Incomplete = 1 # no error
7015+
[builtins fixtures/tuple.pyi]
7016+

0 commit comments

Comments
 (0)