Merge pull request #1971 from davidhewitt/disable-unwanted-protos

pymethods: test and document opt-out of protos
This commit is contained in:
David Hewitt 2021-11-11 08:52:58 +00:00 committed by GitHub
commit 2784b31bea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 234 additions and 47 deletions

View File

@ -6,6 +6,16 @@ PyO3 versions, please see the [migration guide](https://pyo3.rs/latest/migration
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- `#[classattr]` constants with a known magic method name (which is lowercase) no longer trigger lint warnings expecting constants to be uppercase. [#1969](https://github.com/PyO3/pyo3/pull/1969)
### Fixed
- Fix creating `#[classattr]` by functions with the name of a known magic method. [#1969](https://github.com/PyO3/pyo3/pull/1969)
## [0.15.0] - 2021-11-03
### Packaging

View File

@ -47,6 +47,25 @@ given signatures should be interpreted as follows:
- `__str__(<self>) -> object (str)`
- `__repr__(<self>) -> object (str)`
- `__hash__(<self>) -> isize`
<details>
<summary>Disabling Python's default hash</summary>
By default, all `#[pyclass]` types have a default hash implementation from Python. Types which should not be hashable can override this by setting `__hash__` to `None`. This is the same mechanism as for a pure-Python class. This is done like so:
```rust
# use pyo3::prelude::*;
#
#[pyclass]
struct NotHashable { }
#[pymethods]
impl NotHashable {
#[classattr]
const __hash__: Option<PyObject> = None;
}
```
</details>
- `__richcmp__(<self>, object, pyo3::basic::CompareOp) -> object`
- `__getattr__(<self>, object) -> object`
- `__setattr__(<self>, object, object) -> ()`
@ -140,6 +159,24 @@ TODO; see [#1884](https://github.com/PyO3/pyo3/issues/1884)
- `__len__(<self>) -> usize`
- `__contains__(<self>, object) -> bool`
<details>
<summary>Disabling Python's default contains</summary>
By default, all `#[pyclass]` types with an `__iter__` method support a default implementation of the `in` operator. Types which do not want this can override this by setting `__contains__` to `None`. This is the same mechanism as for a pure-Python class. This is done like so:
```rust
# use pyo3::prelude::*;
#
#[pyclass]
struct NoContains { }
#[pymethods]
impl NoContains {
#[classattr]
const __contains__: Option<PyObject> = None;
}
```
</details>
- `__getitem__(<self>, object) -> object`
- `__setitem__(<self>, object, object) -> ()`
- `__delitem__(<self>, object) -> ()`

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use crate::{
attributes::{
self, get_deprecated_name_attribute, get_pyo3_options, is_attribute_ident, take_attributes,
@ -5,7 +7,7 @@ use crate::{
},
deprecations::Deprecations,
};
use proc_macro2::TokenStream;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{
ext::IdentExt,
@ -20,15 +22,18 @@ pub struct ConstSpec {
}
impl ConstSpec {
pub fn python_name(&self) -> Cow<Ident> {
if let Some(name) = &self.attributes.name {
Cow::Borrowed(&name.0)
} else {
Cow::Owned(self.rust_ident.unraw())
}
}
/// Null-terminated Python name
pub fn null_terminated_python_name(&self) -> TokenStream {
if let Some(name) = &self.attributes.name {
let name = format!("{}\0", name.0);
quote!({#name})
} else {
let name = format!("{}\0", self.rust_ident.unraw().to_string());
quote!(#name)
}
let name = format!("{}\0", self.python_name());
quote!({#name})
}
}

View File

@ -5,7 +5,7 @@ use std::collections::HashSet;
use crate::{
konst::{ConstAttributes, ConstSpec},
pyfunction::PyFunctionOptions,
pymethod,
pymethod::{self, is_proto_method},
};
use proc_macro2::TokenStream;
use pymethod::GeneratedPyMethod;
@ -79,6 +79,13 @@ pub fn impl_methods(
let attrs = get_cfg_attributes(&konst.attrs);
let meth = gen_py_const(ty, &spec);
methods.push(quote!(#(#attrs)* #meth));
if is_proto_method(&spec.python_name().to_string()) {
// If this is a known protocol method e.g. __contains__, then allow this
// symbol even though it's not an uppercase constant.
konst
.attrs
.push(syn::parse_quote!(#[allow(non_upper_case_globals)]));
}
}
}
_ => (),

View File

@ -24,6 +24,63 @@ pub enum GeneratedPyMethod {
SlotTraitImpl(String, TokenStream),
}
pub struct PyMethod<'a> {
kind: PyMethodKind,
method_name: String,
spec: FnSpec<'a>,
}
enum PyMethodKind {
Fn,
Proto(PyMethodProtoKind),
}
impl PyMethodKind {
fn from_name(name: &str) -> Self {
if let Some(slot_def) = pyproto(name) {
PyMethodKind::Proto(PyMethodProtoKind::Slot(slot_def))
} else if name == "__call__" {
PyMethodKind::Proto(PyMethodProtoKind::Call)
} else if let Some(slot_fragment_def) = pyproto_fragment(name) {
PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(slot_fragment_def))
} else {
PyMethodKind::Fn
}
}
}
enum PyMethodProtoKind {
Slot(&'static SlotDef),
Call,
SlotFragment(&'static SlotFragmentDef),
}
impl<'a> PyMethod<'a> {
fn parse(
sig: &'a mut syn::Signature,
meth_attrs: &mut Vec<syn::Attribute>,
options: PyFunctionOptions,
) -> Result<Self> {
let spec = FnSpec::parse(sig, meth_attrs, options)?;
let method_name = spec.python_name.to_string();
let kind = PyMethodKind::from_name(&method_name);
Ok(Self {
kind,
method_name,
spec,
})
}
}
pub fn is_proto_method(name: &str) -> bool {
match PyMethodKind::from_name(name) {
PyMethodKind::Fn => false,
PyMethodKind::Proto(_) => true,
}
}
pub fn gen_py_method(
cls: &syn::Type,
sig: &mut syn::Signature,
@ -33,56 +90,55 @@ pub fn gen_py_method(
check_generic(sig)?;
ensure_not_async_fn(sig)?;
ensure_function_options_valid(&options)?;
let spec = FnSpec::parse(sig, &mut *meth_attrs, options)?;
let method = PyMethod::parse(sig, &mut *meth_attrs, options)?;
let spec = &method.spec;
let method_name = spec.python_name.to_string();
if let Some(slot_def) = pyproto(&method_name) {
ensure_no_forbidden_protocol_attributes(&spec, &method_name)?;
let slot = slot_def.generate_type_slot(cls, &spec)?;
return Ok(GeneratedPyMethod::Proto(slot));
} else if method_name == "__call__" {
ensure_no_forbidden_protocol_attributes(&spec, &method_name)?;
return Ok(GeneratedPyMethod::Proto(impl_call_slot(cls, spec)?));
}
if let Some(slot_fragment_def) = pyproto_fragment(&method_name) {
ensure_no_forbidden_protocol_attributes(&spec, &method_name)?;
let proto = slot_fragment_def.generate_pyproto_fragment(cls, &spec)?;
return Ok(GeneratedPyMethod::SlotTraitImpl(method_name, proto));
}
Ok(match &spec.tp {
Ok(match (method.kind, &spec.tp) {
// Class attributes go before protos so that class attributes can be used to set proto
// method to None.
(_, FnType::ClassAttribute) => {
GeneratedPyMethod::Method(impl_py_class_attribute(cls, spec))
}
(PyMethodKind::Proto(proto_kind), _) => {
ensure_no_forbidden_protocol_attributes(spec, &method.method_name)?;
match proto_kind {
PyMethodProtoKind::Slot(slot_def) => {
let slot = slot_def.generate_type_slot(cls, spec)?;
GeneratedPyMethod::Proto(slot)
}
PyMethodProtoKind::Call => {
GeneratedPyMethod::Proto(impl_call_slot(cls, method.spec)?)
}
PyMethodProtoKind::SlotFragment(slot_fragment_def) => {
let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec)?;
GeneratedPyMethod::SlotTraitImpl(method.method_name, proto)
}
}
}
// ordinary functions (with some specialties)
FnType::Fn(_) => GeneratedPyMethod::Method(impl_py_method_def(cls, &spec, None)?),
FnType::FnClass => GeneratedPyMethod::Method(impl_py_method_def(
(_, FnType::Fn(_)) => GeneratedPyMethod::Method(impl_py_method_def(cls, spec, None)?),
(_, FnType::FnClass) => GeneratedPyMethod::Method(impl_py_method_def(
cls,
&spec,
spec,
Some(quote!(::pyo3::ffi::METH_CLASS)),
)?),
FnType::FnStatic => GeneratedPyMethod::Method(impl_py_method_def(
(_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def(
cls,
&spec,
spec,
Some(quote!(::pyo3::ffi::METH_STATIC)),
)?),
// special prototypes
FnType::FnNew => GeneratedPyMethod::TraitImpl(impl_py_method_def_new(cls, &spec)?),
FnType::ClassAttribute => GeneratedPyMethod::Method(impl_py_class_attribute(cls, &spec)),
FnType::Getter(self_type) => GeneratedPyMethod::Method(impl_py_getter_def(
(_, FnType::FnNew) => GeneratedPyMethod::TraitImpl(impl_py_method_def_new(cls, spec)?),
(_, FnType::Getter(self_type)) => GeneratedPyMethod::Method(impl_py_getter_def(
cls,
PropertyType::Function {
self_type,
spec: &spec,
},
PropertyType::Function { self_type, spec },
)?),
FnType::Setter(self_type) => GeneratedPyMethod::Method(impl_py_setter_def(
(_, FnType::Setter(self_type)) => GeneratedPyMethod::Method(impl_py_setter_def(
cls,
PropertyType::Function {
self_type,
spec: &spec,
},
PropertyType::Function { self_type, spec },
)?),
FnType::FnModule => {
(_, FnType::FnModule) => {
unreachable!("methods cannot be FnModule")
}
})

View File

@ -1,11 +1,14 @@
use pyo3::exceptions::PyValueError;
use pyo3::types::{PySlice, PyType};
use pyo3::types::{PyList, PySlice, PyType};
use pyo3::{exceptions::PyAttributeError, prelude::*};
use pyo3::{ffi, py_run, AsPyPointer, PyCell};
use std::{isize, iter};
mod common;
#[pyclass]
struct EmptyClass;
#[pyclass]
struct ExampleClass {
#[pyo3(get, set)]
@ -560,3 +563,72 @@ assert c.counter.count == 1
.map_err(|e| e.print(py))
.unwrap();
}
#[pyclass]
struct NotHashable;
#[pymethods]
impl NotHashable {
#[classattr]
const __hash__: Option<PyObject> = None;
}
#[test]
fn test_hash_opt_out() {
// By default Python provides a hash implementation, which can be disabled by setting __hash__
// to None.
Python::with_gil(|py| {
let empty = Py::new(py, EmptyClass).unwrap();
py_assert!(py, empty, "hash(empty) is not None");
let not_hashable = Py::new(py, NotHashable).unwrap();
py_expect_exception!(py, not_hashable, "hash(not_hashable)", PyTypeError);
})
}
/// Class with __iter__ gets default contains from CPython.
#[pyclass]
struct DefaultedContains;
#[pymethods]
impl DefaultedContains {
fn __iter__(&self, py: Python) -> PyObject {
PyList::new(py, &["a", "b", "c"])
.as_ref()
.iter()
.unwrap()
.into()
}
}
#[pyclass]
struct NoContains;
#[pymethods]
impl NoContains {
fn __iter__(&self, py: Python) -> PyObject {
PyList::new(py, &["a", "b", "c"])
.as_ref()
.iter()
.unwrap()
.into()
}
// Equivalent to the opt-out const form in NotHashable above, just more verbose, to confirm this
// also works.
#[classattr]
fn __contains__() -> Option<PyObject> {
None
}
}
#[test]
fn test_contains_opt_out() {
Python::with_gil(|py| {
let defaulted_contains = Py::new(py, DefaultedContains).unwrap();
py_assert!(py, defaulted_contains, "'a' in defaulted_contains");
let no_contains = Py::new(py, NoContains).unwrap();
py_expect_exception!(py, no_contains, "'a' in no_contains", PyTypeError);
})
}