[ty] Handle field specifier functions that accept **kwargs and recognize metaclass-based transformers as instances of DataclassInstance (#22018)

## Summary

This contains two bug fixes:

- [Handle field specifier functions that accept
`**kwargs`](ad6918d505)
- [Recognize metaclass-based transformers as instances of
`DataclassInstance`](1a8e29b23c)

closes https://github.com/astral-sh/ty/issues/1987

## Test Plan

* New Markdown tests
* Made sure that the example in 1987 checks without errors
This commit is contained in:
David Peter
2025-12-17 14:22:16 +01:00
committed by GitHub
parent 764ad8b29b
commit 2a61fe2353
3 changed files with 201 additions and 21 deletions

View File

@@ -214,7 +214,7 @@ impl<'db> Bindings<'db> {
}
}
self.evaluate_known_cases(db, dataclass_field_specifiers);
self.evaluate_known_cases(db, argument_types, dataclass_field_specifiers);
// In order of precedence:
//
@@ -337,7 +337,12 @@ impl<'db> Bindings<'db> {
/// Evaluates the return type of certain known callables, where we have special-case logic to
/// determine the return type in a way that isn't directly expressible in the type system.
fn evaluate_known_cases(&mut self, db: &'db dyn Db, dataclass_field_specifiers: &[Type<'db>]) {
fn evaluate_known_cases(
&mut self,
db: &'db dyn Db,
argument_types: &CallArguments<'_, 'db>,
dataclass_field_specifiers: &[Type<'db>],
) {
let to_bool = |ty: &Option<Type<'_>>, default: bool| -> bool {
if let Some(Type::BooleanLiteral(value)) = ty {
*value
@@ -666,25 +671,32 @@ impl<'db> Bindings<'db> {
if dataclass_field_specifiers.contains(&function)
|| function_type.is_known(db, KnownFunction::Field) =>
{
let has_default_value = overload
.parameter_type_by_name("default", false)
.is_ok_and(|ty| ty.is_some())
|| overload
.parameter_type_by_name("default_factory", false)
.is_ok_and(|ty| ty.is_some())
|| overload
.parameter_type_by_name("factory", false)
.is_ok_and(|ty| ty.is_some());
// Helper to get the type of a keyword argument by name. We first try to get it from
// the parameter binding (for explicit parameters), and then fall back to checking the
// call site arguments (for field-specifier functions that use a `**kwargs` parameter,
// instead of specifying `init`, `default` etc. explicitly).
let get_argument_type = |name, fallback_to_default| -> Option<Type<'db>> {
if let Ok(ty) =
overload.parameter_type_by_name(name, fallback_to_default)
{
return ty;
}
argument_types.iter().find_map(|(arg, ty)| {
if matches!(arg, Argument::Keyword(arg_name) if arg_name == name) {
ty
} else {
None
}
})
};
let init = overload
.parameter_type_by_name("init", true)
.unwrap_or(None);
let kw_only = overload
.parameter_type_by_name("kw_only", true)
.unwrap_or(None);
let alias = overload
.parameter_type_by_name("alias", true)
.unwrap_or(None);
let has_default_value = get_argument_type("default", false).is_some()
|| get_argument_type("default_factory", false).is_some()
|| get_argument_type("factory", false).is_some();
let init = get_argument_type("init", true);
let kw_only = get_argument_type("kw_only", true);
let alias = get_argument_type("alias", true);
// `dataclasses.field` and field-specifier functions of commonly used
// libraries like `pydantic`, `attrs`, and `SQLAlchemy` all return

View File

@@ -2277,7 +2277,11 @@ impl<'db> ClassLiteral<'db> {
specialization: Option<Specialization<'db>>,
name: &str,
) -> Member<'db> {
if self.dataclass_params(db).is_some() {
// Check if this class is dataclass-like (either via @dataclass or via dataclass_transform)
if matches!(
CodeGeneratorKind::from_class(db, self, specialization),
Some(CodeGeneratorKind::DataclassLike(_))
) {
if name == "__dataclass_fields__" {
// Make this class look like a subclass of the `DataClassInstance` protocol
return Member {