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
14 changes: 7 additions & 7 deletions crates/ty_ide/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1247,7 +1247,7 @@ quux.<CURSOR>
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: bound method Quux.__new__() -> Self@object
__new__ :: bound method Quux.__new__() -> Self@__new__
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method Quux.__repr__() -> str
Expand Down Expand Up @@ -1292,7 +1292,7 @@ quux.b<CURSOR>
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: bound method Quux.__new__() -> Self@object
__new__ :: bound method Quux.__new__() -> Self@__new__
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method Quux.__repr__() -> str
Expand Down Expand Up @@ -1346,7 +1346,7 @@ C.<CURSOR>
__mro__ :: tuple[<class 'C'>, <class 'object'>]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self@object
__new__ :: def __new__(cls) -> Self@__new__
__or__ :: bound method <class 'C'>.__or__(value: Any, /) -> UnionType
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
Expand Down Expand Up @@ -1522,7 +1522,7 @@ Quux.<CURSOR>
__mro__ :: tuple[<class 'Quux'>, <class 'object'>]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self@object
__new__ :: def __new__(cls) -> Self@__new__
__or__ :: bound method <class 'Quux'>.__or__(value: Any, /) -> UnionType
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
Expand Down Expand Up @@ -1574,8 +1574,8 @@ Answer.<CURSOR>
__bool__ :: bound method <class 'Answer'>.__bool__() -> Literal[True]
__class__ :: <class 'EnumMeta'>
__contains__ :: bound method <class 'Answer'>.__contains__(value: object) -> bool
__copy__ :: def __copy__(self) -> Self@Enum
__deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@Enum
__copy__ :: def __copy__(self) -> Self@__copy__
__deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@__deepcopy__
__delattr__ :: def __delattr__(self, name: str, /) -> None
__dict__ :: MappingProxyType[str, Any]
__dictoffset__ :: int
Expand All @@ -1599,7 +1599,7 @@ Answer.<CURSOR>
__mro__ :: tuple[<class 'Answer'>, <class 'Enum'>, <class 'object'>]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls, value: object) -> Self@Enum
__new__ :: def __new__(cls, value: object) -> Self@__new__
__or__ :: bound method <class 'Answer'>.__or__(value: Any, /) -> UnionType
__order__ :: str
__prepare__ :: bound method <class 'EnumMeta'>.__prepare__(cls: str, bases: tuple[type, ...], **kwds: Any) -> _EnumDict
Expand Down
24 changes: 19 additions & 5 deletions crates/ty_python_semantic/resources/mdtest/annotations/self.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@ from typing import Self

class Shape:
def set_scale(self: Self, scale: float) -> Self:
reveal_type(self) # revealed: Self@Shape
reveal_type(self) # revealed: Self@set_scale
return self

def nested_type(self: Self) -> list[Self]:
return [self]

def nested_func(self: Self) -> Self:
def inner() -> Self:
reveal_type(self) # revealed: Self@Shape
reveal_type(self) # revealed: Self@nested_func
return self
return inner()

def nested_func_without_enclosing_binding(self):
def inner(x: Self):
# TODO: revealed: Self@nested_func_without_enclosing_binding
# (The outer method binds an implicit `Self`)
reveal_type(x) # revealed: Self@inner
inner(self)

def implicit_self(self) -> Self:
# TODO: first argument in a method should be considered as "typing.Self"
reveal_type(self) # revealed: Unknown
Expand All @@ -38,13 +45,13 @@ reveal_type(Shape().nested_func()) # revealed: Shape

class Circle(Shape):
def set_scale(self: Self, scale: float) -> Self:
reveal_type(self) # revealed: Self@Circle
reveal_type(self) # revealed: Self@set_scale
return self

class Outer:
class Inner:
def foo(self: Self) -> Self:
reveal_type(self) # revealed: Self@Inner
reveal_type(self) # revealed: Self@foo
return self
```

Expand Down Expand Up @@ -99,6 +106,9 @@ reveal_type(Shape.bar()) # revealed: Unknown
python-version = "3.11"
```

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.
Comment on lines +109 to +110
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have good support for properties, so I would suggest opening up a help wanted issue to tackle this as a follow-on

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds fine (feel free to create the issue), but will that be enough to solve this case? Wouldn't we consider these to be two separate "bindings" of Self (the one on the return type of the next_node property and the one on the return type of next method), and not allow the one to be assignable to the other, even if they share the same upper bound?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing the attribute would count as a call to the property getter, so my thinking was that we would infer a specialization of {Self@next_node = Self@next} which would make them assignable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense!


