1use crate::{
2 exceptions::PyTypeError,
3 ffi,
4 pyclass::boolean_struct::False,
5 types::{PyDict, PyString, PyTuple},
6 FromPyObject, PyAny, PyClass, PyErr, PyRef, PyRefMut, PyResult, Python,
7};
8
9/// A trait which is used to help PyO3 macros extract function arguments.
10///
11/// `#[pyclass]` structs need to extract as `PyRef<T>` and `PyRefMut<T>`
12/// wrappers rather than extracting `&T` and `&mut T` directly. The `Holder` type is used
13/// to hold these temporary wrappers - the way the macro is constructed, these wrappers
14/// will be dropped as soon as the pyfunction call ends.
15///
16/// There exists a trivial blanket implementation for `T: FromPyObject` with `Holder = ()`.
17pub trait PyFunctionArgument<'a, 'py>: Sized + 'a {
18 type Holder: FunctionArgumentHolder;
19 fn extract(obj: &'py PyAny, holder: &'a mut Self::Holder) -> PyResult<Self>;
20}
21
22impl<'a, 'py, T> PyFunctionArgument<'a, 'py> for T
23where
24 T: FromPyObject<'py> + 'a,
25{
26 type Holder = ();
27
28 #[inline]
29 fn extract(obj: &'py PyAny, _: &'a mut ()) -> PyResult<Self> {
30 obj.extract()
31 }
32}
33
34/// Trait for types which can be a function argument holder - they should
35/// to be able to const-initialize to an empty value.
36pub trait FunctionArgumentHolder: Sized {
37 const INIT: Self;
38}
39
40impl FunctionArgumentHolder for () {
41 const INIT: Self = ();
42}
43
44impl<T> FunctionArgumentHolder for Option<T> {
45 const INIT: Self = None;
46}
47
48#[inline]
49pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>(
50 obj: &'py PyAny,
51 holder: &'a mut Option<PyRef<'py, T>>,
52) -> PyResult<&'a T> {
53 Ok(&*holder.insert(obj.extract()?))
54}
55
56#[inline]
57pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass<Frozen = False>>(
58 obj: &'py PyAny,
59 holder: &'a mut Option<PyRefMut<'py, T>>,
60) -> PyResult<&'a mut T> {
61 Ok(&mut *holder.insert(obj.extract()?))
62}
63
64/// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument.
65#[doc(hidden)]
66pub fn extract_argument<'a, 'py, T>(
67 obj: &'py PyAny,
68 holder: &'a mut T::Holder,
69 arg_name: &str,
70) -> PyResult<T>
71where
72 T: PyFunctionArgument<'a, 'py>,
73{
74 match PyFunctionArgument::extract(obj, holder) {
75 Ok(value: T) => Ok(value),
76 Err(e: PyErr) => Err(argument_extraction_error(obj.py(), arg_name, error:e)),
77 }
78}
79
80/// Alternative to [`extract_argument`] used for `Option<T>` arguments. This is necessary because Option<&T>
81/// does not implement `PyFunctionArgument` for `T: PyClass`.
82#[doc(hidden)]
83pub fn extract_optional_argument<'a, 'py, T>(
84 obj: Option<&'py PyAny>,
85 holder: &'a mut T::Holder,
86 arg_name: &str,
87 default: fn() -> Option<T>,
88) -> PyResult<Option<T>>
89where
90 T: PyFunctionArgument<'a, 'py>,
91{
92 match obj {
93 Some(obj: &PyAny) => {
94 if obj.is_none() {
95 // Explicit `None` will result in None being used as the function argument
96 Ok(None)
97 } else {
98 extract_argument(obj, holder, arg_name).map(op:Some)
99 }
100 }
101 _ => Ok(default()),
102 }
103}
104
105/// Alternative to [`extract_argument`] used when the argument has a default value provided by an annotation.
106#[doc(hidden)]
107pub fn extract_argument_with_default<'a, 'py, T>(
108 obj: Option<&'py PyAny>,
109 holder: &'a mut T::Holder,
110 arg_name: &str,
111 default: fn() -> T,
112) -> PyResult<T>
113where
114 T: PyFunctionArgument<'a, 'py>,
115{
116 match obj {
117 Some(obj: &PyAny) => extract_argument(obj, holder, arg_name),
118 None => Ok(default()),
119 }
120}
121
122/// Alternative to [`extract_argument`] used when the argument has a `#[pyo3(from_py_with)]` annotation.
123#[doc(hidden)]
124pub fn from_py_with<'py, T>(
125 obj: &'py PyAny,
126 arg_name: &str,
127 extractor: fn(&'py PyAny) -> PyResult<T>,
128) -> PyResult<T> {
129 match extractor(obj) {
130 Ok(value: T) => Ok(value),
131 Err(e: PyErr) => Err(argument_extraction_error(obj.py(), arg_name, error:e)),
132 }
133}
134
135/// Alternative to [`extract_argument`] used when the argument has a `#[pyo3(from_py_with)]` annotation and also a default value.
136#[doc(hidden)]
137pub fn from_py_with_with_default<'py, T>(
138 obj: Option<&'py PyAny>,
139 arg_name: &str,
140 extractor: fn(&'py PyAny) -> PyResult<T>,
141 default: fn() -> T,
142) -> PyResult<T> {
143 match obj {
144 Some(obj: &PyAny) => from_py_with(obj, arg_name, extractor),
145 None => Ok(default()),
146 }
147}
148
149/// Adds the argument name to the error message of an error which occurred during argument extraction.
150///
151/// Only modifies TypeError. (Cannot guarantee all exceptions have constructors from
152/// single string.)
153#[doc(hidden)]
154#[cold]
155pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) -> PyErr {
156 if error.get_type(py).is(py.get_type::<PyTypeError>()) {
157 let remapped_error: PyErr =
158 PyTypeError::new_err(args:format!("argument '{}': {}", arg_name, error.value(py)));
159 remapped_error.set_cause(py, error.cause(py));
160 remapped_error
161 } else {
162 error
163 }
164}
165
166/// Unwraps the Option<&PyAny> produced by the FunctionDescription `extract_arguments_` methods.
167/// They check if required methods are all provided.
168///
169/// # Safety
170/// `argument` must not be `None`
171#[doc(hidden)]
172#[inline]
173pub unsafe fn unwrap_required_argument(argument: Option<&PyAny>) -> &PyAny {
174 match argument {
175 Some(value: &PyAny) => value,
176 #[cfg(debug_assertions)]
177 None => unreachable!("required method argument was not extracted"),
178 #[cfg(not(debug_assertions))]
179 None => std::hint::unreachable_unchecked(),
180 }
181}
182
183pub struct KeywordOnlyParameterDescription {
184 pub name: &'static str,
185 pub required: bool,
186}
187
188/// Function argument specification for a `#[pyfunction]` or `#[pymethod]`.
189pub struct FunctionDescription {
190 pub cls_name: Option<&'static str>,
191 pub func_name: &'static str,
192 pub positional_parameter_names: &'static [&'static str],
193 pub positional_only_parameters: usize,
194 pub required_positional_parameters: usize,
195 pub keyword_only_parameters: &'static [KeywordOnlyParameterDescription],
196}
197
198impl FunctionDescription {
199 fn full_name(&self) -> String {
200 if let Some(cls_name) = self.cls_name {
201 format!("{}.{}()", cls_name, self.func_name)
202 } else {
203 format!("{}()", self.func_name)
204 }
205 }
206
207 /// Equivalent of `extract_arguments_tuple_dict` which uses the Python C-API "fastcall" convention.
208 ///
209 /// # Safety
210 /// - `args` must be a pointer to a C-style array of valid `ffi::PyObject` pointers, or NULL.
211 /// - `kwnames` must be a pointer to a PyTuple, or NULL.
212 /// - `nargs + kwnames.len()` is the total length of the `args` array.
213 #[cfg(not(Py_LIMITED_API))]
214 pub unsafe fn extract_arguments_fastcall<'py, V, K>(
215 &self,
216 py: Python<'py>,
217 args: *const *mut ffi::PyObject,
218 nargs: ffi::Py_ssize_t,
219 kwnames: *mut ffi::PyObject,
220 output: &mut [Option<&'py PyAny>],
221 ) -> PyResult<(V::Varargs, K::Varkeywords)>
222 where
223 V: VarargsHandler<'py>,
224 K: VarkeywordsHandler<'py>,
225 {
226 let num_positional_parameters = self.positional_parameter_names.len();
227
228 debug_assert!(nargs >= 0);
229 debug_assert!(self.positional_only_parameters <= num_positional_parameters);
230 debug_assert!(self.required_positional_parameters <= num_positional_parameters);
231 debug_assert_eq!(
232 output.len(),
233 num_positional_parameters + self.keyword_only_parameters.len()
234 );
235
236 // Handle positional arguments
237 // Safety: Option<&PyAny> has the same memory layout as `*mut ffi::PyObject`
238 let args: *const Option<&PyAny> = args.cast();
239 let positional_args_provided = nargs as usize;
240 let remaining_positional_args = if args.is_null() {
241 debug_assert_eq!(positional_args_provided, 0);
242 &[]
243 } else {
244 // Can consume at most the number of positional parameters in the function definition,
245 // the rest are varargs.
246 let positional_args_to_consume =
247 num_positional_parameters.min(positional_args_provided);
248 let (positional_parameters, remaining) =
249 std::slice::from_raw_parts(args, positional_args_provided)
250 .split_at(positional_args_to_consume);
251 output[..positional_args_to_consume].copy_from_slice(positional_parameters);
252 remaining
253 };
254 let varargs = V::handle_varargs_fastcall(py, remaining_positional_args, self)?;
255
256 // Handle keyword arguments
257 let mut varkeywords = K::Varkeywords::default();
258 if let Some(kwnames) = py.from_borrowed_ptr_or_opt::<PyTuple>(kwnames) {
259 // Safety: &PyAny has the same memory layout as `*mut ffi::PyObject`
260 let kwargs =
261 ::std::slice::from_raw_parts((args as *const &PyAny).offset(nargs), kwnames.len());
262
263 self.handle_kwargs::<K, _>(
264 kwnames.iter().zip(kwargs.iter().copied()),
265 &mut varkeywords,
266 num_positional_parameters,
267 output,
268 )?
269 }
270
271 // Once all inputs have been processed, check that all required arguments have been provided.
272
273 self.ensure_no_missing_required_positional_arguments(output, positional_args_provided)?;
274 self.ensure_no_missing_required_keyword_arguments(output)?;
275
276 Ok((varargs, varkeywords))
277 }
278
279 /// Extracts the `args` and `kwargs` provided into `output`, according to this function
280 /// definition.
281 ///
282 /// `output` must have the same length as this function has positional and keyword-only
283 /// parameters (as per the `positional_parameter_names` and `keyword_only_parameters`
284 /// respectively).
285 ///
286 /// Unexpected, duplicate or invalid arguments will cause this function to return `TypeError`.
287 ///
288 /// # Safety
289 /// - `args` must be a pointer to a PyTuple.
290 /// - `kwargs` must be a pointer to a PyDict, or NULL.
291 pub unsafe fn extract_arguments_tuple_dict<'py, V, K>(
292 &self,
293 py: Python<'py>,
294 args: *mut ffi::PyObject,
295 kwargs: *mut ffi::PyObject,
296 output: &mut [Option<&'py PyAny>],
297 ) -> PyResult<(V::Varargs, K::Varkeywords)>
298 where
299 V: VarargsHandler<'py>,
300 K: VarkeywordsHandler<'py>,
301 {
302 let args = py.from_borrowed_ptr::<PyTuple>(args);
303 let kwargs: ::std::option::Option<&PyDict> = py.from_borrowed_ptr_or_opt(kwargs);
304
305 let num_positional_parameters = self.positional_parameter_names.len();
306
307 debug_assert!(self.positional_only_parameters <= num_positional_parameters);
308 debug_assert!(self.required_positional_parameters <= num_positional_parameters);
309 debug_assert_eq!(
310 output.len(),
311 num_positional_parameters + self.keyword_only_parameters.len()
312 );
313
314 // Copy positional arguments into output
315 for (i, arg) in args.iter().take(num_positional_parameters).enumerate() {
316 output[i] = Some(arg);
317 }
318
319 // If any arguments remain, push them to varargs (if possible) or error
320 let varargs = V::handle_varargs_tuple(args, self)?;
321
322 // Handle keyword arguments
323 let mut varkeywords = K::Varkeywords::default();
324 if let Some(kwargs) = kwargs {
325 self.handle_kwargs::<K, _>(kwargs, &mut varkeywords, num_positional_parameters, output)?
326 }
327
328 // Once all inputs have been processed, check that all required arguments have been provided.
329
330 self.ensure_no_missing_required_positional_arguments(output, args.len())?;
331 self.ensure_no_missing_required_keyword_arguments(output)?;
332
333 Ok((varargs, varkeywords))
334 }
335
336 #[inline]
337 fn handle_kwargs<'py, K, I>(
338 &self,
339 kwargs: I,
340 varkeywords: &mut K::Varkeywords,
341 num_positional_parameters: usize,
342 output: &mut [Option<&'py PyAny>],
343 ) -> PyResult<()>
344 where
345 K: VarkeywordsHandler<'py>,
346 I: IntoIterator<Item = (&'py PyAny, &'py PyAny)>,
347 {
348 debug_assert_eq!(
349 num_positional_parameters,
350 self.positional_parameter_names.len()
351 );
352 debug_assert_eq!(
353 output.len(),
354 num_positional_parameters + self.keyword_only_parameters.len()
355 );
356 let mut positional_only_keyword_arguments = Vec::new();
357 for (kwarg_name_py, value) in kwargs {
358 // All keyword arguments should be UTF-8 strings, but we'll check, just in case.
359 // If it isn't, then it will be handled below as a varkeyword (which may raise an
360 // error if this function doesn't accept **kwargs). Rust source is always UTF-8
361 // and so all argument names in `#[pyfunction]` signature must be UTF-8.
362 if let Ok(kwarg_name) = kwarg_name_py.downcast::<PyString>()?.to_str() {
363 // Try to place parameter in keyword only parameters
364 if let Some(i) = self.find_keyword_parameter_in_keyword_only(kwarg_name) {
365 if output[i + num_positional_parameters]
366 .replace(value)
367 .is_some()
368 {
369 return Err(self.multiple_values_for_argument(kwarg_name));
370 }
371 continue;
372 }
373
374 // Repeat for positional parameters
375 if let Some(i) = self.find_keyword_parameter_in_positional(kwarg_name) {
376 if i < self.positional_only_parameters {
377 // If accepting **kwargs, then it's allowed for the name of the
378 // kwarg to conflict with a postional-only argument - the value
379 // will go into **kwargs anyway.
380 if K::handle_varkeyword(varkeywords, kwarg_name_py, value, self).is_err() {
381 positional_only_keyword_arguments.push(kwarg_name);
382 }
383 } else if output[i].replace(value).is_some() {
384 return Err(self.multiple_values_for_argument(kwarg_name));
385 }
386 continue;
387 }
388 };
389
390 K::handle_varkeyword(varkeywords, kwarg_name_py, value, self)?
391 }
392
393 if !positional_only_keyword_arguments.is_empty() {
394 return Err(self.positional_only_keyword_arguments(&positional_only_keyword_arguments));
395 }
396
397 Ok(())
398 }
399
400 #[inline]
401 fn find_keyword_parameter_in_positional(&self, kwarg_name: &str) -> Option<usize> {
402 self.positional_parameter_names
403 .iter()
404 .position(|&param_name| param_name == kwarg_name)
405 }
406
407 #[inline]
408 fn find_keyword_parameter_in_keyword_only(&self, kwarg_name: &str) -> Option<usize> {
409 // Compare the keyword name against each parameter in turn. This is exactly the same method
410 // which CPython uses to map keyword names. Although it's O(num_parameters), the number of
411 // parameters is expected to be small so it's not worth constructing a mapping.
412 self.keyword_only_parameters
413 .iter()
414 .position(|param_desc| param_desc.name == kwarg_name)
415 }
416
417 #[inline]
418 fn ensure_no_missing_required_positional_arguments(
419 &self,
420 output: &[Option<&PyAny>],
421 positional_args_provided: usize,
422 ) -> PyResult<()> {
423 if positional_args_provided < self.required_positional_parameters {
424 for out in &output[positional_args_provided..self.required_positional_parameters] {
425 if out.is_none() {
426 return Err(self.missing_required_positional_arguments(output));
427 }
428 }
429 }
430 Ok(())
431 }
432
433 #[inline]
434 fn ensure_no_missing_required_keyword_arguments(
435 &self,
436 output: &[Option<&PyAny>],
437 ) -> PyResult<()> {
438 let keyword_output = &output[self.positional_parameter_names.len()..];
439 for (param, out) in self.keyword_only_parameters.iter().zip(keyword_output) {
440 if param.required && out.is_none() {
441 return Err(self.missing_required_keyword_arguments(keyword_output));
442 }
443 }
444 Ok(())
445 }
446
447 #[cold]
448 fn too_many_positional_arguments(&self, args_provided: usize) -> PyErr {
449 let was = if args_provided == 1 { "was" } else { "were" };
450 let msg = if self.required_positional_parameters != self.positional_parameter_names.len() {
451 format!(
452 "{} takes from {} to {} positional arguments but {} {} given",
453 self.full_name(),
454 self.required_positional_parameters,
455 self.positional_parameter_names.len(),
456 args_provided,
457 was
458 )
459 } else {
460 format!(
461 "{} takes {} positional arguments but {} {} given",
462 self.full_name(),
463 self.positional_parameter_names.len(),
464 args_provided,
465 was
466 )
467 };
468 PyTypeError::new_err(msg)
469 }
470
471 #[cold]
472 fn multiple_values_for_argument(&self, argument: &str) -> PyErr {
473 PyTypeError::new_err(format!(
474 "{} got multiple values for argument '{}'",
475 self.full_name(),
476 argument
477 ))
478 }
479
480 #[cold]
481 fn unexpected_keyword_argument(&self, argument: &PyAny) -> PyErr {
482 PyTypeError::new_err(format!(
483 "{} got an unexpected keyword argument '{}'",
484 self.full_name(),
485 argument
486 ))
487 }
488
489 #[cold]
490 fn positional_only_keyword_arguments(&self, parameter_names: &[&str]) -> PyErr {
491 let mut msg = format!(
492 "{} got some positional-only arguments passed as keyword arguments: ",
493 self.full_name()
494 );
495 push_parameter_list(&mut msg, parameter_names);
496 PyTypeError::new_err(msg)
497 }
498
499 #[cold]
500 fn missing_required_arguments(&self, argument_type: &str, parameter_names: &[&str]) -> PyErr {
501 let arguments = if parameter_names.len() == 1 {
502 "argument"
503 } else {
504 "arguments"
505 };
506 let mut msg = format!(
507 "{} missing {} required {} {}: ",
508 self.full_name(),
509 parameter_names.len(),
510 argument_type,
511 arguments,
512 );
513 push_parameter_list(&mut msg, parameter_names);
514 PyTypeError::new_err(msg)
515 }
516
517 #[cold]
518 fn missing_required_keyword_arguments(&self, keyword_outputs: &[Option<&PyAny>]) -> PyErr {
519 debug_assert_eq!(self.keyword_only_parameters.len(), keyword_outputs.len());
520
521 let missing_keyword_only_arguments: Vec<_> = self
522 .keyword_only_parameters
523 .iter()
524 .zip(keyword_outputs)
525 .filter_map(|(keyword_desc, out)| {
526 if keyword_desc.required && out.is_none() {
527 Some(keyword_desc.name)
528 } else {
529 None
530 }
531 })
532 .collect();
533
534 debug_assert!(!missing_keyword_only_arguments.is_empty());
535 self.missing_required_arguments("keyword", &missing_keyword_only_arguments)
536 }
537
538 #[cold]
539 fn missing_required_positional_arguments(&self, output: &[Option<&PyAny>]) -> PyErr {
540 let missing_positional_arguments: Vec<_> = self
541 .positional_parameter_names
542 .iter()
543 .take(self.required_positional_parameters)
544 .zip(output)
545 .filter_map(|(param, out)| if out.is_none() { Some(*param) } else { None })
546 .collect();
547
548 debug_assert!(!missing_positional_arguments.is_empty());
549 self.missing_required_arguments("positional", &missing_positional_arguments)
550 }
551}
552
553/// A trait used to control whether to accept varargs in FunctionDescription::extract_argument_(method) functions.
554pub trait VarargsHandler<'py> {
555 type Varargs;
556 /// Called by `FunctionDescription::extract_arguments_fastcall` with any additional arguments.
557 fn handle_varargs_fastcall(
558 py: Python<'py>,
559 varargs: &[Option<&PyAny>],
560 function_description: &FunctionDescription,
561 ) -> PyResult<Self::Varargs>;
562 /// Called by `FunctionDescription::extract_arguments_tuple_dict` with the original tuple.
563 ///
564 /// Additional arguments are those in the tuple slice starting from `function_description.positional_parameter_names.len()`.
565 fn handle_varargs_tuple(
566 args: &'py PyTuple,
567 function_description: &FunctionDescription,
568 ) -> PyResult<Self::Varargs>;
569}
570
571/// Marker struct which indicates varargs are not allowed.
572pub struct NoVarargs;
573
574impl<'py> VarargsHandler<'py> for NoVarargs {
575 type Varargs = ();
576
577 #[inline]
578 fn handle_varargs_fastcall(
579 _py: Python<'py>,
580 varargs: &[Option<&PyAny>],
581 function_description: &FunctionDescription,
582 ) -> PyResult<Self::Varargs> {
583 let extra_arguments = varargs.len();
584 if extra_arguments > 0 {
585 return Err(function_description.too_many_positional_arguments(
586 function_description.positional_parameter_names.len() + extra_arguments,
587 ));
588 }
589 Ok(())
590 }
591
592 #[inline]
593 fn handle_varargs_tuple(
594 args: &'py PyTuple,
595 function_description: &FunctionDescription,
596 ) -> PyResult<Self::Varargs> {
597 let positional_parameter_count = function_description.positional_parameter_names.len();
598 let provided_args_count = args.len();
599 if provided_args_count <= positional_parameter_count {
600 Ok(())
601 } else {
602 Err(function_description.too_many_positional_arguments(provided_args_count))
603 }
604 }
605}
606
607/// Marker struct which indicates varargs should be collected into a `PyTuple`.
608pub struct TupleVarargs;
609
610impl<'py> VarargsHandler<'py> for TupleVarargs {
611 type Varargs = &'py PyTuple;
612 #[inline]
613 fn handle_varargs_fastcall(
614 py: Python<'py>,
615 varargs: &[Option<&PyAny>],
616 _function_description: &FunctionDescription,
617 ) -> PyResult<Self::Varargs> {
618 Ok(PyTuple::new(py, elements:varargs))
619 }
620
621 #[inline]
622 fn handle_varargs_tuple(
623 args: &'py PyTuple,
624 function_description: &FunctionDescription,
625 ) -> PyResult<Self::Varargs> {
626 let positional_parameters: usize = function_description.positional_parameter_names.len();
627 Ok(args.get_slice(low:positional_parameters, high:args.len()))
628 }
629}
630
631/// A trait used to control whether to accept varkeywords in FunctionDescription::extract_argument_(method) functions.
632pub trait VarkeywordsHandler<'py> {
633 type Varkeywords: Default;
634 fn handle_varkeyword(
635 varkeywords: &mut Self::Varkeywords,
636 name: &'py PyAny,
637 value: &'py PyAny,
638 function_description: &FunctionDescription,
639 ) -> PyResult<()>;
640}
641
642/// Marker struct which indicates unknown keywords are not permitted.
643pub struct NoVarkeywords;
644
645impl<'py> VarkeywordsHandler<'py> for NoVarkeywords {
646 type Varkeywords = ();
647 #[inline]
648 fn handle_varkeyword(
649 _varkeywords: &mut Self::Varkeywords,
650 name: &'py PyAny,
651 _value: &'py PyAny,
652 function_description: &FunctionDescription,
653 ) -> PyResult<()> {
654 Err(function_description.unexpected_keyword_argument(name))
655 }
656}
657
658/// Marker struct which indicates unknown keywords should be collected into a `PyDict`.
659pub struct DictVarkeywords;
660
661impl<'py> VarkeywordsHandler<'py> for DictVarkeywords {
662 type Varkeywords = Option<&'py PyDict>;
663 #[inline]
664 fn handle_varkeyword(
665 varkeywords: &mut Self::Varkeywords,
666 name: &'py PyAny,
667 value: &'py PyAny,
668 _function_description: &FunctionDescription,
669 ) -> PyResult<()> {
670 varkeywords
671 .get_or_insert_with(|| PyDict::new(name.py()))
672 .set_item(key:name, value)
673 }
674}
675
676fn push_parameter_list(msg: &mut String, parameter_names: &[&str]) {
677 for (i: usize, parameter: &&str) in parameter_names.iter().enumerate() {
678 if i != 0 {
679 if parameter_names.len() > 2 {
680 msg.push(ch:',');
681 }
682
683 if i == parameter_names.len() - 1 {
684 msg.push_str(string:" and ")
685 } else {
686 msg.push(ch:' ')
687 }
688 }
689
690 msg.push(ch:'\'');
691 msg.push_str(string:parameter);
692 msg.push(ch:'\'');
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use crate::{
699 types::{IntoPyDict, PyTuple},
700 PyAny, Python, ToPyObject,
701 };
702
703 use super::{push_parameter_list, FunctionDescription, NoVarargs, NoVarkeywords};
704
705 #[test]
706 fn unexpected_keyword_argument() {
707 let function_description = FunctionDescription {
708 cls_name: None,
709 func_name: "example",
710 positional_parameter_names: &[],
711 positional_only_parameters: 0,
712 required_positional_parameters: 0,
713 keyword_only_parameters: &[],
714 };
715
716 Python::with_gil(|py| {
717 let args = PyTuple::new(py, Vec::<&PyAny>::new());
718 let kwargs = [("foo".to_object(py).into_ref(py), 0u8)].into_py_dict(py);
719 let err = unsafe {
720 function_description
721 .extract_arguments_tuple_dict::<NoVarargs, NoVarkeywords>(
722 py,
723 args.as_ptr(),
724 kwargs.as_ptr(),
725 &mut [],
726 )
727 .unwrap_err()
728 };
729 assert_eq!(
730 err.to_string(),
731 "TypeError: example() got an unexpected keyword argument 'foo'"
732 );
733 })
734 }
735
736 #[test]
737 fn keyword_not_string() {
738 let function_description = FunctionDescription {
739 cls_name: None,
740 func_name: "example",
741 positional_parameter_names: &[],
742 positional_only_parameters: 0,
743 required_positional_parameters: 0,
744 keyword_only_parameters: &[],
745 };
746
747 Python::with_gil(|py| {
748 let args = PyTuple::new(py, Vec::<&PyAny>::new());
749 let kwargs = [(1u8.to_object(py).into_ref(py), 1u8)].into_py_dict(py);
750 let err = unsafe {
751 function_description
752 .extract_arguments_tuple_dict::<NoVarargs, NoVarkeywords>(
753 py,
754 args.as_ptr(),
755 kwargs.as_ptr(),
756 &mut [],
757 )
758 .unwrap_err()
759 };
760 assert_eq!(
761 err.to_string(),
762 "TypeError: 'int' object cannot be converted to 'PyString'"
763 );
764 })
765 }
766
767 #[test]
768 fn missing_required_arguments() {
769 let function_description = FunctionDescription {
770 cls_name: None,
771 func_name: "example",
772 positional_parameter_names: &["foo", "bar"],
773 positional_only_parameters: 0,
774 required_positional_parameters: 2,
775 keyword_only_parameters: &[],
776 };
777
778 Python::with_gil(|py| {
779 let args = PyTuple::new(py, Vec::<&PyAny>::new());
780 let mut output = [None, None];
781 let err = unsafe {
782 function_description.extract_arguments_tuple_dict::<NoVarargs, NoVarkeywords>(
783 py,
784 args.as_ptr(),
785 std::ptr::null_mut(),
786 &mut output,
787 )
788 }
789 .unwrap_err();
790 assert_eq!(
791 err.to_string(),
792 "TypeError: example() missing 2 required positional arguments: 'foo' and 'bar'"
793 );
794 })
795 }
796
797 #[test]
798 fn push_parameter_list_empty() {
799 let mut s = String::new();
800 push_parameter_list(&mut s, &[]);
801 assert_eq!(&s, "");
802 }
803
804 #[test]
805 fn push_parameter_list_one() {
806 let mut s = String::new();
807 push_parameter_list(&mut s, &["a"]);
808 assert_eq!(&s, "'a'");
809 }
810
811 #[test]
812 fn push_parameter_list_two() {
813 let mut s = String::new();
814 push_parameter_list(&mut s, &["a", "b"]);
815 assert_eq!(&s, "'a' and 'b'");
816 }
817
818 #[test]
819 fn push_parameter_list_three() {
820 let mut s = String::new();
821 push_parameter_list(&mut s, &["a", "b", "c"]);
822 assert_eq!(&s, "'a', 'b', and 'c'");
823 }
824
825 #[test]
826 fn push_parameter_list_four() {
827 let mut s = String::new();
828 push_parameter_list(&mut s, &["a", "b", "c", "d"]);
829 assert_eq!(&s, "'a', 'b', 'c', and 'd'");
830 }
831}
832