[ty] Enum literal types (#19328)

## Summary

Add a new `Type::EnumLiteral(…)` variant and infer this type for member
accesses on enums.

**Example**: No more `@Todo` types here:
```py
from enum import Enum

class Answer(Enum):
    YES = 1
    NO = 2

    def is_yes(self) -> bool:
        return self == Answer.YES

reveal_type(Answer.YES)  # revealed: Literal[Answer.YES]
reveal_type(Answer.YES == Answer.NO)  # revealed: Literal[False]
reveal_type(Answer.YES.is_yes())  # revealed: bool
```

## Test Plan

* Many new Markdown tests for the new type variant
* Added enum literal types to property tests, ran property tests

## Ecosystem analysis

Summary:

Lots of false positives removed. All of the new diagnostics are
either new true positives (the majority) or known problems. Click for
detailed analysis</summary>

Details:

```diff
AutoSplit (https://github.com/Toufool/AutoSplit)
+ error[call-non-callable] src/capture_method/__init__.py:137:9: Method `__getitem__` of type `bound method CaptureMethodDict.__getitem__(key: Never, /) -> type[CaptureMethodBase]` is not callable on object of type `CaptureMethodDict`
+ error[call-non-callable] src/capture_method/__init__.py:147:9: Method `__getitem__` of type `bound method CaptureMethodDict.__getitem__(key: Never, /) -> type[CaptureMethodBase]` is not callable on object of type `CaptureMethodDict`
+ error[call-non-callable] src/capture_method/__init__.py:148:1: Method `__getitem__` of type `bound method CaptureMethodDict.__getitem__(key: Never, /) -> type[CaptureMethodBase]` is not callable on object of type `CaptureMethodDict`
```

New true positives. That `__getitem__` method is apparently annotated
with `Never` to prevent developers from using it.


```diff
dd-trace-py (https://github.com/DataDog/dd-trace-py)
+ error[invalid-assignment] ddtrace/vendor/psutil/_common.py:29:5: Object of type `None` is not assignable to `Literal[AddressFamily.AF_INET6]`
+ error[invalid-assignment] ddtrace/vendor/psutil/_common.py:33:5: Object of type `None` is not assignable to `Literal[AddressFamily.AF_UNIX]`
```

Arguably true positives:
e0a772c28b/ddtrace/vendor/psutil/_common.py (L29)

```diff
ignite (https://github.com/pytorch/ignite)
+ error[invalid-argument-type] tests/ignite/engine/test_custom_events.py:190:34: Argument to bound method `__call__` is incorrect: Expected `((...) -> Unknown) | None`, found `Literal["123"]`
+ error[invalid-argument-type] tests/ignite/engine/test_custom_events.py:220:37: Argument to function `default_event_filter` is incorrect: Expected `Engine`, found `None`
+ error[invalid-argument-type] tests/ignite/engine/test_custom_events.py:220:43: Argument to function `default_event_filter` is incorrect: Expected `int`, found `None`
+ error[call-non-callable] tests/ignite/engine/test_custom_events.py:561:9: Object of type `CustomEvents` is not callable
+ error[invalid-argument-type] tests/ignite/metrics/test_frequency.py:50:38: Argument to bound method `attach` is incorrect: Expected `Events`, found `CallableEventWithFilter`
```

All true positives. Some of them are inside `pytest.raises(TypeError,
…)` blocks 🙃

```diff
meson (https://github.com/mesonbuild/meson)
+ error[invalid-argument-type] unittests/internaltests.py:243:51: Argument to bound method `__init__` is incorrect: Expected `bool`, found `Literal[MachineChoice.HOST]`
+ error[invalid-argument-type] unittests/internaltests.py:271:51: Argument to bound method `__init__` is incorrect: Expected `bool`, found `Literal[MachineChoice.HOST]`
```

New true positives. Enum literals can not be assigned to `bool`, even if
their value types are `0` and `1`.

```diff
poetry (https://github.com/python-poetry/poetry)
+ error[invalid-assignment] src/poetry/console/exceptions.py:101:5: Object of type `Literal[""]` is not assignable to `InitVar[str]`
```

New false positive, missing support for `InitVar`.

```diff
prefect (https://github.com/PrefectHQ/prefect)
+ error[invalid-argument-type] src/integrations/prefect-dask/tests/test_task_runners.py:193:17: Argument is incorrect: Expected `StateType`, found `Literal[StateType.COMPLETED]`
```

This is confusing. There are two definitions
([one](74d8cd93ee/src/prefect/client/schemas/objects.py (L89-L100)),
[two](https://github.com/PrefectHQ/prefect/blob/main/src/prefect/server/schemas/states.py#L40))
of the `StateType` enum. Here, we're trying to assign one to the other.
I don't think that should be allowed, so this is a true positive (?).

```diff
python-htmlgen (https://github.com/srittau/python-htmlgen)
+ error[invalid-assignment] test_htmlgen/form.py:51:9: Object of type `str` is not assignable to attribute `autocomplete` of type `Autocomplete | None`
+ error[invalid-assignment] test_htmlgen/video.py:38:9: Object of type `str` is not assignable to attribute `preload` of type `Preload | None`
```

True positives. [The stubs are
wrong](01e3b911ac/htmlgen/form.pyi (L8-L10)).
These should not contain type annotations, but rather just `OFF = ...`.

```diff
rotki (https://github.com/rotki/rotki)
+ error[invalid-argument-type] rotkehlchen/tests/unit/test_serialization.py:62:30: Argument to bound method `deserialize` is incorrect: Expected `str`, found `Literal[15]`
```

New true positive.

```diff
vision (https://github.com/pytorch/vision)
+ error[unresolved-attribute] test/test_extended_models.py:302:17: Type `type[WeightsEnum]` has no attribute `DEFAULT`
+ error[unresolved-attribute] test/test_extended_models.py:302:58: Type `type[WeightsEnum]` has no attribute `DEFAULT`
```

Also new true positives. No `DEFAULT` member exists on `WeightsEnum`.
This commit is contained in:
David Peter 2025-07-15 21:31:53 +02:00 committed by GitHub
parent a0d4e1f854
commit a1edb69ea5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1016 additions and 75 deletions

View file

@ -35,8 +35,7 @@ def f():
reveal_type(a6) # revealed: Literal[True]
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
# TODO: This should be Color.RED
reveal_type(b1) # revealed: @Todo(Attribute access on enum classes)
reveal_type(b1) # revealed: Literal[Color.RED]
# error: [invalid-type-form]
invalid1: Literal[3 + 4]

View file

@ -2350,19 +2350,17 @@ reveal_type(C().x) # revealed: int
## Enum classes
Enums are not supported yet; attribute access on an enum class is inferred as `Todo`.
```py
import enum
reveal_type(enum.Enum.__members__) # revealed: @Todo(Attribute access on enum classes)
reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Unknown]
class Foo(enum.Enum):
BAR = 1
reveal_type(Foo.BAR) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Foo.BAR) # revealed: Literal[Foo.BAR]
reveal_type(Foo.BAR.value) # revealed: Any
reveal_type(Foo.__members__) # revealed: MappingProxyType[str, Unknown]
```
## References

View file

@ -398,12 +398,11 @@ from overloaded import SomeEnum, A, B, C, f
def _(x: SomeEnum):
reveal_type(f(SomeEnum.A)) # revealed: A
# TODO: This should be `B` once enums are supported and are expanded
reveal_type(f(SomeEnum.B)) # revealed: A
# TODO: This should be `C` once enums are supported and are expanded
reveal_type(f(SomeEnum.C)) # revealed: A
# TODO: This should be `A | B | C` once enums are supported and are expanded
reveal_type(f(x)) # revealed: A
reveal_type(f(SomeEnum.B)) # revealed: B
reveal_type(f(SomeEnum.C)) # revealed: C
# TODO: This should not be an error. The return type should be `A | B | C` once enums are expanded
# error: [no-matching-overload]
reveal_type(f(x)) # revealed: Unknown
```
### No matching overloads

View file

@ -0,0 +1,21 @@
# Comparison: Enums
```py
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
reveal_type(Answer.NO == Answer.NO) # revealed: Literal[True]
reveal_type(Answer.NO == Answer.YES) # revealed: Literal[False]
reveal_type(Answer.NO != Answer.NO) # revealed: Literal[False]
reveal_type(Answer.NO != Answer.YES) # revealed: Literal[True]
reveal_type(Answer.NO is Answer.NO) # revealed: Literal[True]
reveal_type(Answer.NO is Answer.YES) # revealed: Literal[False]
reveal_type(Answer.NO is not Answer.NO) # revealed: Literal[False]
reveal_type(Answer.NO is not Answer.YES) # revealed: Literal[True]
```

View file

@ -226,6 +226,28 @@ def _(target: str):
reveal_type(y) # revealed: Literal[1]
```
## Matching on enums
```py
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
def _(answer: Answer):
y = 0
match answer:
case Answer.YES:
reveal_type(answer) # revealed: Literal[Answer.YES]
y = 1
case Answer.NO:
reveal_type(answer) # revealed: Literal[Answer.NO]
y = 2
reveal_type(y) # revealed: Literal[0, 1, 2]
```
## Or match
A `|` pattern matches if any of the subpatterns match.

View file

@ -12,7 +12,7 @@ class Member:
tag: str | None = field(default=None, init=False)
# TODO: this should not include the `tag` parameter, since it has `init=False` set
# revealed: (self: Member, name: str, role: str = Unknown, tag: str | None = Unknown) -> None
# revealed: (self: Member, name: str, role: str = Literal["user"], tag: str | None = None) -> None
reveal_type(Member.__init__)
alice = Member(name="Alice", role="admin")
@ -28,8 +28,5 @@ bob = Member(name="Bob", tag="VIP")
```py
from dataclasses import field
# TODO: this should be `Literal[1]`. This is currently blocked on enum support, because
# the `dataclasses.field` overloads make use of a `_MISSING_TYPE` enum, for which we
# infer a @Todo type, and therefore pick the wrong overload.
reveal_type(field(default=1)) # revealed: Unknown
reveal_type(field(default=1)) # revealed: Literal[1]
```

View file

@ -10,8 +10,9 @@ class Color(Enum):
GREEN = 2
BLUE = 3
reveal_type(Color.RED) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Color.RED.value) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Color.RED) # revealed: Literal[Color.RED]
# TODO: This could be `Literal[1]`
reveal_type(Color.RED.value) # revealed: Any
# TODO: Should be `Color` or `Literal[Color.RED]`
reveal_type(Color["RED"]) # revealed: Unknown
@ -85,6 +86,21 @@ class Answer(Enum):
reveal_type(enum_members(Answer))
```
Enum members are allowed to be marked `Final` (without a type), even if unnecessary:
```py
from enum import Enum
from typing import Final
from ty_extensions import enum_members
class Answer(Enum):
YES: Final = 1
NO: Final = 2
# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))
```
### Non-member attributes with disallowed type
Methods, callables, descriptors (including properties), and nested classes that are defined in the
@ -208,6 +224,27 @@ class Answer(Enum):
# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))
reveal_type(Answer.DEFINITELY) # revealed: Literal[Answer.YES]
```
If a value is duplicated, we also treat that as an alias:
```py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
red = 1
green = 2
# revealed: tuple[Literal["RED"], Literal["GREEN"]]
reveal_type(enum_members(Color))
# revealed: Literal[Color.RED]
reveal_type(Color.red)
```
### Using `auto()`
@ -354,9 +391,8 @@ class Answer(Enum):
# revealed: tuple[Literal["name"], Literal["value"]]
reveal_type(enum_members(Answer))
# TODO: These should be `Answer` or `Literal[Answer.name]`/``Literal[Answer.value]`
reveal_type(Answer.name) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Answer.value) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Answer.name) # revealed: Literal[Answer.name]
reveal_type(Answer.value) # revealed: Literal[Answer.value]
```
## Iterating over enum members
@ -377,6 +413,75 @@ for color in Color:
reveal_type(list(Color)) # revealed: list[Unknown]
```
## Methods / non-member attributes
Methods and non-member attributes defined in the enum class can be accessed on enum members:
```py
from enum import Enum
class Answer(Enum):
YES = 1
NO = 2
def is_yes(self) -> bool:
return self == Answer.YES
constant: int = 1
reveal_type(Answer.YES.is_yes()) # revealed: bool
reveal_type(Answer.YES.constant) # revealed: int
class MyEnum(Enum):
def some_method(self) -> None:
pass
class MyAnswer(MyEnum):
YES = 1
NO = 2
reveal_type(MyAnswer.YES.some_method()) # revealed: None
```
## Accessing enum members from `type[…]`
```py
from enum import Enum
class Answer(Enum):
YES = 1
NO = 2
def _(answer: type[Answer]) -> None:
reveal_type(answer.YES) # revealed: Literal[Answer.YES]
reveal_type(answer.NO) # revealed: Literal[Answer.NO]
```
## Calling enum variants
```py
from enum import Enum
from typing import Callable
import sys
class Printer(Enum):
STDOUT = 1
STDERR = 2
def __call__(self, msg: str) -> None:
if self == Printer.STDOUT:
print(msg)
elif self == Printer.STDERR:
print(msg, file=sys.stderr)
Printer.STDOUT("Hello, world!")
Printer.STDERR("An error occurred!")
callable: Callable[[str], None] = Printer.STDOUT
callable("Hello again!")
callable = Printer.STDERR
callable("Another error!")
```
## Properties of enum types
### Implicitly final
@ -391,14 +496,13 @@ class Color(Enum):
GREEN = 2
BLUE = 3
# TODO: This should emit an error
# error: [subclass-of-final-class] "Class `ExtendedColor` cannot inherit from final class `Color`"
class ExtendedColor(Color):
YELLOW = 4
def f(color: Color):
if isinstance(color, int):
# TODO: This should be `Never`
reveal_type(color) # revealed: Color & int
reveal_type(color) # revealed: Never
```
An `Enum` subclass without any defined members can be subclassed:
@ -419,6 +523,43 @@ class Answer(MyEnum):
reveal_type(enum_members(Answer))
```
### Meta-type
```py
from enum import Enum
class Answer(Enum):
YES = 1
NO = 2
reveal_type(type(Answer.YES)) # revealed: <class 'Answer'>
class NoMembers(Enum): ...
def _(answer: Answer, no_members: NoMembers):
reveal_type(type(answer)) # revealed: <class 'Answer'>
reveal_type(type(no_members)) # revealed: type[NoMembers]
```
### Cyclic references
```py
from enum import Enum
from typing import Literal
from ty_extensions import enum_members
class Answer(Enum):
YES = 1
NO = 2
@classmethod
def yes(cls) -> "Literal[Answer.YES]":
return Answer.YES
# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))
```
## Custom enum types
To do: <https://typing.python.org/en/latest/spec/enums.html#enum-definition>

View file

@ -192,6 +192,21 @@ static_assert("__doc__" in all_members(len))
static_assert("__doc__" in all_members("a".startswith))
```
### Enums
```py
from ty_extensions import all_members, static_assert
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
static_assert("NO" in all_members(Answer))
static_assert("YES" in all_members(Answer))
static_assert("__members__" in all_members(Answer))
```
### Unions
For unions, `all_members` will only return members that are available on all elements of the union.

View file

@ -14,16 +14,73 @@ def _(flag: bool):
## `!=` for other singleton types
```py
def _(flag: bool):
x = True if flag else False
### Bool
```py
def _(x: bool):
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
```
### Enums
```py
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
def _(answer: Answer):
if answer != Answer.NO:
# TODO: This should be simplified to `Literal[Answer.YES]`
reveal_type(answer) # revealed: Answer & ~Literal[Answer.NO]
else:
# TODO: This should be `Literal[Answer.NO]`
reveal_type(answer) # revealed: Answer
```
This narrowing behavior is only safe if the enum has no custom `__eq__`/`__ne__` method:
```py
from enum import Enum
class AmbiguousEnum(Enum):
NO = 0
YES = 1
def __ne__(self, other) -> bool:
return True
def _(answer: AmbiguousEnum):
if answer != AmbiguousEnum.NO:
reveal_type(answer) # revealed: AmbiguousEnum
else:
reveal_type(answer) # revealed: AmbiguousEnum
```
Similar if that method is inherited from a base class:
```py
from enum import Enum
class Mixin:
def __eq__(self, other) -> bool:
return True
class AmbiguousEnum(Mixin, Enum):
NO = 0
YES = 1
def _(answer: AmbiguousEnum):
if answer == AmbiguousEnum.NO:
reveal_type(answer) # revealed: AmbiguousEnum
else:
reveal_type(answer) # revealed: AmbiguousEnum
```
## `x != y` where `y` is of literal type
```py

View file

@ -65,6 +65,23 @@ def _(flag1: bool, flag2: bool):
reveal_type(x) # revealed: Literal[1]
```
## `is` for enums
```py
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
def _(answer: Answer):
if answer is Answer.NO:
reveal_type(answer) # revealed: Literal[Answer.NO]
else:
# TODO: This should be `Literal[Answer.YES]`
reveal_type(answer) # revealed: Answer & ~Literal[Answer.NO]
```
## `is` for `EllipsisType` (Python 3.10+)
```toml

View file

@ -154,7 +154,8 @@ reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, @Todo(GenericAlias in
## `@final` classes
`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is
used as the type argument. This applies to standard-library classes and user-defined classes:
used as the type argument. This applies to standard-library classes and user-defined classes. The
same also applies to enum classes with members, which are implicitly final:
```toml
[environment]
@ -164,11 +165,17 @@ python-version = "3.10"
```py
from types import EllipsisType
from typing import final
from enum import Enum
@final
class Foo: ...
def _(x: type[Foo], y: type[EllipsisType]):
class Answer(Enum):
NO = 0
YES = 1
def _(x: type[Foo], y: type[EllipsisType], z: type[Answer]):
reveal_type(x) # revealed: <class 'Foo'>
reveal_type(y) # revealed: <class 'EllipsisType'>
reveal_type(z) # revealed: <class 'Answer'>
```

View file

@ -124,6 +124,27 @@ static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"]))
static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"]))
```
### Enum literals
```py
from ty_extensions import static_assert, is_assignable_to
from typing_extensions import Literal
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
static_assert(is_assignable_to(Literal[Answer.YES], Literal[Answer.YES]))
static_assert(is_assignable_to(Literal[Answer.YES], Answer))
static_assert(is_assignable_to(Literal[Answer.YES, Answer.NO], Answer))
# TODO: this should not be an error
# error: [static-assert-error]
static_assert(is_assignable_to(Answer, Literal[Answer.YES, Answer.NO]))
static_assert(not is_assignable_to(Literal[Answer.YES], Literal[Answer.NO]))
```
### Slice literals
The type of a slice literal is currently inferred as a specialization of `slice`.

View file

@ -297,6 +297,11 @@ static_assert(is_disjoint_from(None, Intersection[int, Not[str]]))
```py
from typing_extensions import Literal, LiteralString
from ty_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert, AlwaysFalsy, AlwaysTruthy
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
static_assert(is_disjoint_from(Literal[True], Literal[False]))
static_assert(is_disjoint_from(Literal[True], Literal[1]))
@ -310,6 +315,10 @@ static_assert(is_disjoint_from(Literal[b"a"], LiteralString))
static_assert(is_disjoint_from(Literal[b"a"], Literal[b"b"]))
static_assert(is_disjoint_from(Literal[b"a"], Literal["a"]))
static_assert(is_disjoint_from(Literal[Answer.YES], Literal[Answer.NO]))
static_assert(is_disjoint_from(Literal[Answer.YES], int))
static_assert(not is_disjoint_from(Literal[Answer.YES], Answer))
static_assert(is_disjoint_from(type[object], TypeOf[Literal]))
static_assert(is_disjoint_from(type[str], LiteralString))
@ -705,3 +714,28 @@ static_assert(not is_disjoint_from(TypeOf[Deque], Callable[..., Any]))
static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[OrderedDict]))
static_assert(not is_disjoint_from(TypeOf[OrderedDict], Callable[..., Any]))
```
## Custom enum classes
```py
from enum import Enum
from ty_extensions import is_disjoint_from, static_assert
from typing_extensions import Literal
class MyEnum(Enum):
def special_method(self):
pass
class MyAnswer(MyEnum):
NO = 0
YES = 1
class UnrelatedClass:
pass
static_assert(is_disjoint_from(Literal[MyAnswer.NO], Literal[MyAnswer.YES]))
static_assert(is_disjoint_from(Literal[MyAnswer.NO], UnrelatedClass))
static_assert(not is_disjoint_from(Literal[MyAnswer.NO], MyAnswer))
static_assert(not is_disjoint_from(Literal[MyAnswer.NO], MyEnum))
```

