Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions crates/red_knot_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,75 +731,75 @@ object first, i.e. on the metaclass:
from typing import Literal

class Meta1:
attr: Literal["meta class value"] = "meta class value"
attr: Literal["metaclass value"] = "metaclass value"

class C1(metaclass=Meta1): ...

reveal_type(C1.attr) # revealed: Literal["meta class value"]
reveal_type(C1.attr) # revealed: Literal["metaclass value"]
```

However, the meta class attribute only takes precedence over a class-level attribute if it is a data
However, the metaclass attribute only takes precedence over a class-level attribute if it is a data
descriptor. If it is a non-data descriptor or a normal attribute, the class-level attribute is used
instead (see the [descriptor protocol tests] for data/non-data descriptor attributes):

```py
class Meta2:
attr: str = "meta class value"
attr: str = "metaclass value"

class C2(metaclass=Meta2):
attr: Literal["class value"] = "class value"

reveal_type(C2.attr) # revealed: Literal["class value"]
```

If the class-level attribute is only partially defined, we union the meta class attribute with the
If the class-level attribute is only partially defined, we union the metaclass attribute with the
class-level attribute:

```py
def _(flag: bool):
class Meta3:
attr1 = "meta class value"
attr2: Literal["meta class value"] = "meta class value"
attr1 = "metaclass value"
attr2: Literal["metaclass value"] = "metaclass value"

class C3(metaclass=Meta3):
if flag:
attr1 = "class value"
# TODO: Neither mypy nor pyright show an error here, but we could consider emitting a conflicting-declaration diagnostic here.
attr2: Literal["class value"] = "class value"

reveal_type(C3.attr1) # revealed: Unknown | Literal["meta class value", "class value"]
reveal_type(C3.attr2) # revealed: Literal["meta class value", "class value"]
reveal_type(C3.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
reveal_type(C3.attr2) # revealed: Literal["metaclass value", "class value"]
```

If the *meta class* attribute is only partially defined, we emit a `possibly-unbound-attribute`
If the *metaclass* attribute is only partially defined, we emit a `possibly-unbound-attribute`
diagnostic:

```py
def _(flag: bool):
class Meta4:
if flag:
attr1: str = "meta class value"
attr1: str = "metaclass value"

class C4(metaclass=Meta4): ...
# error: [possibly-unbound-attribute]
reveal_type(C4.attr1) # revealed: str
```

Finally, if both the meta class attribute and the class-level attribute are only partially defined,
Finally, if both the metaclass attribute and the class-level attribute are only partially defined,
we union them and emit a `possibly-unbound-attribute` diagnostic:

```py
def _(flag1: bool, flag2: bool):
class Meta5:
if flag1:
attr1 = "meta class value"
attr1 = "metaclass value"

class C5(metaclass=Meta5):
if flag2:
attr1 = "class value"

# error: [possibly-unbound-attribute]
reveal_type(C5.attr1) # revealed: Unknown | Literal["meta class value", "class value"]
reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
```

## Union of attributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ class Meta(type):
def __getitem__(cls, key: int) -> str:
return str(key)

class DunderOnMetaClass(metaclass=Meta):
class DunderOnMetaclass(metaclass=Meta):
pass

reveal_type(DunderOnMetaClass[0]) # revealed: str
reveal_type(DunderOnMetaclass[0]) # revealed: str
```

If the dunder method is only present on the class itself, it will not be called:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ reveal_type(C.d) # revealed: int

## Dunder methods

Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol:
Dunder methods are looked up on the meta-type, but we still need to invoke the descriptor protocol:

```py
class SomeCallable:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ reveal_type(B.__class__) # revealed: Literal[M]
## Non-class

When a class has an explicit `metaclass` that is not a class, but is a callable that accepts
`type.__new__` arguments, we should return the meta type of its return type.
`type.__new__` arguments, we should return the meta-type of its return type.

```py
def f(*args, **kwargs) -> int: ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ static_assert(is_subtype_of(LiteralStr, type[object]))

static_assert(not is_subtype_of(type[str], LiteralStr))

# custom meta classes
# custom metaclasses

type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass]

