Skip to content

Commit c547cfa

Browse files
committed
[ty] static/functional parity
1 parent 91418cc commit c547cfa

4 files changed

Lines changed: 368 additions & 57 deletions

File tree

crates/ty_python_semantic/resources/mdtest/enums.md

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,30 @@ reveal_type(ManyAliases.alias3.value) # revealed: Literal["real_member"]
641641
reveal_type(ManyAliases.alias3.name) # revealed: Literal["real_member"]
642642
```
643643

644+
Functional enums also detect duplicate-value aliases in both dict and list-of-tuples forms:
645+
646+
```py
647+
from enum import Enum
648+
from ty_extensions import enum_members
649+
650+
DictAlias = Enum("DictAlias", {"A": 1, "B": 1})
651+
652+
# revealed: tuple[Literal["A"]]
653+
reveal_type(enum_members(DictAlias))
654+
655+
# single-member enum is a singleton, so member access resolves to the instance type
656+
reveal_type(DictAlias.A) # revealed: DictAlias
657+
reveal_type(DictAlias.B) # revealed: DictAlias
658+
659+
PairsAlias = Enum("PairsAlias", [("A", 1), ("B", 1)])
660+
661+
# revealed: tuple[Literal["A"]]
662+
reveal_type(enum_members(PairsAlias))
663+
664+
reveal_type(PairsAlias.A) # revealed: PairsAlias
665+
reveal_type(PairsAlias.B) # revealed: PairsAlias
666+
```
667+
644668
### Using `auto()`
645669

646670
```toml
@@ -683,6 +707,24 @@ reveal_type(Mixed.MANUAL_2.value) # revealed: Literal[-2]
683707
reveal_type(Mixed.AUTO_2.value) # revealed: Literal[2]
684708
```
685709

710+
If `auto()` follows a non-literal value, the generated value widens to `int` since the previous
711+
value isn't known at type-check time:
712+
713+
```py
714+
def f(n: int):
715+
class StaticDynamic(Enum):
716+
A = n
717+
B = auto()
718+
719+
reveal_type(StaticDynamic.A.value) # revealed: int
720+
reveal_type(StaticDynamic.B.value) # revealed: int
721+
722+
Dynamic = Enum("Dynamic", {"A": n, "B": auto()})
723+
724+
reveal_type(Dynamic.A.value) # revealed: int
725+
reveal_type(Dynamic.B.value) # revealed: int
726+
```
727+
686728
When using `auto()` with `StrEnum`, the value is the lowercase name of the member:
687729

688730
```py
@@ -1410,6 +1452,24 @@ reveal_type(Mixed.B.value) # revealed: Literal[11]
14101452
reveal_type(Mixed.C.value) # revealed: Literal[12]
14111453
```
14121454

1455+
### `auto()` in tuple/list entries
1456+
1457+
`auto()` should also expand in tuple/list entry forms of the functional syntax:
1458+
1459+
```py
1460+
from enum import Enum, Flag, auto
1461+
1462+
Color = Enum("Color", [("RED", auto()), ("GREEN", auto())])
1463+
1464+
reveal_type(Color.RED.value) # revealed: Literal[1]
1465+
reveal_type(Color.GREEN.value) # revealed: Literal[2]
1466+
1467+
Perm = Flag("Perm", (("READ", auto()), ("WRITE", auto())))
1468+
1469+
reveal_type(Perm.READ.value) # revealed: Literal[1]
1470+
reveal_type(Perm.WRITE.value) # revealed: Literal[2]
1471+
```
1472+
14131473
### Duplicate member names
14141474

14151475
Duplicate member names raise `TypeError` at runtime. We degrade to unknown members rather than
@@ -1503,6 +1563,19 @@ from enum import Enum
15031563
Color = Enum("Color", "RED GREEN BLUE", bad_kwarg=True)
15041564
```
15051565

