pyo3/impl_/
extract_argument.rs

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