[ty] Infer function call typevars in both directions (#18155)

This primarily comes up with annotated `self` parameters in
constructors:

```py
class C[T]:
    def __init__(self: C[int]): ...
```

Here, we want infer a specialization of `{T = int}` for a call that hits
this overload.

Normally when inferring a specialization of a function call, typevars
appear in the parameter annotations, and not in the argument types. In
this case, this is reversed: we need to verify that the `self` argument
(`C[T]`, as we have not yet completed specialization inference) is
assignable to the parameter type `C[int]`.

To do this, we simply look for a typevar/type in both directions when
performing inference, and apply the inferred specialization to argument
types as well as parameter types before verifying assignability.

As a wrinkle, this exposed that we were not checking
subtyping/assignability for function literals correctly. Our function
literal representation includes an optional specialization that should
be applied to the signature. Before, function literals were considered
subtypes of (assignable to) each other only if they were identical Salsa
objects. Two function literals with different specializations should
still be considered subtypes of (assignable to) each other if those
specializations result in the same function signature (typically because
the function doesn't use the typevars in the specialization).

Closes https://github.com/astral-sh/ty/issues/370
Closes https://github.com/astral-sh/ty/issues/100
Closes https://github.com/astral-sh/ty/issues/258

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Douglas Creager 2025-05-19 11:45:40 -04:00 committed by GitHub
parent 569c94b71b
commit 97058e8093
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 366 additions and 53 deletions

View file

@ -341,6 +341,39 @@ reveal_type(C(1, True)) # revealed: C[Literal[1]]
wrong_innards: C[int] = C("five", 1)
```
### Some `__init__` overloads only apply to certain specializations
```py
from typing import overload, Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
@overload
def __init__(self: "C[str]", x: str) -> None: ...
@overload
def __init__(self: "C[bytes]", x: bytes) -> None: ...
@overload
def __init__(self, x: int) -> None: ...
def __init__(self, x: str | bytes | int) -> None: ...
reveal_type(C("string")) # revealed: C[str]
reveal_type(C(b"bytes")) # revealed: C[bytes]
reveal_type(C(12)) # revealed: C[Unknown]
C[str]("string")
C[str](b"bytes") # error: [no-matching-overload]
C[str](12)
C[bytes]("string") # error: [no-matching-overload]
C[bytes](b"bytes")
C[bytes](12)
C[None]("string") # error: [no-matching-overload]
C[None](b"bytes") # error: [no-matching-overload]
C[None](12)
```
## Generic subclass
When a generic subclass fills its superclass's type parameter with one of its own, the actual types

View file

@ -279,6 +279,37 @@ reveal_type(C(1, True)) # revealed: C[Literal[1]]
wrong_innards: C[int] = C("five", 1)
```
### Some `__init__` overloads only apply to certain specializations
```py
from typing import overload
class C[T]:
@overload
def __init__(self: C[str], x: str) -> None: ...
@overload
def __init__(self: C[bytes], x: bytes) -> None: ...
@overload
def __init__(self, x: int) -> None: ...
def __init__(self, x: str | bytes | int) -> None: ...
reveal_type(C("string")) # revealed: C[str]
reveal_type(C(b"bytes")) # revealed: C[bytes]
reveal_type(C(12)) # revealed: C[Unknown]
C[str]("string")
C[str](b"bytes") # error: [no-matching-overload]
C[str](12)
C[bytes]("string") # error: [no-matching-overload]
C[bytes](b"bytes")
C[bytes](12)
C[None]("string") # error: [no-matching-overload]
C[None](b"bytes") # error: [no-matching-overload]
C[None](12)
```
## Generic subclass
When a generic subclass fills its superclass's type parameter with one of its own, the actual types

View file

@ -102,7 +102,7 @@ class C[T]:
return "a"
reveal_type(getattr_static(C[int], "f")) # revealed: def f(self, x: int) -> str
reveal_type(getattr_static(C[int], "f").__get__) # revealed: <method-wrapper `__get__` of `f[int]`>
reveal_type(getattr_static(C[int], "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: def f(self, x: int) -> str
# revealed: bound method C[int].f(x: int) -> str
reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int]))

View file

@ -1170,6 +1170,21 @@ impl<'db> Type<'db> {
target.is_equivalent_to(db, Type::object(db))
}
// These clauses handle type variants that include function literals. A function
// literal is the subtype of itself, and not of any other function literal. However,
// our representation of a function literal includes any specialization that should be
// applied to the signature. Different specializations of the same function literal are
// only subtypes of each other if they result in the same signature.
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
self_function.is_subtype_of(db, target_function)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_subtype_of(db, target_method)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_subtype_of(db, target_method)
}
// No literal type is a subtype of any other literal type, unless they are the same
// type (which is handled above). This case is not necessary from a correctness
// perspective (the fallback cases below will handle it correctly), but it is important
@ -1504,6 +1519,21 @@ impl<'db> Type<'db> {
true
}
// These clauses handle type variants that include function literals. A function
// literal is assignable to itself, and not to any other function literal. However, our
// representation of a function literal includes any specialization that should be
// applied to the signature. Different specializations of the same function literal are
// only assignable to each other if they result in the same signature.
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
self_function.is_assignable_to(db, target_function)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_assignable_to(db, target_method)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_assignable_to(db, target_method)
}
// `type[Any]` is assignable to any `type[...]` type, because `type[Any]` can
// materialize to any `type[...]` type.
(Type::SubclassOf(subclass_of_ty), Type::SubclassOf(_))
@ -1627,6 +1657,15 @@ impl<'db> Type<'db> {
left.is_equivalent_to(db, right)
}
(Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
self_function.is_equivalent_to(db, target_function)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_equivalent_to(db, target_method)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_equivalent_to(db, target_method)
}
(Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right),
(Type::NominalInstance(left), Type::NominalInstance(right)) => {
left.is_equivalent_to(db, right)
@ -1682,6 +1721,15 @@ impl<'db> Type<'db> {
first.is_gradual_equivalent_to(db, second)
}
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
self_function.is_gradual_equivalent_to(db, target_function)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_gradual_equivalent_to(db, target_method)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_gradual_equivalent_to(db, target_method)
}
(Type::Callable(first), Type::Callable(second)) => {
first.is_gradual_equivalent_to(db, second)
}
@ -6727,6 +6775,7 @@ impl<'db> FunctionType<'db> {
/// would depend on the function's AST and rerun for every change in that file.
#[salsa::tracked(returns(ref), cycle_fn=signature_cycle_recover, cycle_initial=signature_cycle_initial)]
pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> {
let inherited_generic_context = self.inherited_generic_context(db);
let specialization = self.specialization(db);
if let Some(overloaded) = self.to_overloaded(db) {
FunctionSignature {
@ -6734,13 +6783,13 @@ impl<'db> FunctionType<'db> {
Type::FunctionLiteral(self),
overloaded.overloads.iter().copied().map(|overload| {
overload
.internal_signature(db)
.internal_signature(db, inherited_generic_context)
.apply_optional_specialization(db, specialization)
}),
),
implementation: overloaded.implementation.map(|implementation| {
implementation
.internal_signature(db)
.internal_signature(db, inherited_generic_context)
.apply_optional_specialization(db, specialization)
}),
}
@ -6748,7 +6797,7 @@ impl<'db> FunctionType<'db> {
FunctionSignature {
overloads: CallableSignature::single(
Type::FunctionLiteral(self),
self.internal_signature(db)
self.internal_signature(db, inherited_generic_context)
.apply_optional_specialization(db, specialization),
),
implementation: None,
@ -6766,7 +6815,11 @@ impl<'db> FunctionType<'db> {
///
/// Don't call this when checking any other file; only when type-checking the function body
/// scope.
fn internal_signature(self, db: &'db dyn Db) -> Signature<'db> {
fn internal_signature(
self,
db: &'db dyn Db,
inherited_generic_context: Option<GenericContext<'db>>,
) -> Signature<'db> {
let scope = self.body_scope(db);
let function_stmt_node = scope.node(db).expect_function();
let definition = self.definition(db);
@ -6777,7 +6830,7 @@ impl<'db> FunctionType<'db> {
Signature::from_function(
db,
generic_context,
self.inherited_generic_context(db),
inherited_generic_context,
definition,
function_stmt_node,
)
@ -6934,6 +6987,42 @@ impl<'db> FunctionType<'db> {
}
}
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
// A function literal is the subtype of itself, and not of any other function literal.
// However, our representation of a function literal includes any specialization that
// should be applied to the signature. Different specializations of the same function
// literal are only subtypes of each other if they result in subtype signatures.
self.body_scope(db) == other.body_scope(db)
&& self
.into_callable_type(db)
.is_subtype_of(db, other.into_callable_type(db))
}
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
// A function literal is assignable to itself, and not to any other function literal.
// However, our representation of a function literal includes any specialization that
// should be applied to the signature. Different specializations of the same function
// literal are only assignable to each other if they result in assignable signatures.
self.body_scope(db) == other.body_scope(db)
&& self
.into_callable_type(db)
.is_assignable_to(db, other.into_callable_type(db))
}
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.body_scope(db) == other.body_scope(db)
&& self
.into_callable_type(db)
.is_equivalent_to(db, other.into_callable_type(db))
}
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.body_scope(db) == other.body_scope(db)
&& self
.into_callable_type(db)
.is_gradual_equivalent_to(db, other.into_callable_type(db))
}
/// Returns a tuple of two spans. The first is
/// the span for the identifier of the function
/// definition for `self`. The second is
@ -7215,6 +7304,43 @@ impl<'db> BoundMethodType<'db> {
.map(signatures::Signature::bind_self),
))
}
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
// A bound method is a typically a subtype of itself. However, we must explicitly verify
// the subtyping of the underlying function signatures (since they might be specialized
// differently), and of the bound self parameter (taking care that parameters, including a
// bound self parameter, are contravariant.)
self.function(db).is_subtype_of(db, other.function(db))
&& other
.self_instance(db)
.is_subtype_of(db, self.self_instance(db))
}
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
// A bound method is a typically assignable to itself. However, we must explicitly verify
// the assignability of the underlying function signatures (since they might be specialized
// differently), and of the bound self parameter (taking care that parameters, including a
// bound self parameter, are contravariant.)
self.function(db).is_assignable_to(db, other.function(db))
&& other
.self_instance(db)
.is_assignable_to(db, self.self_instance(db))
}
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.function(db).is_equivalent_to(db, other.function(db))
&& other
.self_instance(db)
.is_equivalent_to(db, self.self_instance(db))
}
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.function(db)
.is_gradual_equivalent_to(db, other.function(db))
&& other
.self_instance(db)
.is_gradual_equivalent_to(db, self.self_instance(db))
}
}
/// This type represents the set of all callable objects with a certain, possibly overloaded,
@ -7445,6 +7571,140 @@ pub enum MethodWrapperKind<'db> {
StrStartswith(StringLiteralType<'db>),
}
impl<'db> MethodWrapperKind<'db> {
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
match (self, other) {
(
MethodWrapperKind::FunctionTypeDunderGet(self_function),
MethodWrapperKind::FunctionTypeDunderGet(other_function),
) => self_function.is_subtype_of(db, other_function),
(
MethodWrapperKind::FunctionTypeDunderCall(self_function),
MethodWrapperKind::FunctionTypeDunderCall(other_function),
) => self_function.is_subtype_of(db, other_function),
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
self == other
}
(
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
) => false,
}
}
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
match (self, other) {
(
MethodWrapperKind::FunctionTypeDunderGet(self_function),
MethodWrapperKind::FunctionTypeDunderGet(other_function),
) => self_function.is_assignable_to(db, other_function),
(
MethodWrapperKind::FunctionTypeDunderCall(self_function),
MethodWrapperKind::FunctionTypeDunderCall(other_function),
) => self_function.is_assignable_to(db, other_function),
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
self == other
}
(
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
) => false,
}
}
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
match (self, other) {
(
MethodWrapperKind::FunctionTypeDunderGet(self_function),
MethodWrapperKind::FunctionTypeDunderGet(other_function),
) => self_function.is_equivalent_to(db, other_function),
(
MethodWrapperKind::FunctionTypeDunderCall(self_function),
MethodWrapperKind::FunctionTypeDunderCall(other_function),
) => self_function.is_equivalent_to(db, other_function),
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
self == other
}
(
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
) => false,
}
}
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
match (self, other) {
(
MethodWrapperKind::FunctionTypeDunderGet(self_function),
MethodWrapperKind::FunctionTypeDunderGet(other_function),
) => self_function.is_gradual_equivalent_to(db, other_function),
(
MethodWrapperKind::FunctionTypeDunderCall(self_function),
MethodWrapperKind::FunctionTypeDunderCall(other_function),
) => self_function.is_gradual_equivalent_to(db, other_function),
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
self == other
}
(
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
) => false,
}
}
}
/// Represents a specific instance of `types.WrapperDescriptorType`
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update)]
pub enum WrapperDescriptorKind {

View file

@ -1412,7 +1412,7 @@ impl<'db> Binding<'db> {
}
num_synthetic_args = 0;
for (argument_index, (argument, argument_type)) in argument_types.iter().enumerate() {
for (argument_index, (argument, mut argument_type)) in argument_types.iter().enumerate() {
if matches!(argument, Argument::Synthetic) {
num_synthetic_args += 1;
}
@ -1424,9 +1424,12 @@ impl<'db> Binding<'db> {
let parameter = &parameters[parameter_index];
if let Some(mut expected_ty) = parameter.annotated_type() {
if let Some(specialization) = self.specialization {
argument_type = argument_type.apply_specialization(db, specialization);
expected_ty = expected_ty.apply_specialization(db, specialization);
}
if let Some(inherited_specialization) = self.inherited_specialization {
argument_type =
argument_type.apply_specialization(db, inherited_specialization);
expected_ty = expected_ty.apply_specialization(db, inherited_specialization);
}
if !argument_type.is_assignable_to(db, expected_ty) {

View file

@ -170,31 +170,15 @@ impl Display for DisplayRepresentation<'_> {
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
write!(
f,
"<method-wrapper `__get__` of `{function}{specialization}`>",
"<method-wrapper `__get__` of `{function}`>",
function = function.name(self.db),
specialization = if let Some(specialization) = function.specialization(self.db)
{
specialization
.display_short(self.db, TupleSpecialization::No)
.to_string()
} else {
String::new()
},
)
}
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)) => {
write!(
f,
"<method-wrapper `__call__` of `{function}{specialization}`>",
"<method-wrapper `__call__` of `{function}`>",
function = function.name(self.db),
specialization = if let Some(specialization) = function.specialization(self.db)
{
specialization
.display_short(self.db, TupleSpecialization::No)
.to_string()
} else {
String::new()
},
)
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => {

View file

@ -641,32 +641,34 @@ impl<'db> SpecializationBuilder<'db> {
}
match (formal, actual) {
(Type::TypeVar(typevar), _) => match typevar.bound_or_constraints(self.db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
if !actual.is_assignable_to(self.db, bound) {
return Err(SpecializationError::MismatchedBound {
(Type::TypeVar(typevar), ty) | (ty, Type::TypeVar(typevar)) => {
match typevar.bound_or_constraints(self.db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
if !ty.is_assignable_to(self.db, bound) {
return Err(SpecializationError::MismatchedBound {
typevar,
argument: ty,
});
}
self.add_type_mapping(typevar, ty);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
for constraint in constraints.iter(self.db) {
if ty.is_assignable_to(self.db, *constraint) {
self.add_type_mapping(typevar, *constraint);
return Ok(());
}
}
return Err(SpecializationError::MismatchedConstraint {
typevar,
argument: actual,
argument: ty,
});
}
self.add_type_mapping(typevar, actual);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
for constraint in constraints.iter(self.db) {
if actual.is_assignable_to(self.db, *constraint) {
self.add_type_mapping(typevar, *constraint);
return Ok(());
}
_ => {
self.add_type_mapping(typevar, ty);
}
return Err(SpecializationError::MismatchedConstraint {
typevar,
argument: actual,
});
}
_ => {
self.add_type_mapping(typevar, actual);
}
},
}
(Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => {
let formal_elements = formal_tuple.elements(self.db);

View file

@ -1528,7 +1528,7 @@ mod tests {
db.write_dedented("/src/a.py", "def f(): ...").unwrap();
let func = get_function_f(&db, "/src/a.py");
let sig = func.internal_signature(&db);
let sig = func.internal_signature(&db, None);
assert!(sig.return_ty.is_none());
assert_params(&sig, &[]);
@ -1551,7 +1551,7 @@ mod tests {
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let sig = func.internal_signature(&db);
let sig = func.internal_signature(&db, None);
assert_eq!(sig.return_ty.unwrap().display(&db).to_string(), "bytes");
assert_params(
@ -1602,7 +1602,7 @@ mod tests {
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let sig = func.internal_signature(&db);
let sig = func.internal_signature(&db, None);
let [
Parameter {
@ -1638,7 +1638,7 @@ mod tests {
.unwrap();
let func = get_function_f(&db, "/src/a.pyi");
let sig = func.internal_signature(&db);
let sig = func.internal_signature(&db, None);
let [
Parameter {
@ -1674,7 +1674,7 @@ mod tests {
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let sig = func.internal_signature(&db);
let sig = func.internal_signature(&db, None);
let [
Parameter {
@ -1720,7 +1720,7 @@ mod tests {
.unwrap();
let func = get_function_f(&db, "/src/a.pyi");
let sig = func.internal_signature(&db);
let sig = func.internal_signature(&db, None);
let [
Parameter {
@ -1756,7 +1756,7 @@ mod tests {
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let expected_sig = func.internal_signature(&db);
let expected_sig = func.internal_signature(&db, None);
// With no decorators, internal and external signature are the same
assert_eq!(