From 3f63c08728314bf591055f236271c1c235add0d0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 1 Apr 2025 09:38:23 -0700 Subject: [PATCH] [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. --- crates/red_knot_python_semantic/src/types.rs | 1 + .../src/types/class.rs | 21 +++++++++++++---- .../src/types/class_base.rs | 6 ++++- .../src/types/display.rs | 23 ++++++++++--------- .../src/types/infer.rs | 17 +++++++++++++- .../src/types/narrow.rs | 10 +++++++- .../vendor/typeshed/stdlib/typing.pyi | 2 +- 7 files changed, 60 insertions(+), 20 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index db8d74b6a4..923884d268 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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, [ diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 33e14acd24..c12316ccb4 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -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, diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 2d4161dc33..e7606468ae 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -61,7 +61,11 @@ impl<'db> ClassBase<'db> { pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { 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__`? diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 3d4fb84c8c..0b31eb69e3 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -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), } } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index e6c6e09ff0..c2b323ea89 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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) } diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index ceaec3d3c1..9410253a79 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -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) } diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi index 5875b69157..cf69c11bca 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi @@ -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: ...