mirror of
https://github.com/astral-sh/ruff
synced 2026-01-08 15:14:19 -05:00
[ty] Improve rendering of default values for function args (#22010)
## Summary We're actually quite good at computing this but the main issue is just that we compute it at the type-level and so wrap it in `Literal[...]`. So just special-case the rendering of these to omit `Literal[...]` and fallback to `...` in cases where the thing we'll show is probably useless (i.e. `x: str = str`). Fixes https://github.com/astral-sh/ty/issues/1882
This commit is contained in:
@@ -57,7 +57,7 @@ We can access attributes on objects of all kinds:
|
||||
import sys
|
||||
|
||||
reveal_type(inspect.getattr_static(sys, "dont_write_bytecode")) # revealed: bool
|
||||
# revealed: def getattr_static(obj: object, attr: str, default: Any | None = EllipsisType) -> Any
|
||||
# revealed: def getattr_static(obj: object, attr: str, default: Any | None = ...) -> Any
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static"))
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: property
|
||||
@@ -144,7 +144,7 @@ from typing import Any
|
||||
def _(a: Any, tuple_of_any: tuple[Any]):
|
||||
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
|
||||
|
||||
# revealed: def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int
|
||||
# revealed: def index(self, value: Any, start: SupportsIndex = 0, stop: SupportsIndex = ..., /) -> int
|
||||
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default"))
|
||||
```
|
||||
|
||||
|
||||
@@ -598,9 +598,9 @@ from typing_extensions import Self
|
||||
|
||||
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
reveal_type(int.__new__)
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
reveal_type((42).__new__)
|
||||
|
||||
class X:
|
||||
|
||||
@@ -36,7 +36,7 @@ class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
reveal_type(Point.__replace__) # revealed: (self: Point, *, x: int = int, y: int = int) -> Point
|
||||
reveal_type(Point.__replace__) # revealed: (self: Point, *, x: int = ..., y: int = ...) -> Point
|
||||
```
|
||||
|
||||
The `__replace__` method can either be called directly or through the `replace` function:
|
||||
|
||||
@@ -87,25 +87,25 @@ class C:
|
||||
def inner_a(positional=self.a):
|
||||
return
|
||||
self.a = inner_a
|
||||
# revealed: def inner_a(positional=Unknown | (def inner_a(positional=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: def inner_a(positional=...) -> Unknown
|
||||
reveal_type(inner_a)
|
||||
|
||||
def inner_b(*, kw_only=self.b):
|
||||
return
|
||||
self.b = inner_b
|
||||
# revealed: def inner_b(*, kw_only=Unknown | (def inner_b(*, kw_only=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: def inner_b(*, kw_only=...) -> Unknown
|
||||
reveal_type(inner_b)
|
||||
|
||||
def inner_c(positional_only=self.c, /):
|
||||
return
|
||||
self.c = inner_c
|
||||
# revealed: def inner_c(positional_only=Unknown | (def inner_c(positional_only=Unknown, /) -> Unknown), /) -> Unknown
|
||||
# revealed: def inner_c(positional_only=..., /) -> Unknown
|
||||
reveal_type(inner_c)
|
||||
|
||||
def inner_d(*, kw_only=self.d):
|
||||
return
|
||||
self.d = inner_d
|
||||
# revealed: def inner_d(*, kw_only=Unknown | (def inner_d(*, kw_only=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: def inner_d(*, kw_only=...) -> Unknown
|
||||
reveal_type(inner_d)
|
||||
```
|
||||
|
||||
@@ -114,7 +114,7 @@ We do, however, still check assignability of the default value to the parameter
|
||||
```py
|
||||
class D:
|
||||
def f(self: "D"):
|
||||
# error: [invalid-parameter-default] "Default value of type `Unknown | (def inner_a(a: int = Unknown | (def inner_a(a: int = Unknown) -> Unknown)) -> Unknown)` is not assignable to annotated parameter type `int`"
|
||||
# error: [invalid-parameter-default] "Default value of type `Unknown | (def inner_a(a: int = ...) -> Unknown)` is not assignable to annotated parameter type `int`"
|
||||
def inner_a(a: int = self.a): ...
|
||||
self.a = inner_a
|
||||
```
|
||||
@@ -129,16 +129,16 @@ class C:
|
||||
self.c = lambda positional_only=self.c, /: positional_only
|
||||
self.d = lambda *, kw_only=self.d: kw_only
|
||||
|
||||
# revealed: (positional=Unknown | ((positional=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: (positional=...) -> Unknown
|
||||
reveal_type(self.a)
|
||||
|
||||
# revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: (*, kw_only=...) -> Unknown
|
||||
reveal_type(self.b)
|
||||
|
||||
# revealed: (positional_only=Unknown | ((positional_only=Unknown, /) -> Unknown), /) -> Unknown
|
||||
# revealed: (positional_only=..., /) -> Unknown
|
||||
reveal_type(self.c)
|
||||
|
||||
# revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: (*, kw_only=...) -> Unknown
|
||||
reveal_type(self.d)
|
||||
```
|
||||
|
||||
|
||||
@@ -749,8 +749,8 @@ class Outer:
|
||||
outer_a: int = outer_field(init=False)
|
||||
outer_b: str = inner_field(init=False)
|
||||
|
||||
reveal_type(Outer.__init__) # revealed: (self: Outer, outer_b: str = Any) -> None
|
||||
reveal_type(Outer.Inner.__init__) # revealed: (self: Inner, inner_b: str = Any) -> None
|
||||
reveal_type(Outer.__init__) # revealed: (self: Outer, outer_b: str = ...) -> None
|
||||
reveal_type(Outer.Inner.__init__) # revealed: (self: Inner, inner_b: str = ...) -> None
|
||||
```
|
||||
|
||||
## Overloaded dataclass-like decorators
|
||||
|
||||
@@ -69,7 +69,7 @@ class D:
|
||||
y: str = "default"
|
||||
z: int | None = 1 + 2
|
||||
|
||||
reveal_type(D.__init__) # revealed: (self: D, x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
|
||||
reveal_type(D.__init__) # revealed: (self: D, x: int, y: str = "default", z: int | None = 3) -> None
|
||||
```
|
||||
|
||||
This also works if the declaration and binding are split:
|
||||
@@ -221,7 +221,7 @@ class D:
|
||||
(x): int = 1
|
||||
|
||||
# TODO: should ideally not include a `x` parameter
|
||||
reveal_type(D.__init__) # revealed: (self: D, x: int = Literal[1]) -> None
|
||||
reveal_type(D.__init__) # revealed:(self: D, x: int = 1) -> None
|
||||
```
|
||||
|
||||
## `@dataclass` calls with arguments
|
||||
@@ -670,7 +670,7 @@ class A:
|
||||
a: str = field(kw_only=False)
|
||||
b: int = 0
|
||||
|
||||
reveal_type(A.__init__) # revealed: (self: A, a: str, *, b: int = Literal[0]) -> None
|
||||
reveal_type(A.__init__) # revealed:(self: A, a: str, *, b: int = 0) -> None
|
||||
|
||||
A("hi")
|
||||
```
|
||||
@@ -991,7 +991,7 @@ class C:
|
||||
class_variable1: ClassVar[Final[int]] = 1
|
||||
class_variable2: ClassVar[Final[int]] = 1
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
|
||||
reveal_type(C.__init__) # revealed:(self: C, instance_variable_no_default: int, instance_variable: int = 1) -> None
|
||||
|
||||
c = C(1)
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `instance_variable` on type `C`"
|
||||
@@ -1082,7 +1082,7 @@ class C(Base):
|
||||
z: int = 10
|
||||
x: int = 15
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
|
||||
reveal_type(C.__init__) # revealed:(self: C, x: int = 15, y: int = 0, z: int = 10) -> None
|
||||
```
|
||||
|
||||
## Conditionally defined fields
|
||||
@@ -1243,7 +1243,7 @@ class UppercaseString:
|
||||
class C:
|
||||
upper: UppercaseString = UppercaseString()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, upper: str = str) -> None
|
||||
reveal_type(C.__init__) # revealed: (self: C, upper: str = ...) -> None
|
||||
|
||||
c = C("abc")
|
||||
reveal_type(c.upper) # revealed: str
|
||||
@@ -1289,7 +1289,7 @@ class ConvertToLength:
|
||||
class C:
|
||||
converter: ConvertToLength = ConvertToLength()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, converter: str = Literal[""]) -> None
|
||||
reveal_type(C.__init__) # revealed: (self: C, converter: str = "") -> None
|
||||
|
||||
c = C("abc")
|
||||
reveal_type(c.converter) # revealed: int
|
||||
@@ -1328,7 +1328,7 @@ class AcceptsStrAndInt:
|
||||
class C:
|
||||
field: AcceptsStrAndInt = AcceptsStrAndInt()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, field: str | int = int) -> None
|
||||
reveal_type(C.__init__) # revealed: (self: C, field: str | int = ...) -> None
|
||||
```
|
||||
|
||||
## `dataclasses.field`
|
||||
|
||||
@@ -11,7 +11,7 @@ class Member:
|
||||
role: str = field(default="user")
|
||||
tag: str | None = field(default=None, init=False)
|
||||
|
||||
# revealed: (self: Member, name: str, role: str = Literal["user"]) -> None
|
||||
# revealed: (self: Member, name: str, role: str = "user") -> None
|
||||
reveal_type(Member.__init__)
|
||||
|
||||
alice = Member(name="Alice", role="admin")
|
||||
@@ -37,7 +37,7 @@ class Data:
|
||||
content: list[int] = field(default_factory=list)
|
||||
timestamp: datetime = field(default_factory=datetime.now, init=False)
|
||||
|
||||
# revealed: (self: Data, content: list[int] = list[int]) -> None
|
||||
# revealed: (self: Data, content: list[int] = ...) -> None
|
||||
reveal_type(Data.__init__)
|
||||
|
||||
data = Data([1, 2, 3])
|
||||
@@ -63,7 +63,7 @@ class Person:
|
||||
age: int | None = field(default=None, kw_only=True)
|
||||
role: str = field(default="user", kw_only=True)
|
||||
|
||||
# revealed: (self: Person, name: str, *, age: int | None = None, role: str = Literal["user"]) -> None
|
||||
# revealed: (self: Person, name: str, *, age: int | None = None, role: str = "user") -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
alice = Person(role="admin", name="Alice")
|
||||
|
||||
@@ -591,9 +591,9 @@ try:
|
||||
reveal_type(x) # revealed: B | D
|
||||
reveal_type(x) # revealed: B | D
|
||||
x = foo
|
||||
reveal_type(x) # revealed: def foo(param=A) -> Unknown
|
||||
reveal_type(x) # revealed: def foo(param=...) -> Unknown
|
||||
except:
|
||||
reveal_type(x) # revealed: Literal[1] | (def foo(param=A) -> Unknown)
|
||||
reveal_type(x) # revealed: Literal[1] | (def foo(param=...) -> Unknown)
|
||||
|
||||
class Bar:
|
||||
x = could_raise_returns_E()
|
||||
@@ -603,9 +603,9 @@ except:
|
||||
reveal_type(x) # revealed: <class 'Bar'>
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | <class 'foo'> | <class 'Bar'>`
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | <class 'Bar'>
|
||||
reveal_type(x) # revealed: (def foo(param=...) -> Unknown) | <class 'Bar'>
|
||||
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | <class 'Bar'>
|
||||
reveal_type(x) # revealed: (def foo(param=...) -> Unknown) | <class 'Bar'>
|
||||
```
|
||||
|
||||
[1]: https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d
|
||||
|
||||
@@ -24,8 +24,8 @@ reveal_type(lambda a, b: a + b) # revealed: (a, b) -> Unknown
|
||||
But, it can have default values:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> Unknown
|
||||
reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> Unknown
|
||||
reveal_type(lambda a=1: a) # revealed: (a=1) -> Unknown
|
||||
reveal_type(lambda a, b=2: a) # revealed: (a, b=2) -> Unknown
|
||||
```
|
||||
|
||||
And, positional-only parameters:
|
||||
@@ -37,7 +37,7 @@ reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> Unknown
|
||||
And, keyword-only parameters:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> Unknown
|
||||
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=2, c) -> Unknown
|
||||
```
|
||||
|
||||
And, variadic parameter:
|
||||
@@ -55,7 +55,7 @@ reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> Unknown
|
||||
Mixing all of them together:
|
||||
|
||||
```py
|
||||
# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> Unknown
|
||||
# revealed: (a, b, /, c=True, *args, *, d="default", e=5, **kwargs) -> Unknown
|
||||
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
|
||||
```
|
||||
|
||||
@@ -94,7 +94,7 @@ Here, a `lambda` expression is used as the default value for a parameter in anot
|
||||
expression.
|
||||
|
||||
```py
|
||||
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> Unknown) -> Unknown
|
||||
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=...) -> Unknown
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
@@ -38,7 +38,7 @@ class Product(BaseModel):
|
||||
name: str = Field(..., kw_only=False, min_length=1)
|
||||
internal_price_cent: int = Field(..., gt=0, alias="price_cent")
|
||||
|
||||
reveal_type(Product.__init__) # revealed: (self: Product, name: str = Any, *, price_cent: int = Any) -> None
|
||||
reveal_type(Product.__init__) # revealed: (self: Product, name: str = ..., *, price_cent: int = ...) -> None
|
||||
|
||||
product = Product("Laptop", price_cent=999_00)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class User:
|
||||
id: int
|
||||
role: str = strawberry.field(default="user")
|
||||
|
||||
reveal_type(User.__init__) # revealed: (self: User, *, id: int, role: str = Any) -> None
|
||||
reveal_type(User.__init__) # revealed: (self: User, *, id: int, role: str = ...) -> None
|
||||
|
||||
user = User(id=1)
|
||||
reveal_type(user.id) # revealed: int
|
||||
|
||||
@@ -477,7 +477,7 @@ def keyword_only_with_default_2(*, y: int = 42) -> int:
|
||||
# parameter list i.e., `()`
|
||||
# TODO: This shouldn't error
|
||||
# error: [invalid-argument-type]
|
||||
# revealed: (*, x: int = Literal[42]) -> bool
|
||||
# revealed: (*, x: int = 42) -> bool
|
||||
reveal_type(multiple(keyword_only_with_default_1, keyword_only_with_default_2))
|
||||
|
||||
def keyword_only1(*, x: int) -> int:
|
||||
|
||||
@@ -97,7 +97,7 @@ inside the module:
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = EllipsisType) -> None
|
||||
reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ...) -> None
|
||||
|
||||
# For a stub module, we don't know that `__file__` is a string (at runtime it may be entirely
|
||||
# unset, but we follow typeshed here):
|
||||
|
||||
@@ -73,8 +73,8 @@ error[missing-argument]: No argument provided for required parameter `a` of func
|
||||
13 |
|
||||
14 | Foo().method() # error: [missing-argument]
|
||||
|
|
||||
info: Union variant `def f(a, b=Literal[42]) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: Union variant `def f(a, b=42) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b=42) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: rule `missing-argument` is enabled by default
|
||||
|
||||
```
|
||||
@@ -91,7 +91,7 @@ error[missing-argument]: No argument provided for required parameter `a` of func
|
||||
14 | Foo().method() # error: [missing-argument]
|
||||
|
|
||||
info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: Attempted to call union type `(def f(a, b=42) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: rule `missing-argument` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -73,8 +73,8 @@ error[too-many-positional-arguments]: Too many positional arguments to function
|
||||
13 |
|
||||
14 | Foo().method(1, 2) # error: [too-many-positional-arguments]
|
||||
|
|
||||
info: Union variant `def f(a, b=Literal[42]) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: Union variant `def f(a, b=42) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b=42) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: rule `too-many-positional-arguments` is enabled by default
|
||||
|
||||
```
|
||||
@@ -91,7 +91,7 @@ error[too-many-positional-arguments]: Too many positional arguments to function
|
||||
14 | Foo().method(1, 2) # error: [too-many-positional-arguments]
|
||||
|
|
||||
info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: Attempted to call union type `(def f(a, b=42) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: rule `too-many-positional-arguments` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -73,8 +73,8 @@ error[unknown-argument]: Argument `d` does not match any known parameter of func
|
||||
13 |
|
||||
14 | Foo().method(a=1, b=2, c=3) # error: [unknown-argument]
|
||||
|
|
||||
info: Union variant `def f(a, b, c=Literal[42]) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b, c=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: Union variant `def f(a, b, c=42) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b, c=42) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: rule `unknown-argument` is enabled by default
|
||||
|
||||
```
|
||||
@@ -91,7 +91,7 @@ error[unknown-argument]: Argument `d` does not match any known parameter of func
|
||||
14 | Foo().method(a=1, b=2, c=3) # error: [unknown-argument]
|
||||
|
|
||||
info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f(a, b, c=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: Attempted to call union type `(def f(a, b, c=42) -> Unknown) | (def g(a, b) -> Unknown)`
|
||||
info: rule `unknown-argument` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -67,7 +67,7 @@ class Person:
|
||||
|
||||
metadata: InitVar[str] = "default"
|
||||
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int, metadata: str = Literal["default"]) -> None
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int, metadata: str = "default") -> None
|
||||
|
||||
alice = Person("Alice", 30)
|
||||
bob = Person("Bob", 25, "custom metadata")
|
||||
|
||||
Reference in New Issue
Block a user