View file

@ -15,6 +15,11 @@ materializations of `B`, and all materializations of `B` are also materializatio
```py
from typing_extensions import Literal, LiteralString, Never
from ty_extensions import Unknown, is_equivalent_to, static_assert, TypeOf, AlwaysTruthy, AlwaysFalsy
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2]))
static_assert(is_equivalent_to(type[object], type))
@ -25,6 +30,13 @@ static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2]))
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3]))
static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2]))
static_assert(is_equivalent_to(Literal[Answer.YES], Literal[Answer.YES]))
# TODO: these should be equivalent
# error: [static-assert-error]
static_assert(is_equivalent_to(Literal[Answer.YES, Answer.NO], Answer))
static_assert(not is_equivalent_to(Literal[Answer.YES], Literal[Answer.NO]))
static_assert(not is_equivalent_to(Literal[Answer.YES], Answer))
static_assert(is_equivalent_to(Never, Never))
static_assert(is_equivalent_to(AlwaysTruthy, AlwaysTruthy))
static_assert(is_equivalent_to(AlwaysFalsy, AlwaysFalsy))

View file

@ -34,3 +34,49 @@ static_assert(is_single_valued(TypeOf[A().method]))
static_assert(is_single_valued(TypeOf[types.FunctionType.__get__]))
static_assert(is_single_valued(TypeOf[A.method.__get__]))
```
An enum literal is only considered single-valued if it has no custom `__eq__`/`__ne__` method, or if
these methods always return `True`/`False`, respectively. Otherwise, the single member of the enum
literal type might not compare equal to itself.
```py
from ty_extensions import is_single_valued, static_assert, TypeOf
from enum import Enum
class NormalEnum(Enum):
NO = 0
YES = 1
class ComparesEqualEnum(Enum):
NO = 0
YES = 1
def __eq__(self, other: object) -> Literal[True]:
return True
class CustomEqEnum(Enum):
NO = 0
YES = 1
def __eq__(self, other: object) -> bool:
return False
class CustomNeEnum(Enum):
NO = 0
YES = 1
def __ne__(self, other: object) -> bool:
return False
static_assert(is_single_valued(Literal[NormalEnum.NO]))
static_assert(is_single_valued(Literal[NormalEnum.YES]))
static_assert(is_single_valued(Literal[ComparesEqualEnum.NO]))
static_assert(is_single_valued(Literal[ComparesEqualEnum.YES]))
static_assert(not is_single_valued(Literal[CustomEqEnum.NO]))
static_assert(not is_single_valued(Literal[CustomEqEnum.YES]))
static_assert(not is_single_valued(Literal[CustomNeEnum.NO]))
static_assert(not is_single_valued(Literal[CustomNeEnum.YES]))
```

View file

@ -7,10 +7,17 @@ A type is a singleton type iff it has exactly one inhabitant.
```py
from typing_extensions import Literal, Never, Callable
from ty_extensions import is_singleton, static_assert
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
static_assert(is_singleton(None))
static_assert(is_singleton(Literal[True]))
static_assert(is_singleton(Literal[False]))
static_assert(is_singleton(Literal[Answer.YES]))
static_assert(is_singleton(Literal[Answer.NO]))
static_assert(is_singleton(type[bool]))

View file

@ -90,6 +90,11 @@ static_assert(is_subtype_of(C, object))
```py
from typing_extensions import Literal, LiteralString
from ty_extensions import is_subtype_of, static_assert, TypeOf, JustFloat
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
# Boolean literals
static_assert(is_subtype_of(Literal[True], bool))
@ -115,6 +120,16 @@ static_assert(is_subtype_of(LiteralString, object))
# Bytes literals
static_assert(is_subtype_of(Literal[b"foo"], bytes))
static_assert(is_subtype_of(Literal[b"foo"], object))
# Enum literals
static_assert(is_subtype_of(Literal[Answer.YES], Literal[Answer.YES]))
static_assert(is_subtype_of(Literal[Answer.YES], Answer))
static_assert(is_subtype_of(Literal[Answer.YES, Answer.NO], Answer))
# TODO: this should not be an error
# error: [static-assert-error]
static_assert(is_subtype_of(Answer, Literal[Answer.YES, Answer.NO]))
static_assert(not is_subtype_of(Literal[Answer.YES], Literal[Answer.NO]))
```
## Heterogeneous tuple types

View file

@ -80,6 +80,11 @@ The top / bottom (and only) materialization of any fully static type is just its
```py
from typing import Any, Literal
from ty_extensions import TypeOf, bottom_materialization, top_materialization
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
reveal_type(top_materialization(int)) # revealed: int
reveal_type(bottom_materialization(int)) # revealed: int
@ -93,6 +98,9 @@ reveal_type(bottom_materialization(Literal[True])) # revealed: Literal[True]
reveal_type(top_materialization(Literal["abc"])) # revealed: Literal["abc"]
reveal_type(bottom_materialization(Literal["abc"])) # revealed: Literal["abc"]
reveal_type(top_materialization(Literal[Answer.YES])) # revealed: Literal[Answer.YES]
reveal_type(bottom_materialization(Literal[Answer.YES])) # revealed: Literal[Answer.YES]
reveal_type(top_materialization(int | str)) # revealed: int | str
reveal_type(bottom_materialization(int | str)) # revealed: int | str
```

View file

@ -2,26 +2,35 @@
```py
from typing_extensions import Literal, LiteralString
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
def _(
a: Literal[1],
b: Literal[True],
c: Literal[False],
d: Literal["ab'cd"],
e: LiteralString,
f: int,
e: Literal[Answer.YES],
f: LiteralString,
g: int,
):
reveal_type(str(a)) # revealed: Literal["1"]
reveal_type(str(b)) # revealed: Literal["True"]
reveal_type(str(c)) # revealed: Literal["False"]
reveal_type(str(d)) # revealed: Literal["ab'cd"]
reveal_type(str(e)) # revealed: LiteralString
reveal_type(str(f)) # revealed: str
reveal_type(str(e)) # revealed: Literal["Answer.YES"]
reveal_type(str(f)) # revealed: LiteralString
reveal_type(str(g)) # revealed: str
reveal_type(repr(a)) # revealed: Literal["1"]
reveal_type(repr(b)) # revealed: Literal["True"]
reveal_type(repr(c)) # revealed: Literal["False"]
reveal_type(repr(d)) # revealed: Literal["'ab\\'cd'"]
reveal_type(repr(e)) # revealed: LiteralString
reveal_type(repr(f)) # revealed: str
# TODO: this could be `<Answer.YES: 1>`
reveal_type(repr(e)) # revealed: str
reveal_type(repr(f)) # revealed: LiteralString
reveal_type(repr(g)) # revealed: str
```

View file

@ -143,3 +143,65 @@ reveal_type(bool(A().method)) # revealed: Literal[True]
reveal_type(bool(f.__get__)) # revealed: Literal[True]
reveal_type(bool(FunctionType.__get__)) # revealed: Literal[True]
```
## Enums
```py
from enum import Enum
from typing import Literal
class NormalEnum(Enum):
NO = 0
YES = 1
class FalsyEnum(Enum):
NO = 0
YES = 1
def __bool__(self) -> Literal[False]:
return False
class AmbiguousEnum(Enum):
NO = 0
YES = 1
def __bool__(self) -> bool:
return self is AmbiguousEnum.YES
class AmbiguousBase(Enum):
def __bool__(self) -> bool:
return True
class AmbiguousEnum2(AmbiguousBase):
NO = 0
YES = 1
class CustomLenEnum(Enum):
NO = 0
YES = 1
def __len__(self):
return 0
# TODO: these could be `Literal[True]`
reveal_type(bool(NormalEnum.NO)) # revealed: bool
reveal_type(bool(NormalEnum.YES)) # revealed: bool
# TODO: these could be `Literal[False]`
reveal_type(bool(FalsyEnum.NO)) # revealed: bool
reveal_type(bool(FalsyEnum.YES)) # revealed: bool
# All of the following must be `bool`:
reveal_type(bool(AmbiguousEnum.NO)) # revealed: bool
reveal_type(bool(AmbiguousEnum.YES)) # revealed: bool
reveal_type(bool(AmbiguousEnum2.NO)) # revealed: bool
reveal_type(bool(AmbiguousEnum2.YES)) # revealed: bool
reveal_type(bool(AmbiguousEnum2.NO)) # revealed: bool
reveal_type(bool(AmbiguousEnum2.YES)) # revealed: bool
reveal_type(bool(CustomLenEnum.NO)) # revealed: bool
reveal_type(bool(CustomLenEnum.YES)) # revealed: bool
```