Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ def f():
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
reveal_type(b1) # revealed: Literal[Color.RED]
# TODO should be `Literal[MissingT.MISSING]`
reveal_type(b2) # revealed: @Todo(functional `Enum` syntax)
reveal_type(b2) # revealed: MissingT

# error: [invalid-type-form]
invalid1: Literal[3 + 4]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1936,16 +1936,16 @@ from enum import Enum

E = Enum("E", "A B C")

# TODO: should emit `invalid-dataclass`
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass(E)

# TODO: should emit `invalid-dataclass`
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass()(E)

# TODO: should emit `invalid-dataclass`
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass(Enum("Inline1", "X Y"))

# TODO: should emit `invalid-dataclass`
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass()(Enum("Inline2", "X Y"))
```

Expand Down
335 changes: 334 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/enums.md
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,340 @@ def _(x: EnumWithSubclassOfEnumMetaMetaclass):

## Function syntax

To do: <https://typing.python.org/en/latest/spec/enums.html#enum-definition>
### String names (positional)

```py
from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", "RED GREEN BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

Color = Enum("Color", "RED, GREEN, BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
```

### String names (keyword)

```py
from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", names="RED GREEN BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
```

### List/tuple of tuples

```py
from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", [("RED", 1), ("GREEN", 2), ("BLUE", 3)])

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

Color = Enum("Color", (("RED", 1), ("GREEN", 2), ("BLUE", 3)))

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
```

### List of strings

```py
from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", ["RED", "GREEN", "BLUE"])

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
```

### Dict mapping

```py
from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", {"RED": 1, "GREEN": 2, "BLUE": 3})

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

reveal_type(Color.RED.value) # revealed: Literal[1]
reveal_type(Color.GREEN.value) # revealed: Literal[2]
reveal_type(Color.BLUE.value) # revealed: Literal[3]
```

### Dict mapping with `auto()`

```py
from enum import Enum, auto
from ty_extensions import enum_members

Color = Enum("Color", {"RED": auto(), "GREEN": auto(), "BLUE": auto()})

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

reveal_type(Color.RED.value) # revealed: Literal[1]
reveal_type(Color.GREEN.value) # revealed: Literal[2]
reveal_type(Color.BLUE.value) # revealed: Literal[3]
```

When mixing explicit values with `auto()` in a dict, the auto value is derived from the previous
member's value, not from `start + index`:

```py
from enum import Enum, auto
from ty_extensions import enum_members

Mixed = Enum("Mixed", {"A": 10, "B": auto(), "C": auto()})

# revealed: tuple[Literal["A"], Literal["B"], Literal["C"]]
reveal_type(enum_members(Mixed))

reveal_type(Mixed.A.value) # revealed: Literal[10]
reveal_type(Mixed.B.value) # revealed: Literal[11]
reveal_type(Mixed.C.value) # revealed: Literal[12]
```

### Duplicate member names

Duplicate member names raise `TypeError` at runtime. We degrade to unknown members rather than
synthesizing a broken enum.

```py
from enum import Enum
from ty_extensions import enum_members

E1 = Enum("E1", "A A")
reveal_type(enum_members(E1)) # revealed: Unknown

E2 = Enum("E2", ["A", "A"])
reveal_type(enum_members(E2)) # revealed: Unknown

E3 = Enum("E3", [("A", 1), ("A", 2)])
reveal_type(enum_members(E3)) # revealed: Unknown
```

### Unknown members: inherited attribute access

When members are unknown, own member access returns `Unknown`, but inherited attributes from the
enum base class should still resolve through the MRO.

```py
from enum import Enum

names: list[str] = ["A", "B"]
E = Enum("E", names)

# inherited class attributes resolve from Enum base
reveal_type(E.__members__) # revealed: MappingProxyType[str, E]

# own member access is unknown (can't tell if it exists)
reveal_type(E.FOO) # revealed: Unknown
```

### Too many positional args

`Enum(value, names, *, ...)` only accepts two positional args at runtime.

```py
from enum import Enum
from ty_extensions import enum_members

# error: [too-many-positional-arguments]
Color = Enum("Color", "RED", "GREEN", "BLUE")

reveal_type(enum_members(Color)) # revealed: Unknown
```

### No positional args

```py
from enum import Enum

# this is invalid at runtime but should not panic
Color = Enum()

reveal_type(Color) # revealed: @Todo(functional `Enum` syntax)
```

### Non-literal name

Non-literal names should still be recognized as creating an enum class.

```py
from enum import Enum

def make_enum(name: str, labels: tuple[str, ...]) -> type[Enum]:
result = Enum(name.title(), labels, module=__name__)
reveal_type(result) # revealed: type[Enum]
return result
```

### Non-string name

```py
from enum import Enum

# error: [invalid-argument-type]
Color = Enum(123, "RED GREEN BLUE")
```

### Unknown keyword arguments

```py
from enum import Enum

# error: [unknown-argument]
Color = Enum("Color", "RED GREEN BLUE", bad_kwarg=True)
```

### `boundary` keyword (Python 3.11+)

#### Available on 3.11+

```toml
[environment]
python-version = "3.11"
```

```py
from enum import Flag

Perm = Flag("Perm", "READ WRITE EXECUTE", boundary=None)
```

#### Rejected before 3.11

```toml
[environment]
python-version = "3.10"
```

```py
from enum import Flag

# error: [unknown-argument]
Perm = Flag("Perm", "READ WRITE EXECUTE", boundary=None)
```

### StrEnum function syntax

```toml
[environment]
python-version = "3.11"
```

```py
from enum import StrEnum
from ty_extensions import enum_members

Color = StrEnum("Color", "RED GREEN BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

reveal_type(Color.RED.value) # revealed: Literal["red"]
reveal_type(Color.GREEN.value) # revealed: Literal["green"]
reveal_type(Color.BLUE.value) # revealed: Literal["blue"]
```

### Custom start value

```py
from enum import Enum

Color = Enum("Color", "RED GREEN BLUE", start=0)

reveal_type(Color.RED.value) # revealed: Literal[0]
reveal_type(Color.GREEN.value) # revealed: Literal[1]
reveal_type(Color.BLUE.value) # revealed: Literal[2]
```

### Type mixin

```py
from enum import Enum

Http = Enum("Http", "OK NOT_FOUND", type=int)

reveal_type(Http.OK.value) # revealed: Literal[1]
reveal_type(Http.NOT_FOUND.value) # revealed: Literal[2]
```

### IntEnum function syntax

```py
from enum import IntEnum
from ty_extensions import enum_members

Color = IntEnum("Color", "RED GREEN BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
```

### Flag function syntax

```py
from enum import Flag
from ty_extensions import enum_members

Perm = Flag("Perm", "READ WRITE EXECUTE")

# revealed: tuple[Literal["READ"], Literal["WRITE"], Literal["EXECUTE"]]
reveal_type(enum_members(Perm))

reveal_type(Perm.READ.value) # revealed: Literal[1]
reveal_type(Perm.WRITE.value) # revealed: Literal[2]
reveal_type(Perm.EXECUTE.value) # revealed: Literal[4]
```

### IntFlag function syntax

```py
from enum import IntFlag
from ty_extensions import enum_members

Perm = IntFlag("Perm", "READ WRITE EXECUTE")

# revealed: tuple[Literal["READ"], Literal["WRITE"], Literal["EXECUTE"]]
reveal_type(enum_members(Perm))

reveal_type(Perm.READ.value) # revealed: Literal[1]
reveal_type(Perm.WRITE.value) # revealed: Literal[2]
reveal_type(Perm.EXECUTE.value) # revealed: Literal[4]
```

### Large start value (overflow guard)

Values that would overflow `i64` should gracefully widen to `int`.

```py
from enum import Enum, Flag

Big = Enum("Big", "A B", start=9223372036854775807)

reveal_type(Big.A.value) # revealed: Literal[9223372036854775807]
reveal_type(Big.B.value) # revealed: int

BigFlag = Flag("BigFlag", "X Y", start=4611686018427387904)

reveal_type(BigFlag.X.value) # revealed: Literal[4611686018427387904]
reveal_type(BigFlag.Y.value) # revealed: int
```

## Exhaustiveness checking

Expand Down
Loading
Loading