[red-knot] support Any as a class in typeshed (#17107)

## Summary

In https://github.com/python/typeshed/pull/13520 the typeshed definition
of `typing.Any` was changed from `Any = object()` to `class Any: ...`.
Our automated typeshed updater pulled down this change in
https://github.com/astral-sh/ruff/pull/17106, with the consequence that
we no longer understand `Any`, which is... not good.

This PR gives us the ability to understand `Any` defined as a class
instead of `object()`. It doesn't remove our ability to understand the
old form. Perhaps at some point we'll want to remove it, but for now we
may as well support both old and new typeshed?

This also directly patches typeshed to use the new form of `Any`; this
is purely to work around our tests that no known class is inferred as
`Unknown`, which otherwise fail with the old typeshed and the changes in
this PR. (All other tests pass.) This patch to typeshed will shortly be
subsumed by https://github.com/astral-sh/ruff/pull/17106 anyway.

## Test Plan

Without the typeshed change in this PR, all tests pass except for the
two `known_class_doesnt_fallback_to_unknown_unexpectedly_*` tests (so we
still support the old form of defining `Any`). With the typeshed change
in this PR, all tests pass, so we now support the new form in a way that
is indistinguishable to our test suite from the old form. And
indistinguishable to the ecosystem check: after rebasing
https://github.com/astral-sh/ruff/pull/17106 on this PR, there's zero
ecosystem impact.
This commit is contained in:
Carl Meyer 2025-04-01 09:38:23 -07:00 committed by GitHub
parent 5a876ed25e
commit 3f63c08728
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 60 additions and 20 deletions

View File

@ -2950,6 +2950,7 @@ impl<'db> Type<'db> {
// https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
Type::ClassLiteral(ClassLiteralType { class }) => {
let ty = match class.known(db) {
Some(KnownClass::Any) => Type::any(),
Some(KnownClass::Complex) => UnionType::from_elements(
db,
[

View File

@ -840,6 +840,7 @@ pub enum KnownClass {
// Typeshed
NoneType, // Part of `types` for Python >= 3.10
// Typing
Any,
StdlibAlias,
SpecialForm,
TypeVar,
@ -903,7 +904,8 @@ impl<'db> KnownClass {
Self::NoneType => Truthiness::AlwaysFalse,
Self::BaseException
Self::Any
| Self::BaseException
| Self::Object
| Self::OrderedDict
| Self::BaseExceptionGroup
@ -944,6 +946,7 @@ impl<'db> KnownClass {
pub(crate) fn name(self, db: &'db dyn Db) -> &'static str {
match self {
Self::Any => "Any",
Self::Bool => "bool",
Self::Object => "object",
Self::Bytes => "bytes",
@ -1150,7 +1153,8 @@ impl<'db> KnownClass {
| Self::MethodWrapperType
| Self::WrapperDescriptorType => KnownModule::Types,
Self::NoneType => KnownModule::Typeshed,
Self::SpecialForm
Self::Any
| Self::SpecialForm
| Self::TypeVar
| Self::StdlibAlias
| Self::SupportsIndex
@ -1201,7 +1205,8 @@ impl<'db> KnownClass {
| Self::TypeAliasType
| Self::NotImplementedType => true,
Self::Bool
Self::Any
| Self::Bool
| Self::Object
| Self::Bytes
| Self::Type
@ -1258,7 +1263,8 @@ impl<'db> KnownClass {
| Self::TypeAliasType
| Self::NotImplementedType => true,
Self::Bool
Self::Any
| Self::Bool
| Self::Object
| Self::Bytes
| Self::Tuple
@ -1311,6 +1317,7 @@ impl<'db> KnownClass {
// We assert that this match is exhaustive over the right-hand side in the unit test
// `known_class_roundtrip_from_str()`
let candidate = match class_name {
"Any" => Self::Any,
"bool" => Self::Bool,
"object" => Self::Object,
"bytes" => Self::Bytes,
@ -1377,7 +1384,8 @@ impl<'db> KnownClass {
/// Return `true` if the module of `self` matches `module`
fn check_module(self, db: &'db dyn Db, module: KnownModule) -> bool {
match self {
Self::Bool
Self::Any
| Self::Bool
| Self::Object
| Self::Bytes
| Self::Type
@ -1503,6 +1511,9 @@ pub enum KnownInstanceType<'db> {
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
Never,
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
/// This is not used since typeshed switched to representing `Any` as a class; now we use
/// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at
/// least for now. TODO maybe remove?
Any,
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
Tuple,

View File

@ -61,7 +61,11 @@ impl<'db> ClassBase<'db> {
pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
match ty {
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
Type::ClassLiteral(literal) => Some(Self::Class(literal.class())),
Type::ClassLiteral(literal) => Some(if literal.class().is_known(db, KnownClass::Any) {
Self::Dynamic(DynamicType::Any)
} else {
Self::Class(literal.class())
}),
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?

View File

@ -33,18 +33,19 @@ pub struct DisplayType<'db> {
impl Display for DisplayType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let representation = self.ty.representation(self.db);
if matches!(
self.ty,
match self.ty {
Type::ClassLiteral(literal) if literal.class().is_known(self.db, KnownClass::Any) => {
write!(f, "typing.Any")
}
Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_)
) {
write!(f, "Literal[{representation}]")
} else {
representation.fmt(f)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_) => {
write!(f, "Literal[{representation}]")
}
_ => representation.fmt(f),
}
}
}

View File

@ -6522,7 +6522,14 @@ impl<'db> TypeInferenceBuilder<'db> {
let name_ty = self.infer_expression(slice);
match name_ty {
Type::ClassLiteral(class_literal_ty) => {
SubclassOfType::from(self.db(), class_literal_ty.class())
if class_literal_ty
.class()
.is_known(self.db(), KnownClass::Any)
{
SubclassOfType::subclass_of_any()
} else {
SubclassOfType::from(self.db(), class_literal_ty.class())
}
}
Type::KnownInstance(KnownInstanceType::Any) => {
SubclassOfType::subclass_of_any()
@ -6602,6 +6609,14 @@ impl<'db> TypeInferenceBuilder<'db> {
} = subscript;
match value_ty {
Type::ClassLiteral(literal) if literal.class().is_known(self.db(), KnownClass::Any) => {
self.context.report_lint(
&INVALID_TYPE_FORM,
subscript,
format_args!("Type `typing.Any` expected no type parameter",),
);
Type::unknown()
}
Type::KnownInstance(known_instance) => {
self.infer_parameterized_known_instance_type_expression(subscript, known_instance)
}

View File

@ -113,7 +113,15 @@ impl KnownConstraintFunction {
}
Some(builder.build())
}
Type::ClassLiteral(class_literal) => Some(constraint_fn(class_literal.class())),
Type::ClassLiteral(class_literal) => {
// At runtime (on Python 3.11+), this will return `True` for classes that actually
// do inherit `typing.Any` and `False` otherwise. We could accurately model that?
if class_literal.class().is_known(db, KnownClass::Any) {
None
} else {
Some(constraint_fn(class_literal.class()))
}
}
Type::SubclassOf(subclass_of_ty) => {
subclass_of_ty.subclass_of().into_class().map(constraint_fn)
}

View File

@ -130,7 +130,7 @@ if sys.version_info >= (3, 12):
if sys.version_info >= (3, 13):
__all__ += ["get_protocol_members", "is_protocol", "NoDefault", "TypeIs", "ReadOnly"]
Any = object()
class Any: ...
class _Final: ...