1566+
### Keyword argument type validation
1567+
1568+
Functional enum construction should still preserve overload-based argument validation:
1569+
1570+
```py
1571+
from enum import Enum
1572+
1573+
# error: [invalid-argument-type]
1574+
Color = Enum("Color", "RED", start="0")
1575+
1576+
reveal_type(Color.RED.value) # revealed: Literal[1]
1577+
```
1578+
15061579
### `boundary` keyword (Python 3.11+)
15071580

15081581
#### Available on 3.11+
@@ -1576,6 +1649,43 @@ reveal_type(Http.OK.value) # revealed: Literal[1]
15761649
reveal_type(Http.NOT_FOUND.value) # revealed: Literal[2]
15771650
```
15781651

1652+
Functional enums should still validate `type=` arguments eagerly, both for obvious non-types and for
1653+
bases that are structurally invalid to combine with `Enum`:
1654+
1655+
```py
1656+
from enum import Enum
1657+
from typing import TypedDict
1658+
1659+
# error: [invalid-argument-type]
1660+
BadType = Enum("BadType", "RED", type=1)
1661+
1662+
# error: [invalid-argument-type]
1663+
BadStringType = Enum("BadStringType", "RED", type="Mixin")
1664+
1665+
TD = TypedDict("TD", {"x": int})
1666+
1667+
# error: [invalid-base]
1668+
BadBase = Enum("BadBase", "RED", type=TD)
1669+
```
1670+
1671+
Functional enums with a `type=` mixin should also have the same MRO as the equivalent static enum
1672+
class:
1673+
1674+
```py
1675+
from enum import Enum
1676+
from ty_extensions import reveal_mro
1677+
1678+
Http = Enum("Http", "OK NOT_FOUND", type=int)
1679+
1680+
reveal_mro(Http) # revealed: (<class 'Http'>, <class 'int'>, <class 'Enum'>, <class 'object'>)
1681+
1682+
class StaticHttp(int, Enum):
1683+
OK = 1
1684+
NOT_FOUND = 2
1685+
1686+
reveal_mro(StaticHttp) # revealed: (<class 'StaticHttp'>, <class 'int'>, <class 'Enum'>, <class 'object'>)
1687+
```
1688+
15791689
### IntEnum function syntax
15801690

15811691
```py
@@ -1638,6 +1748,59 @@ reveal_type(BigFlag.X.value) # revealed: Literal[4611686018427387904]
16381748
reveal_type(BigFlag.Y.value) # revealed: int
16391749
```
16401750

1751+
### Accessing members from instances
1752+
1753+
```py
1754+
from enum import Enum
1755+
1756+
Answer = Enum("Answer", "YES NO")
1757+
1758+
reveal_type(Answer.YES.NO) # revealed: Literal[Answer.NO]
1759+
1760+
def _(answer: Answer) -> None:
1761+
reveal_type(answer.YES) # revealed: Literal[Answer.YES]
1762+
reveal_type(answer.NO) # revealed: Literal[Answer.NO]
1763+
```
1764+
1765+
### Accessing members from `type[…]`
1766+
1767+
```py
1768+
from enum import Enum
1769+
1770+
Answer = Enum("Answer", "YES NO")
1771+
1772+
def _(answer: type[Answer]) -> None:
1773+
reveal_type(answer.YES) # revealed: Literal[Answer.YES]
1774+
reveal_type(answer.NO) # revealed: Literal[Answer.NO]
1775+
```
1776+
1777+
### Implicitly final
1778+
1779+
Functional enums with members should also be implicitly final:
1780+
1781+
```py
1782+
from enum import Enum
1783+
1784+
Color = Enum("Color", "RED GREEN BLUE")
1785+
1786+
# error: [subclass-of-final-class]
1787+
class ExtendedColor(Color):
1788+
YELLOW = 4
1789+
```
1790+
1791+
### Meta-type
1792+
1793+
```py
1794+
from enum import Enum
1795+
1796+
Answer = Enum("Answer", "YES NO")
1797+
1798+
reveal_type(type(Answer.YES)) # revealed: <class 'Answer'>
1799+
1800+
def _(answer: Answer):
1801+
reveal_type(type(answer)) # revealed: <class 'Answer'>
1802+
```
1803+
16411804
## Exhaustiveness checking
16421805

16431806
## `if` statements
@@ -1744,6 +1907,80 @@ def singleton_check(value: Singleton) -> str:
17441907
assert_never(value)
17451908
```
17461909

