mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] support kw_only=True
for dataclass()
and field()
(#19677)
Some checks are pending
CI / mkdocs (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / mkdocs (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary https://github.com/astral-sh/ty/issues/111 adds support for `@dataclass(kw_only=True)` (https://docs.python.org/3/library/dataclasses.html) ## Test Plan - new mdtests - triaged conformance diffs (notes here: https://diffswarm.dev/d-01k2gknwyq82f6x17zqf3apjxc) - `mypy_primer` no-op
This commit is contained in:
parent
9aaa82d037
commit
dc2e8ab377
6 changed files with 155 additions and 10 deletions
|
@ -195,7 +195,41 @@ OrderTrueOverwritten(1) < OrderTrueOverwritten(2)
|
|||
|
||||
### `kw_only_default`
|
||||
|
||||
To do
|
||||
When provided, sets the default value for the `kw_only` parameter of `field()`.
|
||||
|
||||
```py
|
||||
from typing import dataclass_transform
|
||||
from dataclasses import field
|
||||
|
||||
@dataclass_transform(kw_only_default=True)
|
||||
def create_model(*, init=True): ...
|
||||
@create_model()
|
||||
class A:
|
||||
name: str
|
||||
|
||||
a = A(name="Harry")
|
||||
# error: [missing-argument]
|
||||
# error: [too-many-positional-arguments]
|
||||
a = A("Harry")
|
||||
```
|
||||
|
||||
TODO: This can be overridden by the call to the decorator function.
|
||||
|
||||
```py
|
||||
from typing import dataclass_transform
|
||||
|
||||
@dataclass_transform(kw_only_default=True)
|
||||
def create_model(*, kw_only: bool = True): ...
|
||||
@create_model(kw_only=False)
|
||||
class CustomerModel:
|
||||
id: int
|
||||
name: str
|
||||
|
||||
# TODO: Should not emit errors
|
||||
# error: [missing-argument]
|
||||
# error: [too-many-positional-arguments]
|
||||
c = CustomerModel(1, "Harry")
|
||||
```
|
||||
|
||||
### `field_specifiers`
|
||||
|
||||
|
|
|
@ -465,7 +465,84 @@ To do
|
|||
|
||||
### `kw_only`
|
||||
|
||||
To do
|
||||
An error is emitted if a dataclass is defined with `kw_only=True` and positional arguments are
|
||||
passed to the constructor.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class A:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
# error: [missing-argument] "No arguments provided for required parameters `x`, `y`"
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 2"
|
||||
a = A(1, 2)
|
||||
a = A(x=1, y=2)
|
||||
```
|
||||
|
||||
The class-level parameter can be overridden per-field.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class A:
|
||||
a: str = field(kw_only=False)
|
||||
b: int = 0
|
||||
|
||||
A("hi")
|
||||
```
|
||||
|
||||
If some fields are `kw_only`, they should appear after all positional fields in the `__init__`
|
||||
signature.
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class A:
|
||||
b: int = field(kw_only=True, default=3)
|
||||
a: str
|
||||
|
||||
A("hi")
|
||||
```
|
||||
|
||||
The field-level `kw_only` value takes precedence over the `KW_ONLY` pseudo-type.
|
||||
|
||||
```py
|
||||
from dataclasses import field, dataclass, KW_ONLY
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
_: KW_ONLY
|
||||
x: int = field(kw_only=False)
|
||||
|
||||
C(x=1)
|
||||
C(1)
|
||||
```
|
||||
|
||||
### `kw_only` - Python < 3.10
|
||||
|
||||
For Python < 3.10, `kw_only` is not supported.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(kw_only=True) # TODO: Emit a diagnostic here
|
||||
class A:
|
||||
x: int
|
||||
y: int
|
||||
```
|
||||
|
||||
### `slots`
|
||||
|
||||
|
|
|
@ -63,13 +63,12 @@ class Person:
|
|||
age: int | None = field(default=None, kw_only=True)
|
||||
role: str = field(default="user", kw_only=True)
|
||||
|
||||
# TODO: the `age` and `role` fields should be keyword-only
|
||||
# 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 = Literal["user"]) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
alice = Person(role="admin", name="Alice")
|
||||
|
||||
# TODO: this should be an error
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
|
||||
bob = Person("Bob", 30)
|
||||
```
|
||||
|
||||
|
|
|
@ -6787,6 +6787,9 @@ pub struct FieldInstance<'db> {
|
|||
|
||||
/// Whether this field is part of the `__init__` signature, or not.
|
||||
pub init: bool,
|
||||
|
||||
/// Whether or not this field can only be passed as a keyword argument to `__init__`.
|
||||
pub kw_only: Option<bool>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
|
@ -6798,6 +6801,7 @@ impl<'db> FieldInstance<'db> {
|
|||
db,
|
||||
self.default_type(db).normalized_impl(db, visitor),
|
||||
self.init(db),
|
||||
self.kw_only(db),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use ruff_db::parsed::parsed_module;
|
|||
use smallvec::{SmallVec, smallvec, smallvec_inline};
|
||||
|
||||
use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type};
|
||||
use crate::Program;
|
||||
use crate::db::Db;
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::place::{Boundness, Place};
|
||||
|
@ -33,7 +34,7 @@ use crate::types::{
|
|||
WrapperDescriptorKind, enums, ide_support, todo_type,
|
||||
};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{self as ast, PythonVersion};
|
||||
|
||||
/// Binding information for a possible union of callables. At a call site, the arguments must be
|
||||
/// compatible with _all_ of the types in the union for the call to be valid.
|
||||
|
@ -860,7 +861,11 @@ impl<'db> Bindings<'db> {
|
|||
params |= DataclassParams::MATCH_ARGS;
|
||||
}
|
||||
if to_bool(kw_only, false) {
|
||||
if Program::get(db).python_version(db) >= PythonVersion::PY310 {
|
||||
params |= DataclassParams::KW_ONLY;
|
||||
} else {
|
||||
// TODO: emit diagnostic
|
||||
}
|
||||
}
|
||||
if to_bool(slots, false) {
|
||||
params |= DataclassParams::SLOTS;
|
||||
|
@ -919,7 +924,9 @@ impl<'db> Bindings<'db> {
|
|||
}
|
||||
|
||||
Some(KnownFunction::Field) => {
|
||||
if let [default, default_factory, init, ..] = overload.parameter_types()
|
||||
// TODO this will break on Python 3.14 -- we should match by parameter name instead
|
||||
if let [default, default_factory, init, .., kw_only] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
let default_ty = match (default, default_factory) {
|
||||
(Some(default_ty), _) => *default_ty,
|
||||
|
@ -933,6 +940,14 @@ impl<'db> Bindings<'db> {
|
|||
.map(|init| !init.bool(db).is_always_false())
|
||||
.unwrap_or(true);
|
||||
|
||||
let kw_only = if Program::get(db).python_version(db)
|
||||
>= PythonVersion::PY310
|
||||
{
|
||||
kw_only.map(|kw_only| !kw_only.bool(db).is_always_false())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// `typeshed` pretends that `dataclasses.field()` returns the type of the
|
||||
// default value directly. At runtime, however, this function returns an
|
||||
// instance of `dataclasses.Field`. We also model it this way and return
|
||||
|
@ -942,7 +957,7 @@ impl<'db> Bindings<'db> {
|
|||
// to `T`. Otherwise, we would error on `name: str = field(default="")`.
|
||||
overload.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::Field(FieldInstance::new(
|
||||
db, default_ty, init,
|
||||
db, default_ty, init, kw_only,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -1171,6 +1171,9 @@ pub(crate) struct Field<'db> {
|
|||
|
||||
/// Whether or not this field should appear in the signature of `__init__`.
|
||||
pub(crate) init: bool,
|
||||
|
||||
/// Whether or not this field can only be passed as a keyword argument to `__init__`.
|
||||
pub(crate) kw_only: Option<bool>,
|
||||
}
|
||||
|
||||
/// Representation of a class definition statement in the AST: either a non-generic class, or a
|
||||
|
@ -1922,6 +1925,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
mut default_ty,
|
||||
init_only: _,
|
||||
init,
|
||||
kw_only,
|
||||
},
|
||||
) in self.fields(db, specialization, field_policy)
|
||||
{
|
||||
|
@ -1986,7 +1990,12 @@ impl<'db> ClassLiteral<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
let mut parameter = if kw_only_field_seen || name == "__replace__" {
|
||||
let is_kw_only = name == "__replace__"
|
||||
|| kw_only.unwrap_or(
|
||||
has_dataclass_param(DataclassParams::KW_ONLY) || kw_only_field_seen,
|
||||
);
|
||||
|
||||
let mut parameter = if is_kw_only {
|
||||
Parameter::keyword_only(field_name)
|
||||
} else {
|
||||
Parameter::positional_or_keyword(field_name)
|
||||
|
@ -2005,6 +2014,10 @@ impl<'db> ClassLiteral<'db> {
|
|||
parameters.push(parameter);
|
||||
}
|
||||
|
||||
// In the event that we have a mix of keyword-only and positional parameters, we need to sort them
|
||||
// so that the keyword-only parameters appear after positional parameters.
|
||||
parameters.sort_by_key(Parameter::is_keyword_only);
|
||||
|
||||
let mut signature = Signature::new(Parameters::new(parameters), return_ty);
|
||||
signature.inherited_generic_context = self.generic_context(db);
|
||||
Some(CallableType::function_like(db, signature))
|
||||
|
@ -2316,9 +2329,11 @@ impl<'db> ClassLiteral<'db> {
|
|||
default_ty.map(|ty| ty.apply_optional_specialization(db, specialization));
|
||||
|
||||
let mut init = true;
|
||||
let mut kw_only = None;
|
||||
if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty {
|
||||
default_ty = Some(field.default_type(db));
|
||||
init = field.init(db);
|
||||
kw_only = field.kw_only(db);
|
||||
}
|
||||
|
||||
attributes.insert(
|
||||
|
@ -2328,6 +2343,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
default_ty,
|
||||
init_only: attr.is_init_var(),
|
||||
init,
|
||||
kw_only,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue