opt: make argument extraction code smaller
This commit is contained in:
parent
ebed1db5aa
commit
90479ddae4
|
@ -41,8 +41,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
accompanies your error type in your crate's documentation.
|
accompanies your error type in your crate's documentation.
|
||||||
- Improve performance and error messages for `#[derive(FromPyObject)]` for enums. [#2068](https://github.com/PyO3/pyo3/pull/2068)
|
- Improve performance and error messages for `#[derive(FromPyObject)]` for enums. [#2068](https://github.com/PyO3/pyo3/pull/2068)
|
||||||
- Reduce generated LLVM code size (to improve compile times) for:
|
- Reduce generated LLVM code size (to improve compile times) for:
|
||||||
- internal `handle_panic` helper [#2073](https://github.com/PyO3/pyo3/pull/2073)
|
- internal `handle_panic` helper [#2074](https://github.com/PyO3/pyo3/pull/2074)
|
||||||
- `#[pyclass]` type object creation [#2075](https://github.com/PyO3/pyo3/pull/2075)
|
- `#[pyfunction]` and `#[pymethods]` argument extraction [#2075](https://github.com/PyO3/pyo3/pull/2075)
|
||||||
|
- `#[pyclass]` type object creation [#2076](https://github.com/PyO3/pyo3/pull/2076)
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|
|
@ -213,8 +213,9 @@ fn impl_arg_param(
|
||||||
|
|
||||||
let ty = arg.ty;
|
let ty = arg.ty;
|
||||||
let name = arg.name;
|
let name = arg.name;
|
||||||
|
let name_str = name.to_string();
|
||||||
let transform_error = quote! {
|
let transform_error = quote! {
|
||||||
|e| _pyo3::derive_utils::argument_extraction_error(#py, stringify!(#name), e)
|
|e| _pyo3::impl_::extract_argument::argument_extraction_error(#py, #name_str, e)
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_args(&spec.attrs, name) {
|
if is_args(&spec.attrs, name) {
|
||||||
|
@ -223,7 +224,7 @@ fn impl_arg_param(
|
||||||
arg.name.span() => "args cannot be optional"
|
arg.name.span() => "args cannot be optional"
|
||||||
);
|
);
|
||||||
return Ok(quote_arg_span! {
|
return Ok(quote_arg_span! {
|
||||||
let #arg_name = _args.unwrap().extract().map_err(#transform_error)?;
|
let #arg_name = _pyo3::impl_::extract_argument::extract_argument(_args.unwrap(), #name_str)?;
|
||||||
});
|
});
|
||||||
} else if is_kwargs(&spec.attrs, name) {
|
} else if is_kwargs(&spec.attrs, name) {
|
||||||
ensure_spanned!(
|
ensure_spanned!(
|
||||||
|
@ -231,9 +232,8 @@ fn impl_arg_param(
|
||||||
arg.name.span() => "kwargs must be Option<_>"
|
arg.name.span() => "kwargs must be Option<_>"
|
||||||
);
|
);
|
||||||
return Ok(quote_arg_span! {
|
return Ok(quote_arg_span! {
|
||||||
let #arg_name = _kwargs.map(|kwargs| kwargs.extract())
|
let #arg_name = _kwargs.map(|kwargs| _pyo3::impl_::extract_argument::extract_argument(kwargs, #name_str))
|
||||||
.transpose()
|
.transpose()?;
|
||||||
.map_err(#transform_error)?;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +243,7 @@ fn impl_arg_param(
|
||||||
let extract = if let Some(FromPyWithAttribute(expr_path)) = &arg.attrs.from_py_with {
|
let extract = if let Some(FromPyWithAttribute(expr_path)) = &arg.attrs.from_py_with {
|
||||||
quote_arg_span! { #expr_path(_obj).map_err(#transform_error) }
|
quote_arg_span! { #expr_path(_obj).map_err(#transform_error) }
|
||||||
} else {
|
} else {
|
||||||
quote_arg_span! { _obj.extract().map_err(#transform_error) }
|
quote_arg_span! { _pyo3::impl_::extract_argument::extract_argument(_obj, #name_str) }
|
||||||
};
|
};
|
||||||
|
|
||||||
let arg_value_or_default = match (spec.default_value(name), arg.optional.is_some()) {
|
let arg_value_or_default = match (spec.default_value(name), arg.optional.is_some()) {
|
||||||
|
|
|
@ -3,3 +3,6 @@ usedevelop = True
|
||||||
description = Run the unit tests under {basepython}
|
description = Run the unit tests under {basepython}
|
||||||
deps = -rrequirements-dev.txt
|
deps = -rrequirements-dev.txt
|
||||||
commands = pytest --benchmark-sort=name {posargs}
|
commands = pytest --benchmark-sort=name {posargs}
|
||||||
|
# Use recreate so that tox always rebuilds, otherwise changes to Rust are not
|
||||||
|
# picked up.
|
||||||
|
recreate = True
|
||||||
|
|
|
@ -102,9 +102,12 @@ impl FunctionDescription {
|
||||||
varkeywords
|
varkeywords
|
||||||
}
|
}
|
||||||
(Some(kwargs), false) => {
|
(Some(kwargs), false) => {
|
||||||
self.extract_keyword_arguments(kwargs, output, |name, _| {
|
self.extract_keyword_arguments(
|
||||||
Err(self.unexpected_keyword_argument(name))
|
kwargs,
|
||||||
})?;
|
output,
|
||||||
|
#[cold]
|
||||||
|
|name, _| Err(self.unexpected_keyword_argument(name)),
|
||||||
|
)?;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(None, _) => None,
|
(None, _) => None,
|
||||||
|
@ -112,58 +115,39 @@ impl FunctionDescription {
|
||||||
|
|
||||||
// Check that there's sufficient positional arguments once keyword arguments are specified
|
// Check that there's sufficient positional arguments once keyword arguments are specified
|
||||||
if args_provided < self.required_positional_parameters {
|
if args_provided < self.required_positional_parameters {
|
||||||
let missing_positional_arguments: Vec<_> = self
|
for out in &output[..self.required_positional_parameters] {
|
||||||
.positional_parameter_names
|
if out.is_none() {
|
||||||
.iter()
|
return Err(self.missing_required_positional_arguments(output));
|
||||||
.take(self.required_positional_parameters)
|
}
|
||||||
.zip(output.iter())
|
|
||||||
.filter_map(|(param, out)| if out.is_none() { Some(*param) } else { None })
|
|
||||||
.collect();
|
|
||||||
if !missing_positional_arguments.is_empty() {
|
|
||||||
return Err(
|
|
||||||
self.missing_required_arguments("positional", &missing_positional_arguments)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check no missing required keyword arguments
|
// Check no missing required keyword arguments
|
||||||
let missing_keyword_only_arguments: Vec<_> = self
|
let keyword_output = &output[num_positional_parameters..];
|
||||||
.keyword_only_parameters
|
for (param, out) in self.keyword_only_parameters.iter().zip(keyword_output) {
|
||||||
.iter()
|
if param.required && out.is_none() {
|
||||||
.zip(&output[num_positional_parameters..])
|
return Err(self.missing_required_keyword_arguments(keyword_output));
|
||||||
.filter_map(|(keyword_desc, out)| {
|
}
|
||||||
if keyword_desc.required && out.is_none() {
|
|
||||||
Some(keyword_desc.name)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !missing_keyword_only_arguments.is_empty() {
|
|
||||||
return Err(self.missing_required_arguments("keyword", &missing_keyword_only_arguments));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((varargs, varkeywords))
|
Ok((varargs, varkeywords))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn extract_keyword_arguments<'p>(
|
fn extract_keyword_arguments<'p>(
|
||||||
&self,
|
&self,
|
||||||
kwargs: impl Iterator<Item = (&'p PyAny, &'p PyAny)>,
|
kwargs: impl Iterator<Item = (&'p PyAny, &'p PyAny)>,
|
||||||
output: &mut [Option<&'p PyAny>],
|
output: &mut [Option<&'p PyAny>],
|
||||||
mut unexpected_keyword_handler: impl FnMut(&'p PyAny, &'p PyAny) -> PyResult<()>,
|
mut unexpected_keyword_handler: impl FnMut(&'p PyAny, &'p PyAny) -> PyResult<()>,
|
||||||
) -> PyResult<()> {
|
) -> PyResult<()> {
|
||||||
let (args_output, kwargs_output) =
|
let positional_args_count = self.positional_parameter_names.len();
|
||||||
output.split_at_mut(self.positional_parameter_names.len());
|
|
||||||
let mut positional_only_keyword_arguments = Vec::new();
|
let mut positional_only_keyword_arguments = Vec::new();
|
||||||
for (kwarg_name, value) in kwargs {
|
'for_each_kwarg: for (kwarg_name_py, value) in kwargs {
|
||||||
let utf8_string = match kwarg_name.downcast::<PyString>()?.to_str() {
|
let kwarg_name = match kwarg_name_py.downcast::<PyString>()?.to_str() {
|
||||||
Ok(utf8_string) => utf8_string,
|
Ok(kwarg_name) => kwarg_name,
|
||||||
// This keyword is not a UTF8 string: all PyO3 argument names are guaranteed to be
|
// This keyword is not a UTF8 string: all PyO3 argument names are guaranteed to be
|
||||||
// UTF8 by construction.
|
// UTF8 by construction.
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
unexpected_keyword_handler(kwarg_name, value)?;
|
unexpected_keyword_handler(kwarg_name_py, value)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -171,31 +155,24 @@ impl FunctionDescription {
|
||||||
// Compare the keyword name against each parameter in turn. This is exactly the same method
|
// Compare the keyword name against each parameter in turn. This is exactly the same method
|
||||||
// which CPython uses to map keyword names. Although it's O(num_parameters), the number of
|
// which CPython uses to map keyword names. Although it's O(num_parameters), the number of
|
||||||
// parameters is expected to be small so it's not worth constructing a mapping.
|
// parameters is expected to be small so it's not worth constructing a mapping.
|
||||||
if let Some(i) = self
|
for (i, param) in self.keyword_only_parameters.iter().enumerate() {
|
||||||
.keyword_only_parameters
|
if param.name == kwarg_name {
|
||||||
.iter()
|
output[positional_args_count + i] = Some(value);
|
||||||
.position(|param| utf8_string == param.name)
|
continue 'for_each_kwarg;
|
||||||
{
|
}
|
||||||
kwargs_output[i] = Some(value);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repeat for positional parameters
|
// Repeat for positional parameters
|
||||||
if let Some((i, param)) = self
|
if let Some(i) = self.find_keyword_parameter_in_positionals(kwarg_name) {
|
||||||
.positional_parameter_names
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|&(_, param)| utf8_string == *param)
|
|
||||||
{
|
|
||||||
if i < self.positional_only_parameters {
|
if i < self.positional_only_parameters {
|
||||||
positional_only_keyword_arguments.push(*param);
|
positional_only_keyword_arguments.push(kwarg_name);
|
||||||
} else if args_output[i].replace(value).is_some() {
|
} else if output[i].replace(value).is_some() {
|
||||||
return Err(self.multiple_values_for_argument(param));
|
return Err(self.multiple_values_for_argument(kwarg_name));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
unexpected_keyword_handler(kwarg_name, value)?;
|
unexpected_keyword_handler(kwarg_name_py, value)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if positional_only_keyword_arguments.is_empty() {
|
if positional_only_keyword_arguments.is_empty() {
|
||||||
|
@ -205,6 +182,16 @@ impl FunctionDescription {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_keyword_parameter_in_positionals(&self, kwarg_name: &str) -> Option<usize> {
|
||||||
|
for (i, param_name) in self.positional_parameter_names.iter().enumerate() {
|
||||||
|
if *param_name == kwarg_name {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
fn too_many_positional_arguments(&self, args_provided: usize) -> PyErr {
|
fn too_many_positional_arguments(&self, args_provided: usize) -> PyErr {
|
||||||
let was = if args_provided == 1 { "was" } else { "were" };
|
let was = if args_provided == 1 { "was" } else { "were" };
|
||||||
let msg = if self.required_positional_parameters != self.positional_parameter_names.len() {
|
let msg = if self.required_positional_parameters != self.positional_parameter_names.len() {
|
||||||
|
@ -228,6 +215,7 @@ impl FunctionDescription {
|
||||||
PyTypeError::new_err(msg)
|
PyTypeError::new_err(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
fn multiple_values_for_argument(&self, argument: &str) -> PyErr {
|
fn multiple_values_for_argument(&self, argument: &str) -> PyErr {
|
||||||
PyTypeError::new_err(format!(
|
PyTypeError::new_err(format!(
|
||||||
"{} got multiple values for argument '{}'",
|
"{} got multiple values for argument '{}'",
|
||||||
|
@ -236,6 +224,7 @@ impl FunctionDescription {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
fn unexpected_keyword_argument(&self, argument: &PyAny) -> PyErr {
|
fn unexpected_keyword_argument(&self, argument: &PyAny) -> PyErr {
|
||||||
PyTypeError::new_err(format!(
|
PyTypeError::new_err(format!(
|
||||||
"{} got an unexpected keyword argument '{}'",
|
"{} got an unexpected keyword argument '{}'",
|
||||||
|
@ -244,6 +233,7 @@ impl FunctionDescription {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
fn positional_only_keyword_arguments(&self, parameter_names: &[&str]) -> PyErr {
|
fn positional_only_keyword_arguments(&self, parameter_names: &[&str]) -> PyErr {
|
||||||
let mut msg = format!(
|
let mut msg = format!(
|
||||||
"{} got some positional-only arguments passed as keyword arguments: ",
|
"{} got some positional-only arguments passed as keyword arguments: ",
|
||||||
|
@ -253,6 +243,7 @@ impl FunctionDescription {
|
||||||
PyTypeError::new_err(msg)
|
PyTypeError::new_err(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
fn missing_required_arguments(&self, argument_type: &str, parameter_names: &[&str]) -> PyErr {
|
fn missing_required_arguments(&self, argument_type: &str, parameter_names: &[&str]) -> PyErr {
|
||||||
let arguments = if parameter_names.len() == 1 {
|
let arguments = if parameter_names.len() == 1 {
|
||||||
"argument"
|
"argument"
|
||||||
|
@ -269,18 +260,40 @@ impl FunctionDescription {
|
||||||
push_parameter_list(&mut msg, parameter_names);
|
push_parameter_list(&mut msg, parameter_names);
|
||||||
PyTypeError::new_err(msg)
|
PyTypeError::new_err(msg)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Add the argument name to the error message of an error which occurred during argument extraction
|
#[cold]
|
||||||
pub fn argument_extraction_error(py: Python, arg_name: &str, error: PyErr) -> PyErr {
|
fn missing_required_keyword_arguments(&self, keyword_outputs: &[Option<&PyAny>]) -> PyErr {
|
||||||
if error.is_instance_of::<PyTypeError>(py) {
|
debug_assert_eq!(self.keyword_only_parameters.len(), keyword_outputs.len());
|
||||||
let reason = error
|
|
||||||
.value(py)
|
let missing_keyword_only_arguments: Vec<_> = self
|
||||||
.str()
|
.keyword_only_parameters
|
||||||
.unwrap_or_else(|_| PyString::new(py, ""));
|
.iter()
|
||||||
PyTypeError::new_err(format!("argument '{}': {}", arg_name, reason))
|
.zip(keyword_outputs)
|
||||||
} else {
|
.filter_map(|(keyword_desc, out)| {
|
||||||
error
|
if keyword_desc.required && out.is_none() {
|
||||||
|
Some(keyword_desc.name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
debug_assert!(!missing_keyword_only_arguments.is_empty());
|
||||||
|
self.missing_required_arguments("keyword", &missing_keyword_only_arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
fn missing_required_positional_arguments(&self, output: &[Option<&PyAny>]) -> PyErr {
|
||||||
|
let missing_positional_arguments: Vec<_> = self
|
||||||
|
.positional_parameter_names
|
||||||
|
.iter()
|
||||||
|
.take(self.required_positional_parameters)
|
||||||
|
.zip(output)
|
||||||
|
.filter_map(|(param, out)| if out.is_none() { Some(*param) } else { None })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
debug_assert!(!missing_positional_arguments.is_empty());
|
||||||
|
self.missing_required_arguments("positional", &missing_positional_arguments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
//! breaking semver guarantees.
|
//! breaking semver guarantees.
|
||||||
|
|
||||||
pub mod deprecations;
|
pub mod deprecations;
|
||||||
|
pub mod extract_argument;
|
||||||
pub mod freelist;
|
pub mod freelist;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod frompyobject;
|
pub mod frompyobject;
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
use crate::{
|
||||||
|
exceptions::PyTypeError, type_object::PyTypeObject, FromPyObject, PyAny, PyErr, PyResult,
|
||||||
|
Python,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[inline]
|
||||||
|
pub fn extract_argument<'py, T>(obj: &'py PyAny, arg_name: &str) -> PyResult<T>
|
||||||
|
where
|
||||||
|
T: FromPyObject<'py>,
|
||||||
|
{
|
||||||
|
match obj.extract() {
|
||||||
|
Ok(e) => Ok(e),
|
||||||
|
Err(e) => Err(argument_extraction_error(obj.py(), arg_name, e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the argument name to the error message of an error which occurred during argument extraction.
|
||||||
|
///
|
||||||
|
/// Only modifies TypeError. (Cannot guarantee all exceptions have constructors from
|
||||||
|
/// single string.)
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[cold]
|
||||||
|
pub fn argument_extraction_error(py: Python, arg_name: &str, error: PyErr) -> PyErr {
|
||||||
|
if error.get_type(py) == PyTypeError::type_object(py) {
|
||||||
|
PyTypeError::new_err(format!("argument '{}': {}", arg_name, error.value(py)))
|
||||||
|
} else {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue