Skip to content
Closed
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
3 changes: 3 additions & 0 deletions pyrefly/lib/solver/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2985,6 +2985,8 @@ pub enum SubsetError {
PosParamName(Name, Name),
/// `got` does not accept positional parameters that `want` allows callers to pass.
CallableMissingPositionalParameters(Vec<Type>),
/// Explicit positional callable parameters are incompatible.
CallablePositionalParameterMismatch,
/// Instantiations for quantified vars are incompatible with bounds
TypeVarSpecialization(Vec1<TypeVarSpecializationError>),
/// `got` is missing an attribute that the Protocol `want` requires
Expand Down Expand Up @@ -3039,6 +3041,7 @@ impl SubsetError {
)
})
}
SubsetError::CallablePositionalParameterMismatch => None,
SubsetError::TypeVarSpecialization(_) => {
// TODO
None
Expand Down
26 changes: 22 additions & 4 deletions pyrefly/lib/solver/subset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,13 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
// Don't short-circuit because we may want to pin/solve variables
let result = self.is_subset_param_list_impl(l_args, u_args);
match result {
Err(_) if !self.solver.config.strict_callable_subtyping && (l_gradual || u_gradual) => {
// A gradual want/base (`*args: Any, **kwargs: Any`) accepts any got/override.
Err(_) if !self.solver.config.strict_callable_subtyping && u_gradual => Ok(()),
// A gradual got still softens unstructured mismatches, but not explicit
// positional prefix failures (see CallablePositionalParameterMismatch).
Err(SubsetError::Other)
if !self.solver.config.strict_callable_subtyping && l_gradual =>
{
Ok(())
}
_ => result,
Expand Down Expand Up @@ -286,7 +292,8 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
Some(Param::PosOnly(_, l, l_req) | Param::Pos(_, l, l_req)),
Some(Param::PosOnly(_, u, u_req)),
) if (*u_req == Required::Required || matches!(l_req, Required::Optional(_))) => {
self.is_subset_eq(u, l)?;
self.is_subset_eq(u, l)
.map_err(|_| SubsetError::CallablePositionalParameterMismatch)?;
l_arg = l_args.next();
u_arg = u_args.next();
}
Expand All @@ -296,10 +303,15 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
if l_name != u_name {
return Err(SubsetError::PosParamName(l_name.clone(), u_name.clone()));
}
self.is_subset_eq(u, l)?;
self.is_subset_eq(u, l)
.map_err(|_| SubsetError::CallablePositionalParameterMismatch)?;
l_arg = l_args.next();
u_arg = u_args.next();
}
(
Some(Param::PosOnly(..) | Param::Pos(..)),
Some(Param::PosOnly(..) | Param::Pos(..)),
) => return Err(SubsetError::CallablePositionalParameterMismatch),
(Some(Param::Varargs(_, Type::Unpack(l))), None) => {
self.is_subset_eq(&self.solver.heap.mk_concrete_tuple(Vec::new()), l)?;
l_arg = l_args.next();
Expand Down Expand Up @@ -686,7 +698,13 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
}
};
match result {
Err(_) if !self.solver.config.strict_callable_subtyping && (l_gradual || u_gradual) => {
// A gradual want/base (`*args: Any, **kwargs: Any`) accepts any got/override.
Err(_) if !self.solver.config.strict_callable_subtyping && u_gradual => Ok(()),
// A gradual got still softens unstructured mismatches, but not explicit
// positional prefix failures (see CallablePositionalParameterMismatch).
Err(SubsetError::Other)
if !self.solver.config.strict_callable_subtyping && l_gradual =>
{
Ok(())
}
_ => result,
Expand Down
21 changes: 21 additions & 0 deletions pyrefly/lib/test/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2363,6 +2363,27 @@ def f(n: NoArgs) -> None:
"#,
);

// https://github.com/facebook/pyrefly/issues/5194
testcase!(
test_gradual_variadic_params_keep_explicit_prefix,
r#"
from typing import Any, Callable, Protocol

class Gradual(Protocol):
def __call__(self, *args: Any, **kwargs: Any) -> None: ...

class Prefixed(Protocol):
def __call__(self, value: str, /, *args: Any, **kwargs: Any) -> None: ...

def prefixed_function(value: str, /, *args: Any, **kwargs: Any) -> None: ...

def f(gradual: Gradual, prefixed: Prefixed) -> None:
accepted: Callable[[int], None] = gradual
protocol_error: Callable[[int], None] = prefixed # E: `Prefixed` is not assignable to `(int) -> None`
function_error: Callable[[int], None] = prefixed_function # E: `(value: str, /, *args: Any, **kwargs: Any) -> None` is not assignable to `(int) -> None`
"#,
);

testcase!(
test_defaultdict_frozenset,
r#"
Expand Down
Loading