Expand Down
34 changes: 18 additions & 16 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ enum AttributeKind {

/// This enum is used to control the behavior of the descriptor protocol implementation.
/// When invoked on a class object, the fallback type (a class attribute) can shadow a
/// non-data descriptor of the meta type (the class's metaclass). However, this is not
/// non-data descriptor of the meta-type (the class's metaclass). However, this is not
/// true for instances. When invoked on an instance, the fallback type (an attribute on
/// the instance) can not completely shadow a non-data descriptor of the meta type (the
/// the instance) can not completely shadow a non-data descriptor of the meta-type (the
/// class), because we do not currently attempt to statically infer if an instance
/// attribute is definitely defined (i.e. to check whether a particular method has been
/// called).
Expand All @@ -148,17 +148,17 @@ enum InstanceFallbackShadowsNonDataDescriptor {
No,
}

/// Dunder methods are looked up on the meta type of a type without potentially falling
/// Dunder methods are looked up on the meta-type of a type without potentially falling
/// back on attributes on the type itself. For example, when implicitly invoked on an
/// instance, dunder methods are not looked up as instance attributes. And when invoked
/// on a class, dunder methods are only looked up on the meta class, not the class itself.
/// on a class, dunder methods are only looked up on the metaclass, not the class itself.
///
/// All other attributes use the `WithInstanceFallback` policy.
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
enum MemberLookupPolicy {
/// Only look up the attribute on the meta type.
/// Only look up the attribute on the meta-type.
NoInstanceFallback,
/// Look up the attribute on the meta type, but fall back to attributes on the instance
/// Look up the attribute on the meta-type, but fall back to attributes on the instance
/// if the meta-type attribute is not found or if the meta-type attribute is not a data
/// descriptor.
WithInstanceFallback,
Expand Down Expand Up @@ -1540,7 +1540,7 @@ impl<'db> Type<'db> {
}
}

/// Look up an attribute in the MRO of the meta type of `self`. This returns class-level attributes
/// Look up an attribute in the MRO of the meta-type of `self`. This returns class-level attributes
/// when called on an instance-like type, and metaclass attributes when called on a class-like type.
///
/// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling
Expand All @@ -1555,7 +1555,9 @@ impl<'db> Type<'db> {
_ => self
.to_meta_type(db)
.find_name_in_mro(db, name.as_str())
.expect("was called on meta type"),
.expect(
"`Type::find_name_in_mro()` should return `Some()` when called on a meta-type",
),
}
}

Expand Down Expand Up @@ -1652,14 +1654,14 @@ impl<'db> Type<'db> {
}
}

/// Look up `__get__` on the meta type of self, and call it with the arguments `self`, `instance`,
/// Look up `__get__` on the meta-type of self, and call it with the arguments `self`, `instance`,
/// and `owner`. `__get__` is different than other dunder methods in that it is not looked up using
/// the descriptor protocol itself.
///
/// In addition to the return type of `__get__`, this method also returns the *kind* of attribute
/// that `self` represents: (1) a data descriptor or (2) a non-data descriptor / normal attribute.
///
/// If `__get__` is not defined on the meta type, this method returns `None`.
/// If `__get__` is not defined on the meta-type, this method returns `None`.
#[salsa::tracked]
fn try_call_dunder_get(
self,
Expand Down Expand Up @@ -1698,7 +1700,7 @@ impl<'db> Type<'db> {
}
}

/// Look up `__get__` on the meta type of `attribute`, and call it with `attribute`, `instance`,
/// Look up `__get__` on the meta-type of `attribute`, and call it with `attribute`, `instance`,
/// and `owner` as arguments. This method exists as a separate step as we need to handle unions
/// and intersections explicitly.
fn try_call_dunder_get_on_attribute(
Expand Down Expand Up @@ -1783,7 +1785,7 @@ impl<'db> Type<'db> {
///
/// This method roughly performs the following steps:
///
/// - Look up the attribute `name` on the meta type of `self`. Call the result `meta_attr`.
/// - Look up the attribute `name` on the meta-type of `self`. Call the result `meta_attr`.
/// - Call `__get__` on the meta-type of `meta_attr`, if it exists. If the call succeeds,
/// replace `meta_attr` with the result of the call. Also check if `meta_attr` is a *data*
/// descriptor by testing if `__set__` or `__delete__` exist.
Expand Down Expand Up @@ -1832,7 +1834,7 @@ impl<'db> Type<'db> {
}

// `meta_attr` is the return type of a data descriptor, but the attribute on the
// meta type is possibly-unbound. This means that we "fall through" to the next
// meta-type is possibly-unbound. This means that we "fall through" to the next
// stage of the descriptor protocol and union with the fallback type.
(
Symbol::Type(meta_attr_ty, Boundness::PossiblyUnbound),
Expand Down Expand Up @@ -1873,7 +1875,7 @@ impl<'db> Type<'db> {
)
.with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)),

// If the attribute is not found on the meta type, we simply return the fallback.
// If the attribute is not found on the meta-type, we simply return the fallback.
(Symbol::Unbound, _, fallback) => fallback.with_qualifiers(fallback_qualifiers),
}
}
Expand Down Expand Up @@ -2736,7 +2738,7 @@ impl<'db> Type<'db> {
}
}

/// Look up a dunder method on the meta type of `self` and call it.
/// Look up a dunder method on the meta-type of `self` and call it.
///
/// Returns an `Err` if the dunder method can't be called,
/// or the given arguments are not valid.
Expand Down Expand Up @@ -3163,7 +3165,7 @@ impl<'db> Type<'db> {
KnownClass::WrapperDescriptorType.to_class_literal(db)
}
Type::Callable(CallableType::General(_)) => {
// TODO: Get the meta type
// TODO: Get the meta-type
todo_type!(".to_meta_type() for general callable type")
}
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Expand Down
Loading