mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[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:
parent
a0d4e1f854
commit
a1edb69ea5
36 changed files with 1016 additions and 75 deletions
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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]
|
||||
```
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'>
|
||||
```
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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))
|
||||
```
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]))
|
||||
```
|
||||
|
|
|
@ -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]))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue