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