mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Fall back to object
for attribute access on synthesized protocols (#20286)
This commit is contained in:
parent
982a0a2a7c
commit
deb3d3d150
4 changed files with 39 additions and 2 deletions
|
@ -392,8 +392,14 @@ from inspect import getattr_static
|
||||||
|
|
||||||
def f_okay(c: Callable[[], None]):
|
def f_okay(c: Callable[[], None]):
|
||||||
if hasattr(c, "__qualname__"):
|
if hasattr(c, "__qualname__"):
|
||||||
c.__qualname__ # okay
|
reveal_type(c.__qualname__) # revealed: object
|
||||||
|
|
||||||
|
# TODO: should be `property`
|
||||||
|
# (or complain that we don't know that `type(c)` has the attribute at all!)
|
||||||
|
reveal_type(type(c).__qualname__) # revealed: @Todo(Intersection meta-type)
|
||||||
|
|
||||||
# `hasattr` only guarantees that an attribute is readable.
|
# `hasattr` only guarantees that an attribute is readable.
|
||||||
|
#
|
||||||
# error: [invalid-assignment] "Object of type `Literal["my_callable"]` is not assignable to attribute `__qualname__` on type `(() -> None) & <Protocol with members '__qualname__'>`"
|
# error: [invalid-assignment] "Object of type `Literal["my_callable"]` is not assignable to attribute `__qualname__` on type `(() -> None) & <Protocol with members '__qualname__'>`"
|
||||||
c.__qualname__ = "my_callable"
|
c.__qualname__ = "my_callable"
|
||||||
|
|
||||||
|
|
|
@ -84,3 +84,17 @@ def _(obj: MaybeWithSpam):
|
||||||
# error: [possibly-unbound-attribute]
|
# error: [possibly-unbound-attribute]
|
||||||
reveal_type(obj.spam) # revealed: int
|
reveal_type(obj.spam) # revealed: int
|
||||||
```
|
```
|
||||||
|
|
||||||
|
All attribute available on `object` are still available on these synthesized protocols, but
|
||||||
|
attributes that are not present on `object` are not available:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f(x: object):
|
||||||
|
if hasattr(x, "__qualname__"):
|
||||||
|
reveal_type(x.__repr__) # revealed: bound method object.__repr__() -> str
|
||||||
|
reveal_type(x.__str__) # revealed: bound method object.__str__() -> str
|
||||||
|
reveal_type(x.__dict__) # revealed: dict[str, Any]
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `<Protocol with members '__qualname__'>` has no attribute `foo`"
|
||||||
|
reveal_type(x.foo) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
|
@ -3409,6 +3409,23 @@ impl<'db> Type<'db> {
|
||||||
|
|
||||||
Type::ModuleLiteral(module) => module.static_member(db, name_str),
|
Type::ModuleLiteral(module) => module.static_member(db, name_str),
|
||||||
|
|
||||||
|
// If a protocol does not include a member and the policy disables falling back to
|
||||||
|
// `object`, we return `Place::Unbound` here. This short-circuits attribute lookup
|
||||||
|
// before we find the "fallback to attribute access on `object`" logic later on
|
||||||
|
// (otherwise we would infer that all synthesized protocols have `__getattribute__`
|
||||||
|
// methods, and therefore that all synthesized protocols have all possible attributes.)
|
||||||
|
//
|
||||||
|
// Note that we could do this for *all* protocols, but it's only *necessary* for synthesized
|
||||||
|
// ones, and the standard logic is *probably* more performant for class-based protocols?
|
||||||
|
Type::ProtocolInstance(ProtocolInstanceType {
|
||||||
|
inner: Protocol::Synthesized(protocol),
|
||||||
|
..
|
||||||
|
}) if policy.mro_no_object_fallback()
|
||||||
|
&& !protocol.interface().includes_member(db, name_str) =>
|
||||||
|
{
|
||||||
|
Place::Unbound.into()
|
||||||
|
}
|
||||||
|
|
||||||
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
|
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
|
||||||
db,
|
db,
|
||||||
name_str,
|
name_str,
|
||||||
|
|
|
@ -227,7 +227,7 @@ impl<'db> ProtocolInterface<'db> {
|
||||||
place: Place::bound(member.ty()),
|
place: Place::bound(member.ty()),
|
||||||
qualifiers: member.qualifiers(),
|
qualifiers: member.qualifiers(),
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| Type::object(db).instance_member(db, name))
|
.unwrap_or_else(|| Type::object(db).member(db, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if if all members on `self` are also members of `other`.
|
/// Return `true` if if all members on `self` are also members of `other`.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue