[ty] recognize non-fully-static specializations

This commit is contained in:
Carl Meyer 2025-05-12 16:34:14 -07:00
parent 41fa082414
commit f68dbfdef1
No known key found for this signature in database
GPG Key ID: 2D1FB7916A52E121
6 changed files with 41 additions and 19 deletions

View File

@ -130,3 +130,13 @@ static_assert(is_fully_static(TypeOf[static]))
static_assert(not is_fully_static(CallableTypeOf[gradual])) static_assert(not is_fully_static(CallableTypeOf[gradual]))
static_assert(is_fully_static(CallableTypeOf[static])) static_assert(is_fully_static(CallableTypeOf[static]))
``` ```
## Generics
```py
from typing import Any
from ty_extensions import static_assert, is_fully_static
static_assert(is_fully_static(list[int]))
static_assert(not is_fully_static(list[Any]))
```

View File

@ -112,7 +112,7 @@ from typing_extensions import _NoDefaultType
static_assert(is_subtype_of(sys.version_info.__class__, AlwaysTruthy)) static_assert(is_subtype_of(sys.version_info.__class__, AlwaysTruthy))
static_assert(is_subtype_of(types.EllipsisType, AlwaysTruthy)) static_assert(is_subtype_of(types.EllipsisType, AlwaysTruthy))
static_assert(is_subtype_of(_NoDefaultType, AlwaysTruthy)) static_assert(is_subtype_of(_NoDefaultType, AlwaysTruthy))
static_assert(is_subtype_of(slice, AlwaysTruthy)) static_assert(is_subtype_of(slice[int, int, int], AlwaysTruthy))
static_assert(is_subtype_of(types.FunctionType, AlwaysTruthy)) static_assert(is_subtype_of(types.FunctionType, AlwaysTruthy))
static_assert(is_subtype_of(types.MethodType, AlwaysTruthy)) static_assert(is_subtype_of(types.MethodType, AlwaysTruthy))
static_assert(is_subtype_of(typing.TypeVar, AlwaysTruthy)) static_assert(is_subtype_of(typing.TypeVar, AlwaysTruthy))

View File

@ -2144,6 +2144,7 @@ impl<'db> Type<'db> {
| Type::PropertyInstance(_) => true, | Type::PropertyInstance(_) => true,
Type::ProtocolInstance(protocol) => protocol.is_fully_static(db), Type::ProtocolInstance(protocol) => protocol.is_fully_static(db),
Type::NominalInstance(instance) => instance.is_fully_static(db),
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => true, None => true,
@ -2159,24 +2160,8 @@ impl<'db> Type<'db> {
!matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_)) !matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_))
&& !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_)) && !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_))
} }
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::NominalInstance(_) => { Type::ClassLiteral(class) => class.is_fully_static(db),
// TODO: Ideally, we would iterate over the MRO of the class, check if all Type::GenericAlias(alias) => alias.is_fully_static(db),
// bases are fully static, and only return `true` if that is the case.
//
// This does not work yet, because we currently infer `Unknown` for some
// generic base classes that we don't understand yet. For example, `str`
// is defined as `class str(Sequence[str])` in typeshed and we currently
// compute its MRO as `(str, Unknown, object)`. This would make us think
// that `str` is a gradual type, which causes all sorts of downstream
// issues because it does not participate in equivalence/subtyping etc.
//
// Another problem is that we run into problems if we eagerly query the
// MRO of class literals here. I have not fully investigated this, but
// iterating over the MRO alone, without even acting on it, causes us to
// infer `Unknown` for many classes.
true
}
Type::Union(union) => union.is_fully_static(db), Type::Union(union) => union.is_fully_static(db),
Type::Intersection(intersection) => intersection.is_fully_static(db), Type::Intersection(intersection) => intersection.is_fully_static(db),
// TODO: Once we support them, make sure that we return `false` for other types // TODO: Once we support them, make sure that we return `false` for other types

View File

@ -167,6 +167,10 @@ impl<'db> GenericAlias<'db> {
self.origin(db).definition(db) self.origin(db).definition(db)
} }
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.origin(db).is_fully_static(db) && self.specialization(db).is_fully_static(db)
}
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self { fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
Self::new( Self::new(
db, db,
@ -246,6 +250,13 @@ impl<'db> ClassType<'db> {
self.known(db) == Some(known_class) self.known(db) == Some(known_class)
} }
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
match self {
Self::NonGeneric(class_literal) => class_literal.is_fully_static(db),
Self::Generic(alias) => alias.is_fully_static(db),
}
}
/// Return `true` if this class represents the builtin class `object` /// Return `true` if this class represents the builtin class `object`
pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { pub(crate) fn is_object(self, db: &'db dyn Db) -> bool {
self.is_known(db, KnownClass::Object) self.is_known(db, KnownClass::Object)
@ -506,6 +517,14 @@ impl<'db> ClassLiteral<'db> {
self.known(db) == Some(known_class) self.known(db) == Some(known_class)
} }
#[expect(clippy::unused_self)]
pub(crate) fn is_fully_static(self, _db: &'db dyn Db) -> bool {
// TODO: Ideally, we would iterate over the MRO of the class, check if all
// bases are fully static, and only return `true` if that is the case. But there may be
// cycle issues trying to infer base classes this eagerly.
true
}
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> { pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
// Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code
// the knowledge that this class is not generic. // the knowledge that this class is not generic.

View File

@ -292,6 +292,10 @@ pub struct Specialization<'db> {
} }
impl<'db> Specialization<'db> { impl<'db> Specialization<'db> {
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.types(db).iter().all(|ty| ty.is_fully_static(db))
}
pub(crate) fn type_mapping(self) -> TypeMapping<'db, 'db> { pub(crate) fn type_mapping(self) -> TypeMapping<'db, 'db> {
TypeMapping::Specialization(self) TypeMapping::Specialization(self)
} }

View File

@ -66,6 +66,10 @@ impl<'db> NominalInstanceType<'db> {
self.class self.class
} }
pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.class.is_fully_static(db)
}
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
// N.B. The subclass relation is fully static // N.B. The subclass relation is fully static
self.class.is_subclass_of(db, other.class) self.class.is_subclass_of(db, other.class)