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
1 change: 0 additions & 1 deletion Lib/test/test_sqlite3/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,6 @@ def test_trace_bad_handler(self):
cx.set_trace_callback(lambda stmt: 5/0)
cx.execute("select 1")

@unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for set_trace_callback
def test_set_trace_callback_keyword_args(self):
with self.assertRaisesRegex(TypeError,
'takes exactly 1 positional argument'):
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_sqlite3/test_userfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,6 @@ def test_aggr_text(self):
val = cur.fetchone()[0]
self.assertEqual(val, txt)

@unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for create_aggregate
def test_agg_keyword_args(self):
with self.assertRaisesRegex(TypeError,
'takes exactly 3 positional arguments'):
Expand Down Expand Up @@ -766,7 +765,6 @@ def test_clear_authorizer(self):
self.con.execute("select * from t2")
self.con.execute("select c2 from t1")

@unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for set_authorizer
def test_authorizer_keyword_args(self):
with self.assertRaisesRegex(TypeError,
'takes exactly 1 positional argument'):
Expand Down
4 changes: 4 additions & 0 deletions crates/derive-impl/src/from_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,10 @@ pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {

#binding_params

const FORCE_UNPACK_KEYWORDS: bool = false #(
|| <#flattened as ::rustpython_vm::function::FromArgs>::FORCE_UNPACK_KEYWORDS
)*;

const HAS_REQUIRED_KEYWORD_ONLY: bool =
::rustpython_vm::function::params_have_required_keyword_only(Self::PARAMS) #(
|| <#flattened as ::rustpython_vm::function::FromArgs>::HAS_REQUIRED_KEYWORD_ONLY
Expand Down
1 change: 1 addition & 0 deletions crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2702,6 +2702,7 @@ fn class_internal_doc(
name: "",
params: CHOSEN,
binding_params: CHOSEN,
force_unpack_keywords: false,
},
];
const BASE: ::rustpython_vm::function::ItemDoc =
Expand Down
42 changes: 32 additions & 10 deletions crates/stdlib/src/_sqlite3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ mod _sqlite3 {
function::{
ArgCallable, ArgIntoBool, ArgIntoFloat, ArgIterable, ArgStrOrBytesLike, ArgStrictInt,
FsPath, FuncArgs, KwArgs, OptionalArg, PyComparisonValue, PySetterValue,
UnpackKeywords,
},
object::{Traverse, TraverseFn},
protocol::{
Expand Down Expand Up @@ -484,6 +485,24 @@ mod _sqlite3 {
aggregate_class: PyObjectRef,
}

#[derive(FromArgs)]
struct CreateCollationArgs {
#[pyarg(positional)]
name: SqliteStringArg,
#[pyarg(positional)]
callback: PyObjectRef,
}

#[derive(FromArgs)]
struct CreateWindowFunctionArgs {
#[pyarg(positional)]
name: SqliteStringArg,
#[pyarg(positional)]
num_params: c_int,
#[pyarg(positional)]
aggregate_class: PyObjectRef,
}

#[derive(FromArgs)]
struct CompleteStatementArgs {
#[pyarg(any)]
Expand Down Expand Up @@ -1714,7 +1733,7 @@ mod _sqlite3 {
#[pymethod]
fn create_aggregate(
zelf: &Py<Self>,
args: CreateAggregateArgs,
UnpackKeywords(args): UnpackKeywords<CreateAggregateArgs>,
vm: &VirtualMachine,
) -> PyResult<()> {
with_deferred_drops(|| Self::create_aggregate_inner(zelf, args, vm))
Expand Down Expand Up @@ -1758,11 +1777,10 @@ mod _sqlite3 {
#[pymethod]
fn create_collation(
zelf: &Py<Self>,
name: SqliteStringArg,
callback: PyObjectRef,
UnpackKeywords(args): UnpackKeywords<CreateCollationArgs>,
vm: &VirtualMachine,
) -> PyResult<()> {
with_deferred_drops(|| Self::create_collation_inner(zelf, name, callback, vm))
with_deferred_drops(|| Self::create_collation_inner(zelf, args.name, args.callback, vm))
}

fn create_collation_inner(
Expand Down Expand Up @@ -1811,13 +1829,17 @@ mod _sqlite3 {
#[pymethod]
fn create_window_function(
zelf: &Py<Self>,
name: SqliteStringArg,
num_params: c_int,
aggregate_class: PyObjectRef,
UnpackKeywords(args): UnpackKeywords<CreateWindowFunctionArgs>,
vm: &VirtualMachine,
) -> PyResult<()> {
with_deferred_drops(|| {
Self::create_window_function_inner(zelf, name, num_params, aggregate_class, vm)
Self::create_window_function_inner(
zelf,
args.name,
args.num_params,
args.aggregate_class,
vm,
)
})
}

Expand Down Expand Up @@ -1869,7 +1891,7 @@ mod _sqlite3 {
#[pymethod]
fn set_authorizer(
zelf: &Py<Self>,
authorizer_callback: PyObjectRef,
UnpackKeywords(authorizer_callback): UnpackKeywords<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
zelf.set_callback(authorizer_callback, CallbackKind::Authorizer, vm)
Expand All @@ -1878,7 +1900,7 @@ mod _sqlite3 {
#[pymethod]
fn set_trace_callback(
zelf: &Py<Self>,
trace_callback: PyObjectRef,
UnpackKeywords(trace_callback): UnpackKeywords<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
zelf.set_callback(trace_callback, CallbackKind::Trace, vm)
Expand Down
120 changes: 118 additions & 2 deletions crates/vm/src/function/argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,21 @@ impl BindingShape {
Self::from_params(params, skip)
}

const fn for_parser(
params: Option<&[Param]>,
skip: usize,
force_unpack_keywords: bool,
) -> Option<Self> {
let Some(mut shape) = Self::validated(params, skip) else {
return None;
};
if force_unpack_keywords {
shape.has_keywords = true;
shape.anonymous_positional = false;
}
Some(shape)
}

const fn arity(&self) -> RangeInclusive<usize> {
self.required_positional..=if self.var_positional {
usize::MAX
Expand Down Expand Up @@ -1068,6 +1083,12 @@ pub trait FromArgs: Sized {
None => Some(&[Param::positional_only("")]),
};

/// Force `_PyArg_UnpackKeywords` calling and validation conventions, even
/// when every parameter is positional-only. Otherwise they are inferred
/// from the binding parameters. This does not allow passing positional-only
/// parameters by keyword.
const FORCE_UNPACK_KEYWORDS: bool = false;

/// True when every parameter is positional-only or `*args`.
const TAKES_NO_KEYWORDS: bool = match Self::PARAMS {
Some(params) => params_take_no_keywords(params),
Expand Down Expand Up @@ -1130,9 +1151,9 @@ pub trait FromArgs: Sized {
vm: &VirtualMachine,
) -> Option<PyBaseExceptionRef> {
let shape = if instance == 0 {
const { BindingShape::validated(Self::BINDING_PARAMS, 0) }
const { BindingShape::for_parser(Self::BINDING_PARAMS, 0, Self::FORCE_UNPACK_KEYWORDS) }
} else {
const { BindingShape::validated(Self::BINDING_PARAMS, 1) }
const { BindingShape::for_parser(Self::BINDING_PARAMS, 1, Self::FORCE_UNPACK_KEYWORDS) }
};
if let Some(shape) = shape {
if args.kwargs.is_empty()
Expand Down Expand Up @@ -1174,6 +1195,46 @@ pub trait FromArgs: Sized {
fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError>;
}

/// Select CPython's `_PyArg_UnpackKeywords` calling and validation conventions
/// for typed arguments, including an entirely positional-only signature.
/// Reuses the ordinary binder and preserves `T`'s conversions and signature;
/// positional-only parameters still cannot be passed by keyword. `T` must
/// expose a known binding shape; opaque and legacy parsers retain their own
/// validation order.
#[repr(transparent)]
pub struct UnpackKeywords<T>(pub T);

impl<T: FromArgs> FromArgs for UnpackKeywords<T> {
const PARAMS: Option<&'static [Param]> = T::PARAMS;
const BINDING_PARAMS: Option<&'static [Param]> = {
assert!(
BindingShape::from_params(T::BINDING_PARAMS, 0).is_some(),
"UnpackKeywords requires a known binding shape"
);
T::BINDING_PARAMS
};
const FORCE_UNPACK_KEYWORDS: bool = true;
const TAKES_NO_KEYWORDS: bool = T::TAKES_NO_KEYWORDS;
const HAS_REQUIRED_KEYWORD_ONLY: bool = T::HAS_REQUIRED_KEYWORD_ONLY;

fn arity() -> RangeInclusive<usize> {
T::arity()
}

fn missing_from(
nargs: usize,
kwargs: &KwArgs,
pos: &mut usize,
skip: &mut usize,
) -> Result<(), Option<ArgumentError>> {
T::missing_from(nargs, kwargs, pos, skip)
}

fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
T::from_args(vm, args).map(Self)
}
}

pub trait FromArgOptional {
type Inner: TryFromObject;
fn from_inner(x: Self::Inner) -> Self;
Expand Down Expand Up @@ -1627,6 +1688,8 @@ macro_rules! tuple_from_py_func_args {
}
};

const FORCE_UNPACK_KEYWORDS: bool = false $(|| $T::FORCE_UNPACK_KEYWORDS)*;

const HAS_REQUIRED_KEYWORD_ONLY: bool = false $(|| $T::HAS_REQUIRED_KEYWORD_ONLY)*;

fn arity() -> RangeInclusive<usize> {
Expand Down Expand Up @@ -1882,6 +1945,59 @@ mod tests {
}
}

#[allow(dead_code)]
#[derive(FromArgs)]
struct FlattenedUnpackKeywords {
#[pyarg(flatten)]
args: super::UnpackKeywords<PyObjectRef>,
}

#[test]
fn unpack_keywords_uses_clinic_order_for_positional_only_parameters() {
type Wrapped = super::UnpackKeywords<PyObjectRef>;
crate::Interpreter::without_stdlib(Default::default()).enter(|vm| {
for (nargs, names, expected) in [
(
0,
&[][..],
"example() takes exactly 1 positional argument (0 given)",
),
(2, &[][..], "example() takes at most 1 argument (2 given)"),
(
0,
&["value"][..],
"example() takes exactly 1 positional argument (0 given)",
),
(
0,
&["value", "other"][..],
"example() takes at most 1 keyword argument (2 given)",
),
(
1,
&["value"][..],
"example() takes at most 1 argument (2 given)",
),
] {
assert_eq!(binding_error::<Wrapped>(nargs, names, false, vm), expected);
assert_eq!(
binding_error::<(PyObjectRef, Wrapped)>(nargs + 1, names, true, vm),
expected,
);
assert_eq!(
binding_error::<FlattenedUnpackKeywords>(nargs, names, false, vm),
expected,
);
}
let args = FuncArgs::new(vec![vm.ctx.none()], KwArgs::default());
assert!(args.bind_for::<Wrapped>(vm, "example").is_ok());
assert_eq!(
binding_error::<PyObjectRef>(0, &["value"], false, vm),
"example() takes no keyword arguments",
);
});
}

#[test]
fn clinic_checks_shape_before_converting_and_missing_before_duplicates() {
crate::Interpreter::without_stdlib(Default::default()).enter(|vm| {
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub use argument::{
ArgumentError, Callee, FromArgOptional, FromArgs, FuncArgs, IntoFuncArgs, KwArgs, KwArgsMap,
NameArgs, NameChanges, NameCoordinates, NameExcInfo, NameFields, NameIntegers, NameIterables,
NameKeywords, NameKwargs, NameKwds, NameKws, NameObjects, NameObjs, NameOthers, NameValues,
OptionalArg, OptionalArgDefault, OptionalOption, PosArgs, PositionalIterable,
OptionalArg, OptionalArgDefault, OptionalOption, PosArgs, PositionalIterable, UnpackKeywords,
params_have_required_keyword_only,
};
pub(crate) use argument::{
Expand Down
Loading
Loading