diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 5988b6a930..4e642b0218 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1303,6 +1303,139 @@ quux.b "); } + #[test] + fn metaclass1() { + let test = cursor_test( + "\ +class Meta(type): + @property + def meta_attr(self) -> int: + return 0 + +class C(metaclass=Meta): ... + +C. +", + ); + + assert_snapshot!(test.completions_without_builtins_with_types(), @r" + meta_attr :: int + mro :: bound method .mro() -> list[type] + __annotations__ :: dict[str, Any] + __base__ :: type | None + __bases__ :: tuple[type, ...] + __basicsize__ :: int + __call__ :: bound method .__call__(...) -> Any + __class__ :: + __delattr__ :: def __delattr__(self, name: str, /) -> None + __dict__ :: MappingProxyType[str, Any] + __dictoffset__ :: int + __dir__ :: def __dir__(self) -> Iterable[str] + __doc__ :: str | None + __eq__ :: def __eq__(self, value: object, /) -> bool + __flags__ :: int + __format__ :: def __format__(self, format_spec: str, /) -> str + __getattribute__ :: def __getattribute__(self, name: str, /) -> Any + __getstate__ :: def __getstate__(self) -> object + __hash__ :: def __hash__(self) -> int + __init__ :: def __init__(self) -> None + __init_subclass__ :: def __init_subclass__(cls) -> None + __instancecheck__ :: bound method .__instancecheck__(instance: Any, /) -> bool + __itemsize__ :: int + __module__ :: str + __mro__ :: tuple[, ] + __name__ :: str + __ne__ :: def __ne__(self, value: object, /) -> bool + __new__ :: def __new__(cls) -> Self + __or__ :: bound method .__or__(value: Any, /) -> UnionType + __prepare__ :: bound method .__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] + __qualname__ :: str + __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] + __reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...] + __repr__ :: def __repr__(self) -> str + __ror__ :: bound method .__ror__(value: Any, /) -> UnionType + __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None + __sizeof__ :: def __sizeof__(self) -> int + __str__ :: def __str__(self) -> str + __subclasscheck__ :: bound method .__subclasscheck__(subclass: type, /) -> bool + __subclasses__ :: bound method .__subclasses__() -> list[Self] + __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool + __text_signature__ :: str | None + __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] + __weakrefoffset__ :: int + "); + } + + #[test] + fn metaclass2() { + let test = cursor_test( + "\ +class Meta(type): + @property + def meta_attr(self) -> int: + return 0 + +class C(metaclass=Meta): ... + +Meta. +", + ); + + insta::with_settings!({ + // The formatting of some types are different depending on + // whether we're in release mode or not. These differences + // aren't really relevant for completion tests AFAIK, so + // just redact them. ---AG + filters => [(r"(?m)\s*__(annotations|new)__.+$", "")]}, + { + assert_snapshot!(test.completions_without_builtins_with_types(), @r" + meta_attr :: property + mro :: def mro(self) -> list[type] + __base__ :: type | None + __bases__ :: tuple[type, ...] + __basicsize__ :: int + __call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any + __class__ :: + __delattr__ :: def __delattr__(self, name: str, /) -> None + __dict__ :: MappingProxyType[str, Any] + __dictoffset__ :: int + __dir__ :: def __dir__(self) -> Iterable[str] + __doc__ :: str | None + __eq__ :: def __eq__(self, value: object, /) -> bool + __flags__ :: int + __format__ :: def __format__(self, format_spec: str, /) -> str + __getattribute__ :: def __getattribute__(self, name: str, /) -> Any + __getstate__ :: def __getstate__(self) -> object + __hash__ :: def __hash__(self) -> int + __init__ :: Overload[(self, o: object, /) -> None, (self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None] + __init_subclass__ :: def __init_subclass__(cls) -> None + __instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool + __itemsize__ :: int + __module__ :: str + __mro__ :: tuple[, , ] + __name__ :: str + __ne__ :: def __ne__(self, value: object, /) -> bool + __or__ :: def __or__(self, value: Any, /) -> UnionType + __prepare__ :: bound method .__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] + __qualname__ :: str + __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] + __reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...] + __repr__ :: def __repr__(self) -> str + __ror__ :: def __ror__(self, value: Any, /) -> UnionType + __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None + __sizeof__ :: def __sizeof__(self) -> int + __str__ :: def __str__(self) -> str + __subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool + __subclasses__ :: def __subclasses__(self: Self) -> list[Self] + __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool + __text_signature__ :: str | None + __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] + __weakrefoffset__ :: int + "); + } + ); + } + #[test] fn class_init3() { let test = cursor_test( @@ -1358,7 +1491,7 @@ Quux. ); assert_snapshot!(test.completions_without_builtins_with_types(), @r" - mro :: def mro(self) -> list[type] + mro :: bound method .mro() -> list[type] some_attribute :: int some_class_method :: bound method .some_class_method() -> int some_method :: def some_method(self) -> int @@ -1368,7 +1501,7 @@ Quux. __base__ :: type | None __bases__ :: tuple[type, ...] __basicsize__ :: int - __call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any + __call__ :: bound method .__call__(...) -> Any __class__ :: __delattr__ :: def __delattr__(self, name: str, /) -> None __dict__ :: MappingProxyType[str, Any] @@ -1383,26 +1516,26 @@ Quux. __hash__ :: def __hash__(self) -> int __init__ :: def __init__(self) -> Unknown __init_subclass__ :: def __init_subclass__(cls) -> None - __instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool + __instancecheck__ :: bound method .__instancecheck__(instance: Any, /) -> bool __itemsize__ :: int __module__ :: str - __mro__ :: tuple[, ] + __mro__ :: tuple[, ] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool __new__ :: def __new__(cls) -> Self - __or__ :: def __or__(self, value: Any, /) -> UnionType + __or__ :: bound method .__or__(value: Any, /) -> UnionType __prepare__ :: bound method .__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] __qualname__ :: str __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] __reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...] __repr__ :: def __repr__(self) -> str - __ror__ :: def __ror__(self, value: Any, /) -> UnionType + __ror__ :: bound method .__ror__(value: Any, /) -> UnionType __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None __sizeof__ :: def __sizeof__(self) -> int __str__ :: def __str__(self) -> str - __subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool - __subclasses__ :: def __subclasses__(self: Self) -> list[Self] - __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool + __subclasscheck__ :: bound method .__subclasscheck__(subclass: type, /) -> bool + __subclasses__ :: bound method .__subclasses__() -> list[Self] + __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool __text_signature__ :: str | None __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] __weakrefoffset__ :: int diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index baf8b35625..de075fefe9 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -95,21 +95,21 @@ impl<'db> AllMembers<'db> { } Type::ClassLiteral(class_literal) => { - self.extend_with_class_members(db, class_literal); + self.extend_with_class_members(db, ty, class_literal); if let Type::ClassLiteral(meta_class_literal) = ty.to_meta_type(db) { - self.extend_with_class_members(db, meta_class_literal); + self.extend_with_class_members(db, ty, meta_class_literal); } } Type::GenericAlias(generic_alias) => { let class_literal = generic_alias.origin(db); - self.extend_with_class_members(db, class_literal); + self.extend_with_class_members(db, ty, class_literal); } Type::SubclassOf(subclass_of_type) => { if let Some(class_literal) = subclass_of_type.subclass_of().into_class() { - self.extend_with_class_members(db, class_literal.class_literal(db).0); + self.extend_with_class_members(db, ty, class_literal.class_literal(db).0); } } @@ -136,11 +136,11 @@ impl<'db> AllMembers<'db> { | Type::BoundSuper(_) | Type::TypeIs(_) => match ty.to_meta_type(db) { Type::ClassLiteral(class_literal) => { - self.extend_with_class_members(db, class_literal); + self.extend_with_class_members(db, ty, class_literal); } Type::GenericAlias(generic_alias) => { let class_literal = generic_alias.origin(db); - self.extend_with_class_members(db, class_literal); + self.extend_with_class_members(db, ty, class_literal); } _ => {} }, @@ -214,16 +214,41 @@ impl<'db> AllMembers<'db> { } } - fn extend_with_class_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) { + /// Add members from `class_literal` (including following its + /// parent classes). + /// + /// `ty` should be the original type that we're adding members for. + /// For example, in: + /// + /// ```text + /// class Meta(type): + /// @property + /// def meta_attr(self) -> int: + /// return 0 + /// + /// class C(metaclass=Meta): ... + /// + /// C. + /// ``` + /// + /// then `class_literal` might be `Meta`, but `ty` should be the + /// type of `C`. This ensures that the descriptor protocol is + /// correctly used (or not used) to get the type of each member of + /// `C`. + fn extend_with_class_members( + &mut self, + db: &'db dyn Db, + ty: Type<'db>, + class_literal: ClassLiteral<'db>, + ) { for parent in class_literal .iter_mro(db, None) .filter_map(ClassBase::into_class) .map(|class| class.class_literal(db).0) { - let parent_ty = Type::ClassLiteral(parent); let parent_scope = parent.body_scope(db); for Member { name, .. } in all_declarations_and_bindings(db, parent_scope) { - let result = parent_ty.member(db, name.as_str()); + let result = ty.member(db, name.as_str()); let Some(ty) = result.place.ignore_possibly_unbound() else { continue; };