[ty] Promote float and complex when promoting literals (#22215)

## Summary

Resolve https://github.com/astral-sh/ty/issues/2226

We need to add a special case in `apply_type_mapping` instead of
directly in `promote_literals_impl` because we do not reach this with
non generic non tuple nominal instances. We still ensure we apply the
normal mapping if we do not see `float` or `complex` instances.

## Test Plan

Update existing mdtest and add a new case to `literal_promotion.md`
This commit is contained in:
Matthew Mckee
2025-12-27 00:19:23 +00:00
committed by GitHub
parent 95a532f9fd
commit 6342cec842
4 changed files with 50 additions and 17 deletions

View File

@@ -2204,7 +2204,7 @@ mod tests {
assert_snapshot!(test.inlay_hints(), @r#"
a[: list[Unknown | int]] = [1, 2]
b[: list[Unknown | float]] = [1.0, 2.0]
b[: list[Unknown | int | float]] = [1.0, 2.0]
c[: list[Unknown | bool]] = [True, False]
d[: list[Unknown | None]] = [None, None]
e[: list[Unknown | str]] = ["hel", "lo"]
@@ -2229,7 +2229,7 @@ mod tests {
|
2 | a[: list[Unknown | int]] = [1, 2]
| ^^^^
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
|
@@ -2247,7 +2247,7 @@ mod tests {
|
2 | a[: list[Unknown | int]] = [1, 2]
| ^^^^^^^
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
|
@@ -2265,7 +2265,7 @@ mod tests {
|
2 | a[: list[Unknown | int]] = [1, 2]
| ^^^
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
|
@@ -2281,7 +2281,7 @@ mod tests {
--> main2.py:3:5
|
2 | a[: list[Unknown | int]] = [1, 2]
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
| ^^^^
4 | c[: list[Unknown | bool]] = [True, False]
5 | d[: list[Unknown | None]] = [None, None]
@@ -2300,12 +2300,31 @@ mod tests {
--> main2.py:3:10
|
2 | a[: list[Unknown | int]] = [1, 2]
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
| ^^^^^^^
4 | c[: list[Unknown | bool]] = [True, False]
5 | d[: list[Unknown | None]] = [None, None]
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:3:20
|
2 | a[: list[Unknown | int]] = [1, 2]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
| ^^^
4 | c[: list[Unknown | bool]] = [True, False]
5 | d[: list[Unknown | None]] = [None, None]
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:661:7
|
@@ -2315,11 +2334,11 @@ mod tests {
662 | """Convert a string or number to a floating-point number, if possible."""
|
info: Source
--> main2.py:3:20
--> main2.py:3:26
|
2 | a[: list[Unknown | int]] = [1, 2]
3 | b[: list[Unknown | float]] = [1.0, 2.0]
| ^^^^^
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
| ^^^^^
4 | c[: list[Unknown | bool]] = [True, False]
5 | d[: list[Unknown | None]] = [None, None]
|
@@ -2336,7 +2355,7 @@ mod tests {
--> main2.py:4:5
|
2 | a[: list[Unknown | int]] = [1, 2]
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
| ^^^^
5 | d[: list[Unknown | None]] = [None, None]
@@ -2356,7 +2375,7 @@ mod tests {
--> main2.py:4:10
|
2 | a[: list[Unknown | int]] = [1, 2]
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
| ^^^^^^^
5 | d[: list[Unknown | None]] = [None, None]
@@ -2376,7 +2395,7 @@ mod tests {
--> main2.py:4:20
|
2 | a[: list[Unknown | int]] = [1, 2]
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
| ^^^^
5 | d[: list[Unknown | None]] = [None, None]
@@ -2394,7 +2413,7 @@ mod tests {
info: Source
--> main2.py:5:5
|
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
5 | d[: list[Unknown | None]] = [None, None]
| ^^^^
@@ -2414,7 +2433,7 @@ mod tests {
info: Source
--> main2.py:5:10
|
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
5 | d[: list[Unknown | None]] = [None, None]
| ^^^^^^^
@@ -2434,7 +2453,7 @@ mod tests {
info: Source
--> main2.py:5:20
|
3 | b[: list[Unknown | float]] = [1.0, 2.0]
3 | b[: list[Unknown | int | float]] = [1.0, 2.0]
4 | c[: list[Unknown | bool]] = [True, False]
5 | d[: list[Unknown | None]] = [None, None]
| ^^^^
@@ -2885,7 +2904,7 @@ mod tests {
info: Source
a: list[Unknown | int] = [1, 2]
b: list[Unknown | float] = [1.0, 2.0]
b: list[Unknown | int | float] = [1.0, 2.0]
c: list[Unknown | bool] = [True, False]
d: list[Unknown | None] = [None, None]
e: list[Unknown | str] = ["hel", "lo"]

View File

@@ -7,6 +7,9 @@ python-version = "3.12"
There are certain places where we promote literals to their common supertype.
We also promote `float` to `int | float` and `complex` to `int | float | complex`, even when not in
a type annotation.
## All literal types are promotable
```py
@@ -31,6 +34,9 @@ def _(
reveal_type(promote(lit3)) # revealed: list[bool]
reveal_type(promote(lit4)) # revealed: list[bytes]
reveal_type(promote(lit5)) # revealed: list[MyEnum]
reveal_type(promote(3.14)) # revealed: list[int | float]
reveal_type(promote(3.14j)) # revealed: list[int | float | complex]
```
Function types are also promoted to their `Callable` form:

View File

@@ -251,7 +251,7 @@ reveal_type(LegacyProperty("height", 42)) # revealed: LegacyProperty[int]
reveal_type(LegacyProperty.value) # revealed: property
reveal_type(LegacyProperty.value.fget) # revealed: (self, /) -> Unknown
reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str
reveal_type(LegacyProperty("height", 3.4).value) # revealed: float
reveal_type(LegacyProperty("height", 3.4).value) # revealed: int | float
```
## Attributes on `NamedTuple`

View File

@@ -7948,6 +7948,14 @@ impl<'db> Type<'db> {
method.self_instance(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor),
)),
Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::PromoteLiterals(PromoteLiteralsMode::On)) => {
match instance.known_class(db) {
Some(KnownClass::Complex) => KnownUnion::Complex.to_type(db),
Some(KnownClass::Float) => KnownUnion::Float.to_type(db),
_ => instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor),
}
}
Type::NominalInstance(instance) => {
instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor)
},