1910+
## `if` statements (function syntax)
1911+
1912+
```py
1913+
from enum import Enum
1914+
from typing_extensions import assert_never
1915+
1916+
Color = Enum("Color", "RED GREEN BLUE")
1917+
1918+
def color_name(color: Color) -> str:
1919+
if color is Color.RED:
1920+
return "Red"
1921+
elif color is Color.GREEN:
1922+
return "Green"
1923+
elif color is Color.BLUE:
1924+
return "Blue"
1925+
else:
1926+
assert_never(color)
1927+
1928+
def color_name_without_assertion(color: Color) -> str:
1929+
if color is Color.RED:
1930+
return "Red"
1931+
elif color is Color.GREEN:
1932+
return "Green"
1933+
elif color is Color.BLUE:
1934+
return "Blue"
1935+
1936+
def color_name_misses_one_variant(color: Color) -> str:
1937+
if color is Color.RED:
1938+
return "Red"
1939+
elif color is Color.GREEN:
1940+
return "Green"
1941+
else:
1942+
assert_never(color) # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`"
1943+
```
1944+
1945+
## `match` statements (function syntax)
1946+
1947+
TODO: `match` exhaustiveness does not yet work for functional enums. The pattern matching narrowing
1948+
path does not resolve functional enum members the same way `is` comparisons do.
1949+
1950+
```toml
1951+
[environment]
1952+
python-version = "3.10"
1953+
```
1954+
1955+
```py
1956+
from enum import Enum
1957+
from typing_extensions import assert_never
1958+
1959+
Color = Enum("Color", "RED GREEN BLUE")
1960+
1961+
# TODO: `assert_never` should not fire here (exhaustive match).
1962+
def color_name(color: Color) -> str:
1963+
match color:
1964+
case Color.RED:
1965+
return "Red"
1966+
case Color.GREEN:
1967+
return "Green"
1968+
case Color.BLUE:
1969+
return "Blue"
1970+
case _:
1971+
assert_never(color) # error: [type-assertion-failure]
1972+
1973+
# TODO: This should ideally emit `Literal[Color.BLUE]` in the assertion, not `Color`.
1974+
def color_name_misses_one_variant(color: Color) -> str:
1975+
match color:
1976+
case Color.RED:
1977+
return "Red"
1978+
case Color.GREEN:
1979+
return "Green"
1980+
case _:
1981+
assert_never(color) # error: [type-assertion-failure] "Type `Color` is not equivalent to `Never`"
1982+
```
1983+
17471984
## `__eq__` and `__ne__`
17481985

17491986
### No `__eq__` or `__ne__` overrides

crates/ty_python_semantic/src/types/class.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -547,12 +547,12 @@ impl<'db> ClassLiteral<'db> {
547547
pub(crate) fn is_final(self, db: &'db dyn Db) -> bool {
548548
match self {
549549
Self::Static(class) => class.is_final(db),
550+
Self::DynamicEnum(enum_lit) => {
551+
crate::types::enums::enum_metadata(db, Self::DynamicEnum(enum_lit)).is_some()
552+
}
550553
// Dynamic classes created via `type()`, `collections.namedtuple()`, etc. cannot be
551554
// marked as final.
552-
Self::Dynamic(_)
553-
| Self::DynamicNamedTuple(_)
554-
| Self::DynamicTypedDict(_)
555-
| Self::DynamicEnum(_) => false,
555+
Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicTypedDict(_) => false,
556556
}
557557
}
558558

0 commit comments

Comments
 (0)