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
[ty] Fix false positives for class F(Generic[*Ts]): ...
  • Loading branch information
AlexWaygood committed Dec 1, 2025
commit 64122bd44b40799df189e4fa13bcd269d00388c0
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
At its simplest, to define a generic class using the legacy syntax, you inherit from the
`typing.Generic` special form, which is "specialized" with the generic class's type variables.

```toml
[environment]
python-version = "3.11"
```

```py
from ty_extensions import generic_context
from typing_extensions import Generic, TypeVar, TypeVarTuple, ParamSpec, Unpack
Expand All @@ -19,7 +24,9 @@ class MultipleTypevars(Generic[T, S]): ...
class SingleParamSpec(Generic[P]): ...
class TypeVarAndParamSpec(Generic[P, T]): ...
class SingleTypeVarTuple(Generic[Unpack[Ts]]): ...
class StarredSingleTypeVarTuple(Generic[*Ts]): ...
class TypeVarAndTypeVarTuple(Generic[T, Unpack[Ts]]): ...
class StarredTypeVarAndTypeVarTuple(Generic[T, *Ts]): ...

# revealed: ty_extensions.GenericContext[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar))
Expand All @@ -34,6 +41,8 @@ reveal_type(generic_context(TypeVarAndParamSpec))
# TODO: support `TypeVarTuple` properly (these should not reveal `None`)
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: None
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: None
reveal_type(generic_context(StarredSingleTypeVarTuple)) # revealed: None
reveal_type(generic_context(StarredTypeVarAndTypeVarTuple)) # revealed: None
```

Inheriting from `Generic` multiple times yields a `duplicate-base` diagnostic, just like any other
Expand Down
22 changes: 12 additions & 10 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -957,8 +957,13 @@ impl<'db> Type<'db> {
self.is_instance_of(db, KnownClass::NotImplementedType)
}

pub(crate) const fn is_todo(&self) -> bool {
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
pub(crate) fn is_todo(&self) -> bool {
self.as_dynamic().is_some_and(|dynamic| match dynamic {
DynamicType::Any | DynamicType::Unknown | DynamicType::Divergent(_) => false,
DynamicType::Todo(_) | DynamicType::TodoStarredExpression | DynamicType::TodoUnpack => {
true
}
})
}

pub const fn is_generic_alias(&self) -> bool {
Expand Down Expand Up @@ -8167,7 +8172,7 @@ impl<'db> Type<'db> {
Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db),

// These types have no definition
Self::Dynamic(DynamicType::Divergent(_) | DynamicType::Todo(_) | DynamicType::TodoUnpack)
Self::Dynamic(DynamicType::Divergent(_) | DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression)
| Self::Callable(_)
| Self::TypeIs(_) => None,
}
Expand Down Expand Up @@ -8829,6 +8834,8 @@ pub enum DynamicType {
Todo(TodoType),
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoUnpack,
/// A special Todo-variant for `*Ts`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoStarredExpression,
/// A type that is determined to be divergent during recursive type inference.
Divergent(DivergentType),
}
Expand Down Expand Up @@ -8859,13 +8866,8 @@ impl std::fmt::Display for DynamicType {
// `DynamicType::Todo`'s display should be explicit that is not a valid display of
// any other type
DynamicType::Todo(todo) => write!(f, "@Todo{todo}"),
DynamicType::TodoUnpack => {
if cfg!(debug_assertions) {
f.write_str("@Todo(typing.Unpack)")
} else {
f.write_str("@Todo")
}
Comment on lines -8863 to -8867
Copy link
Contributor

Choose a reason for hiding this comment

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

Not doing this anymore leads to an inconsistent display of various todo types in release mode, but I guess it doesn't hurt to have the one with the message in release builds as well (for these very special todo types)

Copy link
Member Author

Choose a reason for hiding this comment

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

yes. One of the criticisms of Todo types in the past has been that we don't tell the user what exactly is still to be done if ty has been built in release mode. It seems like a feature that we can show that information for at least some TODOs 😄

}
DynamicType::TodoUnpack => f.write_str("@Todo(typing.Unpack)"),
DynamicType::TodoStarredExpression => f.write_str("@Todo(StarredExpression)"),
DynamicType::Divergent(_) => f.write_str("Divergent"),
}
}
Expand Down
4 changes: 3 additions & 1 deletion crates/ty_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ impl<'db> ClassBase<'db> {
ClassBase::Class(class) => class.name(db),
ClassBase::Dynamic(DynamicType::Any) => "Any",
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack) => "@Todo",
ClassBase::Dynamic(
DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression,
) => "@Todo",
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
ClassBase::Protocol => "Protocol",
ClassBase::Generic => "Generic",
Expand Down
28 changes: 22 additions & 6 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8397,7 +8397,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
});

// TODO
todo_type!("starred expression")
Type::Dynamic(DynamicType::TodoStarredExpression)
}

fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> {
Expand Down Expand Up @@ -9561,10 +9561,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(unknown @ Type::Dynamic(DynamicType::Unknown), _, _)
| (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown),

(todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack), _, _)
| (_, todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack), _) => {
Some(todo)
}
(
todo @ Type::Dynamic(
DynamicType::Todo(_)
| DynamicType::TodoUnpack
| DynamicType::TodoStarredExpression,
),
_,
_,
)
| (
_,
todo @ Type::Dynamic(
DynamicType::Todo(_)
| DynamicType::TodoUnpack
| DynamicType::TodoStarredExpression,
),
_,
) => Some(todo),

(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),

Expand Down Expand Up @@ -11869,7 +11883,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.db(),
*typevar,
&|ty| match ty {
Type::Dynamic(DynamicType::TodoUnpack) => true,
Type::Dynamic(
DynamicType::TodoUnpack | DynamicType::TodoStarredExpression,
) => true,
Type::NominalInstance(nominal) => matches!(
nominal.known_class(self.db()),
Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
if starred_type.exact_tuple_instance_spec(self.db()).is_some() {
starred_type
} else {
todo_type!("PEP 646")
Type::Dynamic(DynamicType::TodoStarredExpression)
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/ty_python_semantic/src/types/type_ordering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
(DynamicType::TodoUnpack, _) => Ordering::Less,
(_, DynamicType::TodoUnpack) => Ordering::Greater,

(DynamicType::TodoStarredExpression, _) => Ordering::Less,
(_, DynamicType::TodoStarredExpression) => Ordering::Greater,

(DynamicType::Divergent(left), DynamicType::Divergent(right)) => left.cmp(&right),
(DynamicType::Divergent(_), _) => Ordering::Less,
(_, DynamicType::Divergent(_)) => Ordering::Greater,
Expand Down
Loading