mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-10 02:12:09 +00:00

## 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`.
3.6 KiB
3.6 KiB
Literal
https://typing.python.org/en/latest/spec/literal.html#literals
Parameterization
from typing import Literal
from enum import Enum
mode: Literal["w", "r"]
a1: Literal[26]
a2: Literal[0x1A]
a3: Literal[-4]
a4: Literal["hello world"]
a5: Literal[b"hello world"]
a6: Literal[True]
a7: Literal[None]
a8: Literal[Literal[1]]
class Color(Enum):
RED = 0
GREEN = 1
BLUE = 2
b1: Literal[Color.RED]
def f():
reveal_type(mode) # revealed: Literal["w", "r"]
reveal_type(a1) # revealed: Literal[26]
reveal_type(a2) # revealed: Literal[26]
reveal_type(a3) # revealed: Literal[-4]
reveal_type(a4) # revealed: Literal["hello world"]
reveal_type(a5) # revealed: Literal[b"hello world"]
reveal_type(a6) # revealed: Literal[True]
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
reveal_type(b1) # revealed: Literal[Color.RED]
# error: [invalid-type-form]
invalid1: Literal[3 + 4]
# error: [invalid-type-form]
invalid2: Literal[4 + 3j]
# error: [invalid-type-form]
invalid3: Literal[(3, 4)]
hello = "hello"
invalid4: Literal[
1 + 2, # error: [invalid-type-form]
"foo",
hello, # error: [invalid-type-form]
(1, 2, 3), # error: [invalid-type-form]
]
Shortening unions of literals
When a Literal is parameterized with more than one value, it’s treated as exactly to equivalent to the union of those types.
from typing import Literal
def x(
a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None],
a2: Literal["w"] | Literal["r"],
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
):
reveal_type(a1) # revealed: Literal[1, 2, 3, 5, "foo"] | None
reveal_type(a2) # revealed: Literal["w", "r"]
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
Display of heterogeneous unions of literals
from typing import Literal, Union
def foo(x: int) -> int:
return x + 1
def bar(s: str) -> str:
return s
class A: ...
class B: ...
def union_example(
x: Union[
# unknown type
# error: [unresolved-reference]
y,
Literal[-1],
Literal["A"],
Literal[b"A"],
Literal[b"\x00"],
Literal[b"\x07"],
Literal[0],
Literal[1],
Literal["B"],
Literal["foo"],
Literal["bar"],
Literal["B"],
Literal[True],
None,
],
):
reveal_type(x) # revealed: Unknown | Literal[-1, 0, 1, "A", "B", "foo", "bar", b"A", b"\x00", b"\x07", True] | None
Detecting Literal outside typing and typing_extensions
Only Literal that is defined in typing and typing_extension modules is detected as the special Literal.
other.pyi
:
from typing import _SpecialForm
Literal: _SpecialForm
from other import Literal
# TODO: can we add a subdiagnostic here saying something like:
#
# `other.Literal` and `typing.Literal` have similar names, but are different symbols and don't have the same semantics
#
# ?
#
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
a1: Literal[26]
def f():
reveal_type(a1) # revealed: @Todo(unknown type subscript)
Detecting typing_extensions.Literal
from typing_extensions import Literal
a1: Literal[26]
def f():
reveal_type(a1) # revealed: Literal[26]
Invalid
from typing import Literal
# error: [invalid-type-form] "`typing.Literal` requires at least one argument when used in a type expression"
def _(x: Literal):
reveal_type(x) # revealed: Unknown