Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ The [`typing.dataclass_transform`] specification also allows classes (such as `d
to be listed in `field_specifiers`, but it is currently unclear how this should work, and other type
checkers do not seem to support this either.

### Basic example
### For function-based transformers

```py
from typing_extensions import dataclass_transform, Any
Expand All @@ -478,18 +478,154 @@ class Person:
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)

# TODO: Should be `(self: Person, name: str, *, age: int | None) -> None`
reveal_type(Person.__init__) # revealed: (self: Person, id: int = Any, name: str = Any, age: int | None = Any) -> None
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None

# TODO: No error here
# error: [invalid-argument-type]
alice = Person("Alice", age=30)

reveal_type(alice.id) # revealed: int
reveal_type(alice.name) # revealed: str
reveal_type(alice.age) # revealed: int | None
```

### For metaclass-based transformers

```py
from typing_extensions import dataclass_transform, Any

def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
class FancyMeta(type):
def __new__(cls, name, bases, namespace):
...
return super().__new__(cls, name, bases, namespace)

class FancyBase(metaclass=FancyMeta): ...

class Person(FancyBase):
id: int = fancy_field(init=False)
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)

reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None

alice = Person("Alice", age=30)

reveal_type(alice.id) # revealed: int
reveal_type(alice.name) # revealed: str
reveal_type(alice.age) # revealed: int | None
```

### For base-class-based transformers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[insert "all your base are belong to us" joke]


```py
from typing_extensions import dataclass_transform, Any

def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
class FancyBase:
def __init_subclass__(cls):
...
super().__init_subclass__()

class Person(FancyBase):
id: int = fancy_field(init=False)
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)

# TODO: should be (self: Person, name: str = Unknown, *, age: int | None = Unknown) -> None
reveal_type(Person.__init__) # revealed: def __init__(self) -> None

# TODO: shouldn't be an error
# error: [too-many-positional-arguments]
# error: [unknown-argument]
alice = Person("Alice", age=30)

reveal_type(alice.id) # revealed: int
reveal_type(alice.name) # revealed: str
reveal_type(alice.age) # revealed: int | None
```

### With default arguments

Field specifiers can have default arguments that should be respected:

```py
from typing_extensions import dataclass_transform, Any

def fancy_field(*, init: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
def fancy_model[T](cls: type[T]) -> type[T]:
...
return cls

@fancy_model
class Person:
id: int = fancy_field()
name: str = fancy_field(init=True)

reveal_type(Person.__init__) # revealed: (self: Person, name: str) -> None

Person(name="Alice")
```

### With overloaded field specifiers

```py
from typing_extensions import dataclass_transform, overload, Any

@overload
def fancy_field(*, init: bool = True) -> Any: ...
@overload
def fancy_field(*, kw_only: bool = False) -> Any: ...
def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
def fancy_model[T](cls: type[T]) -> type[T]:
...
return cls

@fancy_model
class Person:
id: int = fancy_field(init=False)
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)

reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
```

### Nested dataclass-transformers

Make sure that models are only affected by the field specifiers of their own transformer:

```py
from typing_extensions import dataclass_transform, Any
from dataclasses import field

def outer_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(outer_field,))
def outer_model[T](cls: type[T]) -> type[T]:
# ...
return cls

def inner_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(inner_field,))
def inner_model[T](cls: type[T]) -> type[T]:
# ...
return cls