```py
from typing import Self

Expand All @@ -108,6 +118,8 @@ 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
Expand Down Expand Up @@ -151,7 +163,7 @@ from typing import Self

class Shape:
def union(self: Self, other: Self | None):
reveal_type(other) # revealed: Self@Shape | None
reveal_type(other) # revealed: Self@union | None
return self
```

Expand Down Expand Up @@ -205,3 +217,5 @@ class MyMetaclass(type):
def __new__(cls) -> Self:
return super().__new__(cls)
```

[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.

class Foo:
def method(self, x: Self):
reveal_type(x) # revealed: Self@Foo
reveal_type(x) # revealed: Self@method
```

## Type expressions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,33 @@ def g(x: T) -> T | None:
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
```

## Opaque decorators don't affect typevar binding

Inside the body of a generic function, we should be able to see that the typevars bound by that
function are in fact bound by that function. This requires being able to see the enclosing
function's _undecorated_ type and signature, especially in the case where a gradually typed
decorator "hides" the function type from outside callers.

```py
from typing import cast, Any, Callable, TypeVar

F = TypeVar("F", bound=Callable[..., Any])
T = TypeVar("T")

def opaque_decorator(f: Any) -> Any:
return f

def transparent_decorator(f: F) -> F:
return f

@opaque_decorator
def decorated(t: T) -> None:
# error: [redundant-cast]
reveal_type(cast(T, t)) # revealed: T@decorated

@transparent_decorator
def decorated(t: T) -> None:
# error: [redundant-cast]
reveal_type(cast(T, t)) # revealed: T@decorated
```
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,30 @@ def f(
# error: [invalid-argument-type] "does not satisfy upper bound"
reveal_type(close_and_return(g)) # revealed: Unknown
```

## Opaque decorators don't affect typevar binding

Inside the body of a generic function, we should be able to see that the typevars bound by that
function are in fact bound by that function. This requires being able to see the enclosing
function's _undecorated_ type and signature, especially in the case where a gradually typed
decorator "hides" the function type from outside callers.

```py
from typing import cast, Any, Callable

def opaque_decorator(f: Any) -> Any:
return f

def transparent_decorator[F: Callable[..., Any]](f: F) -> F:
return f

@opaque_decorator
def decorated[T](t: T) -> None:
# error: [redundant-cast]
reveal_type(cast(T, t)) # revealed: T@decorated

@transparent_decorator
def decorated[T](t: T) -> None:
# error: [redundant-cast]
reveal_type(cast(T, t)) # revealed: T@decorated
```
4 changes: 2 additions & 2 deletions crates/ty_python_semantic/resources/mdtest/named_tuple.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ class Person(NamedTuple):

reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._fields) # revealed: tuple[str, ...]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self@NamedTupleFallback
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self@_make
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@NamedTupleFallback
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace

# TODO: should be `Person` once we support `Self`
reveal_type(Person._make(("Alice", 42))) # revealed: Unknown
Expand Down
27 changes: 19 additions & 8 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ use crate::types::function::{
DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction,
};
use crate::types::generics::{
GenericContext, PartialSpecialization, Specialization, walk_generic_context,
walk_partial_specialization, walk_specialization,
GenericContext, PartialSpecialization, Specialization, bind_legacy_typevar,
walk_generic_context, walk_partial_specialization, walk_specialization,
};
pub use crate::types::ide_support::{
CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name,
Expand Down Expand Up @@ -5268,12 +5268,13 @@ impl<'db> Type<'db> {
/// expression, it names the type `Type::NominalInstance(builtins.int)`, that is, all objects whose
/// `__class__` is `int`.
///
/// The `scope_id` argument must always be a scope from the file we are currently inferring, so
/// The `scope_id` and `legacy_typevar_binding_context` arguments must always come from the file we are currently inferring, so
/// as to avoid cross-module AST dependency.
pub(crate) fn in_type_expression(
&self,
db: &'db dyn Db,
scope_id: ScopeId<'db>,
legacy_typevar_binding_context: Option<Definition<'db>>,
) -> Result<Type<'db>, InvalidTypeExpressionError<'db>> {
match self {
// Special cases for `float` and `complex`
Expand Down Expand Up @@ -5402,16 +5403,26 @@ impl<'db> Type<'db> {
"nearest_enclosing_class must return type that can be instantiated",
);
let class_definition = class.definition(db);
Ok(Type::TypeVar(TypeVarInstance::new(
let typevar = TypeVarInstance::new(
db,
ast::name::Name::new_static("Self"),
Some(class_definition),
Some(class_definition),
None,
Some(TypeVarBoundOrConstraints::UpperBound(instance)),
TypeVarVariance::Invariant,
None,
TypeVarKind::Implicit,
)))
);
let typevar = bind_legacy_typevar(
db,
&module,
index,
scope_id.file_scope_id(db),
legacy_typevar_binding_context,
typevar,
)
.unwrap_or(typevar);
Ok(Type::TypeVar(typevar))
}
SpecialFormType::TypeAlias => Ok(Type::Dynamic(DynamicType::TodoTypeAlias)),
SpecialFormType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")),
Expand Down Expand Up @@ -5481,7 +5492,7 @@ impl<'db> Type<'db> {
let mut builder = UnionBuilder::new(db);
let mut invalid_expressions = smallvec::SmallVec::default();
for element in union.elements(db) {
match element.in_type_expression(db, scope_id) {
match element.in_type_expression(db, scope_id, legacy_typevar_binding_context) {
Ok(type_expr) => builder = builder.add(type_expr),
Err(InvalidTypeExpressionError {
fallback_type,
Expand Down Expand Up @@ -6648,7 +6659,7 @@ impl<'db> InvalidTypeExpression<'db> {
return;
};
if module_member_with_same_name
.in_type_expression(db, scope)
.in_type_expression(db, scope, None)
.is_err()
{
return;
Expand Down
43 changes: 39 additions & 4 deletions crates/ty_python_semantic/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind};
use crate::semantic_index::{SemanticIndex, semantic_index};
use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::infer::infer_definition_types;
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
Expand All @@ -20,7 +21,7 @@ use crate::{Db, FxOrderSet};

/// Returns an iterator of any generic context introduced by the given scope or any enclosing
/// scope.
pub(crate) fn enclosing_generic_contexts<'db>(
fn enclosing_generic_contexts<'db>(
db: &'db dyn Db,
module: &ParsedModuleRef,
index: &SemanticIndex<'db>,
Expand All @@ -35,7 +36,9 @@ pub(crate) fn enclosing_generic_contexts<'db>(
.generic_context(db)
}
NodeWithScopeKind::Function(function) => {
binding_type(db, index.expect_single_definition(function.node(module)))
infer_definition_types(db, index.expect_single_definition(function.node(module)))
.undecorated_type()
.expect("function should have undecorated type")
.into_function_literal()?
.signature(db)
.iter()
Expand All @@ -59,6 +62,37 @@ fn bound_legacy_typevars<'db>(
.filter(|typevar| typevar.is_legacy(db))
}

/// Binds an unbound legacy typevar.
///
/// When a legacy typevar is first created, we will have a [`TypeVarInstance`] which does not have
/// an associated binding context. When the typevar is used in a generic class or function, we
/// "bind" it, adding the [`Definition`] of the generic class or function as its "binding context".
///
/// When an expression resolves to a legacy typevar, our inferred type will refer to the unbound
/// [`TypeVarInstance`] from when the typevar was first created. This function walks the scopes
/// that enclosing the expression, looking for the innermost binding context that binds the
/// typevar.
///
/// If no enclosing scope has already bound the typevar, we might be in a syntactic position that
/// is about to bind it (indicated by a non-`None` `legacy_typevar_binding_context`), in which case
/// we bind the typevar with that new binding context.
pub(crate) fn bind_legacy_typevar<'db>(
db: &'db dyn Db,
module: &ParsedModuleRef,
index: &SemanticIndex<'db>,
containing_scope: FileScopeId,
legacy_typevar_binding_context: Option<Definition<'db>>,
typevar: TypeVarInstance<'db>,
) -> Option<TypeVarInstance<'db>> {
enclosing_generic_contexts(db, module, index, containing_scope)
.find_map(|enclosing_context| enclosing_context.binds_legacy_typevar(db, typevar))
.or_else(|| {
legacy_typevar_binding_context.map(|legacy_typevar_binding_context| {
typevar.with_binding_context(db, legacy_typevar_binding_context)
})
})
}

/// A list of formal type variables for a generic function, class, or type alias.
///
/// TODO: Handle nested generic contexts better, with actual parent links to the lexically
Expand Down Expand Up @@ -259,12 +293,13 @@ impl<'db> GenericContext<'db> {
db: &'db dyn Db,
typevar: TypeVarInstance<'db>,
) -> Option<TypeVarInstance<'db>> {
assert!(typevar.is_legacy(db));
assert!(typevar.is_legacy(db) || typevar.is_implicit(db));
let typevar_def = typevar.definition(db);
self.variables(db)
.iter()
.find(|self_typevar| {
self_typevar.is_legacy(db) && self_typevar.definition(db) == typevar_def
(self_typevar.is_legacy(db) || self_typevar.is_implicit(db))
&& self_typevar.definition(db) == typevar_def
})
.copied()
}
Expand Down
Loading
Loading