pyo3/impl_/
extract_argument.rs

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