@outer_model
class Outer:
@inner_model
class Inner:
inner_a: int = inner_field(init=False)
inner_b: str = outer_field(init=False)

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
```

## Overloaded dataclass-like decorators

In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the
Expand Down
113 changes: 72 additions & 41 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ pub(crate) use self::signatures::{CallableSignature, Parameter, Parameters, Sign
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
use crate::module_name::ModuleName;
use crate::module_resolver::{KnownModule, resolve_module};
use crate::place::{Definedness, Place, PlaceAndQualifiers, TypeOrigin, imported_symbol};
use crate::place::{
Definedness, Place, PlaceAndQualifiers, TypeOrigin, imported_symbol, known_module_symbol,
};
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::place::ScopedPlaceId;
use crate::semantic_index::scope::ScopeId;
Expand All @@ -50,7 +52,8 @@ pub use crate::types::display::DisplaySettings;
use crate::types::display::TupleSpecialization;
use crate::types::enums::{enum_metadata, is_single_member_enum};
use crate::types::function::{
DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction,
DataclassTransformerFlags, DataclassTransformerParams, FunctionSpans, FunctionType,
KnownFunction,
};
use crate::types::generics::{
GenericContext, InferableTypeVars, PartialSpecialization, Specialization, bind_typevar,
Expand Down Expand Up @@ -618,67 +621,95 @@ impl<'db> PropertyInstanceType<'db> {
}

bitflags! {
/// Used for the return type of `dataclass(…)` calls. Keeps track of the arguments
/// that were passed in. For the precise meaning of the fields, see [1].
/// Used to store metadata about a dataclass or dataclass-like class.
/// For the precise meaning of the fields, see [1].
///
/// [1]: https://docs.python.org/3/library/dataclasses.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DataclassParams: u16 {
const INIT = 0b0000_0000_0001;
const REPR = 0b0000_0000_0010;
const EQ = 0b0000_0000_0100;
const ORDER = 0b0000_0000_1000;
const UNSAFE_HASH = 0b0000_0001_0000;
const FROZEN = 0b0000_0010_0000;
const MATCH_ARGS = 0b0000_0100_0000;
const KW_ONLY = 0b0000_1000_0000;
const SLOTS = 0b0001_0000_0000;
const WEAKREF_SLOT = 0b0010_0000_0000;
// This is not an actual argument from `dataclass(...)` but a flag signaling that no
// `field_specifiers` was specified for the `dataclass_transform`, see [1].
// [1]: https://typing.python.org/en/latest/spec/dataclasses.html#dataclass-transform-parameters
const NO_FIELD_SPECIFIERS = 0b0100_0000_0000;
}
}

impl get_size2::GetSize for DataclassParams {}

impl Default for DataclassParams {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DataclassFlags: u16 {
const INIT = 1 << 0;
const REPR = 1 << 1;
const EQ = 1 << 2;
const ORDER = 1 << 3;
const UNSAFE_HASH = 1 << 4;
const FROZEN = 1 << 5;
const MATCH_ARGS = 1 << 6;
const KW_ONLY = 1 << 7;
const SLOTS = 1 << 8 ;
const WEAKREF_SLOT = 1 << 9;
}
}

impl get_size2::GetSize for DataclassFlags {}

impl Default for DataclassFlags {
fn default() -> Self {
Self::INIT | Self::REPR | Self::EQ | Self::MATCH_ARGS
}
}

impl From<DataclassTransformerParams> for DataclassParams {
fn from(params: DataclassTransformerParams) -> Self {
impl From<DataclassTransformerFlags> for DataclassFlags {
fn from(params: DataclassTransformerFlags) -> Self {
let mut result = Self::default();

result.set(
Self::EQ,
params.contains(DataclassTransformerParams::EQ_DEFAULT),
params.contains(DataclassTransformerFlags::EQ_DEFAULT),
);
result.set(
Self::ORDER,
params.contains(DataclassTransformerParams::ORDER_DEFAULT),
params.contains(DataclassTransformerFlags::ORDER_DEFAULT),
);
result.set(
Self::KW_ONLY,
params.contains(DataclassTransformerParams::KW_ONLY_DEFAULT),
params.contains(DataclassTransformerFlags::KW_ONLY_DEFAULT),
);
result.set(
Self::FROZEN,
params.contains(DataclassTransformerParams::FROZEN_DEFAULT),
);

result.set(
Self::NO_FIELD_SPECIFIERS,
!params.contains(DataclassTransformerParams::FIELD_SPECIFIERS),
params.contains(DataclassTransformerFlags::FROZEN_DEFAULT),
);

result
}
}

/// Metadata for a dataclass. Stored inside a `Type::DataclassDecorator(…)`
/// instance that we use as the return type of a `dataclasses.dataclass` and
/// dataclass-transformer decorator calls.
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct DataclassParams<'db> {
flags: DataclassFlags,

#[returns(deref)]
field_specifiers: Box<[Type<'db>]>,
}

impl get_size2::GetSize for DataclassParams<'_> {}

impl<'db> DataclassParams<'db> {
fn default_params(db: &'db dyn Db) -> Self {
Self::from_flags(db, DataclassFlags::default())
}

fn from_flags(db: &'db dyn Db, flags: DataclassFlags) -> Self {
let dataclasses_field = known_module_symbol(db, KnownModule::Dataclasses, "field")
.place
.ignore_possibly_undefined()
.unwrap_or_else(Type::unknown);

Self::new(db, flags, vec![dataclasses_field].into_boxed_slice())
}
Comment on lines +695 to +702
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sort-of morbidly curious about what does happen to our inference if you try to define a dataclass, with dataclasses.field for one of the fields, and you're using a custom typeshed that doesn't have dataclasses.field in it. That might be an interesting test, but it's almost certainly not worth spending time on it right now 😄


fn from_transformer_params(db: &'db dyn Db, params: DataclassTransformerParams<'db>) -> Self {
Self::new(
db,
DataclassFlags::from(params.flags(db)),
params.field_specifiers(db),
)
}
}

/// Representation of a type: a set of possible values at runtime.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
Expand Down Expand Up @@ -719,9 +750,9 @@ pub enum Type<'db> {
/// A special callable that is returned by a `dataclass(…)` call. It is usually
/// used as a decorator. Note that this is only used as a return type for actual
/// `dataclass` calls, not for the argumentless `@dataclass` decorator.
DataclassDecorator(DataclassParams),
DataclassDecorator(DataclassParams<'db>),
/// A special callable that is returned by a `dataclass_transform(…)` call.
DataclassTransformer(DataclassTransformerParams),
DataclassTransformer(DataclassTransformerParams<'db>),
/// The type of an arbitrary callable object with a certain specified signature.
Callable(CallableType<'db>),
/// A specific module object
Expand Down Expand Up @@ -5448,7 +5479,7 @@ impl<'db> Type<'db> {
) -> Result<Bindings<'db>, CallError<'db>> {
self.bindings(db)
.match_parameters(db, argument_types)
.check_types(db, argument_types, &TypeContext::default())
.check_types(db, argument_types, &TypeContext::default(), &[])
}

/// Look up a dunder method on the meta-type of `self` and call it.
Expand Down Expand Up @@ -5500,7 +5531,7 @@ impl<'db> Type<'db> {
let bindings = dunder_callable
.bindings(db)
.match_parameters(db, argument_types)
.check_types(db, argument_types, &tcx)?;
.check_types(db, argument_types, &tcx, &[])?;
if boundness == Definedness::PossiblyUndefined {
return Err(CallDunderError::PossiblyUnbound(Box::new(bindings)));
}
Expand Down
Loading
Loading