opt: make argument extraction code smaller

This commit is contained in:
David Hewitt 2021-12-23 23:33:51 +00:00
parent ebed1db5aa
commit 90479ddae4
6 changed files with 120 additions and 72 deletions

View File

@ -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

View File

@ -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()) {

View File

@ -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

View File

@ -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)
} }
} }

View File

@ -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;

View File

@ -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
}
}