Compare commits
2 Commits
dcreager/s
...
charlie/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27d60685d0 | ||
|
|
72b52121c0 |
@@ -406,9 +406,6 @@ reveal_type(Child.create()) # revealed: Child
|
||||
|
||||
## Attributes
|
||||
|
||||
TODO: The use of `Self` to annotate the `next_node` attribute should be
|
||||
[modeled as a property][self attribute], using `Self` in its parameter and return type.
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
@@ -418,13 +415,36 @@ class LinkedList:
|
||||
|
||||
def next(self: Self) -> Self:
|
||||
reveal_type(self.value) # revealed: int
|
||||
# TODO: no error
|
||||
# error: [invalid-return-type]
|
||||
return self.next_node
|
||||
|
||||
reveal_type(LinkedList().next()) # revealed: LinkedList
|
||||
```
|
||||
|
||||
Dataclass fields can also use `Self` in their annotations:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Self
|
||||
|
||||
@dataclass
|
||||
class Node:
|
||||
parent: Optional[Self] = None
|
||||
|
||||
Node(Node())
|
||||
```
|
||||
|
||||
Attributes annotated with `Self` can be assigned on instances:
|
||||
|
||||
```py
|
||||
from typing import Optional, Self
|
||||
|
||||
class MyClass:
|
||||
field: Optional[Self] = None
|
||||
|
||||
def _(c: MyClass):
|
||||
c.field = c
|
||||
```
|
||||
|
||||
Attributes can also refer to a generic parameter:
|
||||
|
||||
```py
|
||||
@@ -675,4 +695,73 @@ def _(c: CallableTypeOf[C().method]):
|
||||
reveal_type(c) # revealed: (...) -> None
|
||||
```
|
||||
|
||||
[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations
|
||||
## Bound methods from internal data structures stored as instance attributes
|
||||
|
||||
This tests the pattern where a class stores bound methods from internal data structures (like
|
||||
`deque` or `dict`) as instance attributes for performance. When these bound methods are later
|
||||
accessed and called through `self`, the `Self` type binding should not interfere with their
|
||||
signatures.
|
||||
|
||||
This is a regression test for false positives found in ecosystem projects like jinja's `LRUCache`
|
||||
and beartype's `CacheUnboundedStrong`.
|
||||
|
||||
```py
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
class LRUCache:
|
||||
"""A simple LRU cache that stores bound methods from an internal deque."""
|
||||
|
||||
def __init__(self, capacity: int) -> None:
|
||||
self.capacity = capacity
|
||||
self._mapping: dict[Any, Any] = {}
|
||||
self._queue: deque[Any] = deque()
|
||||
self._postinit()
|
||||
|
||||
def _postinit(self) -> None:
|
||||
# Store bound methods from the internal deque for faster attribute lookup
|
||||
self._popleft = self._queue.popleft
|
||||
self._pop = self._queue.pop
|
||||
self._remove = self._queue.remove
|
||||
self._append = self._queue.append
|
||||
|
||||
def __getitem__(self, key: Any) -> Any:
|
||||
# These should not produce errors - the bound methods have signatures
|
||||
# from deque, not involving Self
|
||||
self._remove(key)
|
||||
self._append(key)
|
||||
return self._mapping[key]
|
||||
|
||||
def __setitem__(self, key: Any, value: Any) -> None:
|
||||
self._remove(key)
|
||||
if len(self._queue) >= self.capacity:
|
||||
self._popleft()
|
||||
self._append(key)
|
||||
self._mapping[key] = value
|
||||
|
||||
def __delitem__(self, key: Any) -> None:
|
||||
self._remove(key)
|
||||
del self._mapping[key]
|
||||
```
|
||||
|
||||
Similarly for dict-based patterns:
|
||||
|
||||
```py
|
||||
from typing import Hashable
|
||||
|
||||
class CacheMap:
|
||||
"""A cache that stores bound methods from an internal dict."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._key_to_value: dict[Hashable, object] = {}
|
||||
self._key_to_value_get = self._key_to_value.get
|
||||
self._key_to_value_set = self._key_to_value.__setitem__
|
||||
|
||||
def cache_or_get_cached_value(self, key: Hashable, value: object) -> object:
|
||||
# This should not produce errors - we're using dict's get/setitem methods
|
||||
cached_value = self._key_to_value_get(key)
|
||||
if cached_value is not None:
|
||||
return cached_value
|
||||
self._key_to_value_set(key, value)
|
||||
return value
|
||||
```
|
||||
|
||||
@@ -1814,6 +1814,27 @@ def _(ns: argparse.Namespace):
|
||||
reveal_type(ns.whatever) # revealed: Any
|
||||
```
|
||||
|
||||
### `__getattr__` with `Self` type
|
||||
|
||||
`__getattr__` should also work when the receiver is typed as `Self`:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class CustomGetAttr:
|
||||
def __getattr__(self, name: str) -> int:
|
||||
return 1
|
||||
|
||||
def method(self) -> Self:
|
||||
reveal_type(self.whatever) # revealed: int
|
||||
return self
|
||||
```
|
||||
|
||||
## Classes with custom `__getattribute__` methods
|
||||
|
||||
If a type provides a custom `__getattribute__`, we use its return type as the type for unknown
|
||||
|
||||
@@ -21,6 +21,5 @@ X.aaaaooooooo # error: [unresolved-attribute]
|
||||
Foo.X.startswith # error: [unresolved-attribute]
|
||||
Foo.Bar().y.startswith # error: [unresolved-attribute]
|
||||
|
||||
# TODO: false positive (just testing the diagnostic in the meantime)
|
||||
Foo().b.a # error: [unresolved-attribute]
|
||||
Foo().b.a
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/special_form
|
||||
16 | Foo.X.startswith # error: [unresolved-attribute]
|
||||
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
|
||||
18 |
|
||||
19 | # TODO: false positive (just testing the diagnostic in the meantime)
|
||||
20 | Foo().b.a # error: [unresolved-attribute]
|
||||
19 | Foo().b.a
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
@@ -95,21 +94,9 @@ error[unresolved-attribute]: Special form `typing.LiteralString` has no attribut
|
||||
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
18 |
|
||||
19 | # TODO: false positive (just testing the diagnostic in the meantime)
|
||||
19 | Foo().b.a
|
||||
|
|
||||
help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString`
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Special form `typing.Self` has no attribute `a`
|
||||
--> src/mdtest_snippet.py:20:1
|
||||
|
|
||||
19 | # TODO: false positive (just testing the diagnostic in the meantime)
|
||||
20 | Foo().b.a # error: [unresolved-attribute]
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -2530,15 +2530,34 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
Type::TypeVar(bound_typevar) => {
|
||||
match bound_typevar.typevar(db).bound_or_constraints(db) {
|
||||
let member = match bound_typevar.typevar(db).bound_or_constraints(db) {
|
||||
None => Type::object().instance_member(db, name),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
bound.instance_member(db, name)
|
||||
if bound_typevar.typevar(db).is_self(db) {
|
||||
if let Type::NominalInstance(instance) = bound {
|
||||
instance.class(db).instance_member(db, name)
|
||||
} else {
|
||||
bound.instance_member(db, name)
|
||||
}
|
||||
} else {
|
||||
bound.instance_member(db, name)
|
||||
}
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.map_with_boundness_and_qualifiers(db, |constraint| {
|
||||
constraint.instance_member(db, name)
|
||||
}),
|
||||
};
|
||||
if bound_typevar.typevar(db).is_self(db) {
|
||||
let self_mapping = TypeMapping::BindSelf {
|
||||
self_type: Type::TypeVar(*bound_typevar),
|
||||
self_typevar_identity: Some(bound_typevar.typevar(db).identity(db)),
|
||||
};
|
||||
member.map_type(|ty| {
|
||||
ty.apply_type_mapping(db, &self_mapping, TypeContext::default())
|
||||
})
|
||||
} else {
|
||||
member
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3277,11 +3296,20 @@ impl<'db> Type<'db> {
|
||||
| Type::TypedDict(_) => {
|
||||
let fallback = self.instance_member(db, name_str);
|
||||
|
||||
// `Self` type variables use `InstanceFallbackShadowsNonDataDescriptor::Yes`
|
||||
// because instance attributes should shadow non-data descriptors on the class.
|
||||
let instance_fallback_shadows = if matches!(self, Type::TypeVar(tv) if tv.typevar(db).is_self(db))
|
||||
{
|
||||
InstanceFallbackShadowsNonDataDescriptor::Yes
|
||||
} else {
|
||||
InstanceFallbackShadowsNonDataDescriptor::No
|
||||
};
|
||||
|
||||
let result = self.invoke_descriptor_protocol(
|
||||
db,
|
||||
name_str,
|
||||
fallback,
|
||||
InstanceFallbackShadowsNonDataDescriptor::No,
|
||||
instance_fallback_shadows,
|
||||
policy,
|
||||
);
|
||||
|
||||
@@ -6867,7 +6895,8 @@ pub enum TypeMapping<'a, 'db> {
|
||||
/// Binds any `typing.Self` typevar with a particular `self` class.
|
||||
BindSelf {
|
||||
self_type: Type<'db>,
|
||||
binding_context: Option<BindingContext<'db>>,
|
||||
/// If `Some`, only bind `Self` typevars that have this identity (i.e., from the same class).
|
||||
self_typevar_identity: Option<TypeVarIdentity<'db>>,
|
||||
},
|
||||
/// Replaces occurrences of `typing.Self` with a new `Self` type variable with the given upper bound.
|
||||
ReplaceSelf { new_upper_bound: Type<'db> },
|
||||
@@ -6897,8 +6926,9 @@ impl<'db> TypeMapping<'_, 'db> {
|
||||
| TypeMapping::ReplaceParameterDefaults
|
||||
| TypeMapping::EagerExpansion => context,
|
||||
TypeMapping::BindSelf {
|
||||
binding_context, ..
|
||||
} => context.remove_self(db, *binding_context),
|
||||
self_typevar_identity,
|
||||
..
|
||||
} => context.remove_self(db, *self_typevar_identity),
|
||||
TypeMapping::ReplaceSelf { new_upper_bound } => GenericContext::from_typevar_instances(
|
||||
db,
|
||||
context.variables(db).map(|typevar| {
|
||||
@@ -8533,10 +8563,11 @@ impl<'db> BoundTypeVarInstance<'db> {
|
||||
}
|
||||
TypeMapping::BindSelf {
|
||||
self_type,
|
||||
binding_context,
|
||||
self_typevar_identity,
|
||||
} => {
|
||||
if self.typevar(db).is_self(db)
|
||||
&& binding_context.is_none_or(|context| self.binding_context(db) == context)
|
||||
&& self_typevar_identity
|
||||
.is_none_or(|identity| self.typevar(db).identity(db) == identity)
|
||||
{
|
||||
*self_type
|
||||
} else {
|
||||
|
||||
@@ -23,8 +23,8 @@ use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
|
||||
use crate::types::variance::VarianceInferable;
|
||||
use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard};
|
||||
use crate::types::{
|
||||
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance,
|
||||
ClassLiteral, FindLegacyTypeVarsVisitor, IntersectionType, KnownClass, KnownInstanceType,
|
||||
ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, ClassLiteral,
|
||||
FindLegacyTypeVarsVisitor, IntersectionType, KnownClass, KnownInstanceType,
|
||||
MaterializationKind, NormalizedVisitor, Type, TypeContext, TypeMapping,
|
||||
TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
|
||||
UnionType, declaration_type, walk_type_var_bounds,
|
||||
@@ -66,12 +66,26 @@ pub(crate) fn bind_typevar<'db>(
|
||||
) -> Option<BoundTypeVarInstance<'db>> {
|
||||
// typing.Self is treated like a legacy typevar, but doesn't follow the same scoping rules. It is always bound to the outermost method in the containing class.
|
||||
if matches!(typevar.kind(db), TypeVarKind::TypingSelf) {
|
||||
for ((_, inner), (_, outer)) in index.ancestor_scopes(containing_scope).tuple_windows() {
|
||||
if outer.kind().is_class() {
|
||||
if let NodeWithScopeKind::Function(function) = inner.node() {
|
||||
let definition = index.expect_single_definition(function);
|
||||
let binding_function =
|
||||
typevar_binding_context.filter(|definition| definition.kind(db).is_function_def());
|
||||
let mut function_in_class = None;
|
||||
for (_, scope) in index.ancestor_scopes(containing_scope) {
|
||||
match scope.node() {
|
||||
NodeWithScopeKind::Function(function) => {
|
||||
function_in_class = Some(function);
|
||||
}
|
||||
NodeWithScopeKind::Class(class) => {
|
||||
if let Some(function) = function_in_class {
|
||||
let definition = index.expect_single_definition(function);
|
||||
return Some(typevar.with_binding_context(db, definition));
|
||||
}
|
||||
if let Some(binding_context) = binding_function {
|
||||
return Some(typevar.with_binding_context(db, binding_context));
|
||||
}
|
||||
let definition = index.expect_single_definition(class);
|
||||
return Some(typevar.with_binding_context(db, definition));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,15 +295,14 @@ impl<'db> GenericContext<'db> {
|
||||
pub(crate) fn remove_self(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
binding_context: Option<BindingContext<'db>>,
|
||||
self_typevar_identity: Option<TypeVarIdentity<'db>>,
|
||||
) -> Self {
|
||||
Self::from_typevar_instances(
|
||||
db,
|
||||
self.variables(db).filter(|bound_typevar| {
|
||||
!(bound_typevar.typevar(db).is_self(db)
|
||||
&& binding_context.is_none_or(|binding_context| {
|
||||
bound_typevar.binding_context(db) == binding_context
|
||||
}))
|
||||
&& self_typevar_identity
|
||||
.is_none_or(|identity| bound_typevar.typevar(db).identity(db) == identity))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -112,10 +112,10 @@ use crate::types::{
|
||||
LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
|
||||
ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType,
|
||||
SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
|
||||
TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
|
||||
TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types,
|
||||
todo_type,
|
||||
TypeContext, TypeMapping, TypeQualifiers, TypeVarBoundOrConstraints,
|
||||
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
|
||||
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
|
||||
UnionTypeInstance, binding_type, infer_scope_types, todo_type,
|
||||
};
|
||||
use crate::types::{CallableTypes, overrides};
|
||||
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
|
||||
@@ -4530,6 +4530,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let db = self.db();
|
||||
|
||||
let mut first_tcx = None;
|
||||
let self_mapping = TypeMapping::BindSelf {
|
||||
self_type: object_ty,
|
||||
self_typevar_identity: None,
|
||||
};
|
||||
let bind_self =
|
||||
|ty: Type<'db>| ty.apply_type_mapping(db, &self_mapping, TypeContext::default());
|
||||
|
||||
// A wrapper over `infer_value_ty` that allows inferring the value type multiple times
|
||||
// during attribute resolution.
|
||||
@@ -4877,6 +4883,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}),
|
||||
qualifiers,
|
||||
} => {
|
||||
let meta_attr_ty = bind_self(meta_attr_ty);
|
||||
if invalid_assignment_to_final(self, qualifiers) {
|
||||
return false;
|
||||
}
|
||||
@@ -4928,6 +4935,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
} =
|
||||
object_ty.instance_member(db, attribute)
|
||||
{
|
||||
let instance_attr_ty = bind_self(instance_attr_ty);
|
||||
let value_ty =
|
||||
infer_value_ty(self, TypeContext::new(Some(instance_attr_ty)));
|
||||
if invalid_assignment_to_final(self, qualifiers) {
|
||||
@@ -4973,6 +4981,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
qualifiers,
|
||||
} = object_ty.instance_member(db, attribute)
|
||||
{
|
||||
let instance_attr_ty = bind_self(instance_attr_ty);
|
||||
let value_ty =
|
||||
infer_value_ty(self, TypeContext::new(Some(instance_attr_ty)));
|
||||
if invalid_assignment_to_final(self, qualifiers) {
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::types::relation::{
|
||||
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation,
|
||||
};
|
||||
use crate::types::{
|
||||
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypeKind,
|
||||
ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, CallableTypeKind,
|
||||
FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, NormalizedVisitor,
|
||||
ParamSpecAttrKind, TypeContext, TypeMapping, VarianceInferable, todo_type,
|
||||
};
|
||||
@@ -893,13 +893,21 @@ impl<'db> Signature<'db> {
|
||||
parameters.next();
|
||||
}
|
||||
|
||||
// Find the Self typevar from this signature's generic context, if any.
|
||||
// We only want to bind Self typevars that belong to this signature, not
|
||||
// Self typevars from other classes that might appear in type parameters.
|
||||
let self_typevar_identity = self.generic_context.and_then(|ctx| {
|
||||
ctx.variables(db)
|
||||
.find(|tv| tv.typevar(db).is_self(db))
|
||||
.map(|tv| tv.typevar(db).identity(db))
|
||||
});
|
||||
|
||||
let mut parameters = Parameters::new(db, parameters);
|
||||
let mut return_ty = self.return_ty;
|
||||
let binding_context = self.definition.map(BindingContext::Definition);
|
||||
if let Some(self_type) = self_type {
|
||||
let self_mapping = TypeMapping::BindSelf {
|
||||
self_type,
|
||||
binding_context,
|
||||
self_typevar_identity,
|
||||
};
|
||||
parameters = parameters.apply_type_mapping_impl(
|
||||
db,
|
||||
@@ -912,7 +920,7 @@ impl<'db> Signature<'db> {
|
||||
Self {
|
||||
generic_context: self
|
||||
.generic_context
|
||||
.map(|generic_context| generic_context.remove_self(db, binding_context)),
|
||||
.map(|generic_context| generic_context.remove_self(db, self_typevar_identity)),
|
||||
definition: self.definition,
|
||||
parameters,
|
||||
return_ty,
|
||||
@@ -920,9 +928,15 @@ impl<'db> Signature<'db> {
|
||||
}
|
||||
|
||||
pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self {
|
||||
// Find the Self typevar from this signature's generic context, if any.
|
||||
let self_typevar_identity = self.generic_context.and_then(|ctx| {
|
||||
ctx.variables(db)
|
||||
.find(|tv| tv.typevar(db).is_self(db))
|
||||
.map(|tv| tv.typevar(db).identity(db))
|
||||
});
|
||||
let self_mapping = TypeMapping::BindSelf {
|
||||
self_type,
|
||||
binding_context: self.definition.map(BindingContext::Definition),
|
||||
self_typevar_identity,
|
||||
};
|
||||
let parameters = self.parameters.apply_type_mapping_impl(
|
||||
db,
|
||||
|
||||
Reference in New Issue
Block a user