Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

pymethods: support gc protocol #2159

Merged
merged 2 commits into from Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `wrap_pyfunction!` can now wrap a `#[pyfunction]` which is implemented in a different Rust module or crate. [#2091](https://github.com/PyO3/pyo3/pull/2091)
- Add `PyAny::contains` method (`in` operator for `PyAny`). [#2115](https://github.com/PyO3/pyo3/pull/2115)
- Add `PyMapping::contains` method (`in` operator for `PyMapping`). [#2133](https://github.com/PyO3/pyo3/pull/2133)
- Add garbage collection magic methods `__traverse__` and `__clear__` to `#[pymethods]`. [#2159](https://github.com/PyO3/pyo3/pull/2159)

### Changed

Expand Down Expand Up @@ -69,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `PyTZInfo_CheckExact`
- `PyDateTime_FromTimestamp`
- `PyDate_FromTimestamp`
- Deprecate the `gc` option for `pyclass` (e.g. `#[pyclass(gc)]`). Just implement a `__traverse__` `#[pymethod]`. [#2159](https://github.com/PyO3/pyo3/pull/2159)
- The `ml_meth` field of `PyMethodDef` is now represented by the `PyMethodDefPointer` union [2166](https://github.com/PyO3/pyo3/pull/2166)

### Removed
Expand Down
1 change: 0 additions & 1 deletion guide/src/class.md
Expand Up @@ -967,7 +967,6 @@ impl pyo3::IntoPy<PyObject> for MyClass {

impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
const DOC: &'static str = "Class for demonstration\u{0}";
const IS_GC: bool = false;
const IS_BASETYPE: bool = false;
const IS_SUBCLASS: bool = false;
type Layout = PyCell<MyClass>;
Expand Down
16 changes: 2 additions & 14 deletions guide/src/class/protocols.md
Expand Up @@ -175,7 +175,8 @@ given signatures should be interpreted as follows:

#### Garbage Collector Integration

TODO; see [#1884](https://github.com/PyO3/pyo3/issues/1884)
- `__traverse__(<self>, visit: pyo3::class::gc::PyVisit) -> Result<(), pyo3::class::gc::PyTraverseError>`
- `__clear__(<self>) -> ()`

### `#[pyproto]` traits

Expand Down Expand Up @@ -444,19 +445,6 @@ impl PyGCProtocol for ClassWithGCSupport {
}
```

Special protocol trait implementations have to be annotated with the `#[pyproto]` attribute.

It is also possible to enable GC for custom classes using the `gc` parameter of the `pyclass` attribute.
i.e. `#[pyclass(gc)]`. In that case instances of custom class participate in Python garbage
collection, and it is possible to track them with `gc` module methods. When using the `gc` parameter,
it is *required* to implement the `PyGCProtocol` trait, failure to do so will result in an error
at compile time:

```compile_fail
#[pyclass(gc)]
struct GCTracked {} // Fails because it does not implement PyGCProtocol
```

#### Iterator Types

Iterators can be defined using the
Expand Down
2 changes: 2 additions & 0 deletions pyo3-macros-backend/src/deprecations.rs
Expand Up @@ -3,12 +3,14 @@ use quote::{quote_spanned, ToTokens};

pub enum Deprecation {
CallAttribute,
PyClassGcOption,
}

impl Deprecation {
fn ident(&self, span: Span) -> syn::Ident {
let string = match self {
Deprecation::CallAttribute => "CALL_ATTRIBUTE",
Deprecation::PyClassGcOption => "PYCLASS_GC_OPTION",
};
syn::Ident::new(string, span)
}
Expand Down
38 changes: 9 additions & 29 deletions pyo3-macros-backend/src/pyclass.rs
Expand Up @@ -3,7 +3,7 @@
use crate::attributes::{
self, take_pyo3_options, CrateAttribute, NameAttribute, TextSignatureAttribute,
};
use crate::deprecations::Deprecations;
use crate::deprecations::{Deprecation, Deprecations};
use crate::konst::{ConstAttributes, ConstSpec};
use crate::pyimpl::{gen_default_items, gen_py_const, PyClassMethodsType};
use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType};
Expand All @@ -29,12 +29,12 @@ pub struct PyClassArgs {
pub base: syn::TypePath,
pub has_dict: bool,
pub has_weaklist: bool,
pub is_gc: bool,
pub is_basetype: bool,
pub has_extends: bool,
pub has_unsendable: bool,
pub module: Option<syn::LitStr>,
pub class_kind: PyClassKind,
pub deprecations: Deprecations,
}

impl PyClassArgs {
Expand Down Expand Up @@ -63,11 +63,11 @@ impl PyClassArgs {
base: parse_quote! { _pyo3::PyAny },
has_dict: false,
has_weaklist: false,
is_gc: false,
is_basetype: false,
has_extends: false,
has_unsendable: false,
class_kind,
deprecations: Deprecations::new(),
}
}

Expand Down Expand Up @@ -158,9 +158,9 @@ impl PyClassArgs {
fn add_path(&mut self, exp: &syn::ExprPath) -> syn::Result<()> {
let flag = exp.path.segments.first().unwrap().ident.to_string();
match flag.as_str() {
"gc" => {
self.is_gc = true;
}
"gc" => self
.deprecations
.push(Deprecation::PyClassGcOption, exp.span()),
"weakref" => {
self.has_weaklist = true;
}
Expand Down Expand Up @@ -757,7 +757,6 @@ impl<'a> PyClassImplsBuilder<'a> {
self.impl_into_py(),
self.impl_pyclassimpl(),
self.impl_freelist(),
self.impl_gc(),
]
.into_iter()
.collect()
Expand Down Expand Up @@ -826,7 +825,6 @@ impl<'a> PyClassImplsBuilder<'a> {
fn impl_pyclassimpl(&self) -> TokenStream {
let cls = self.cls;
let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc});
let is_gc = self.attr.is_gc;
let is_basetype = self.attr.is_basetype;
let base = &self.attr.base;
let is_subclass = self.attr.has_extends;
Expand Down Expand Up @@ -904,10 +902,11 @@ impl<'a> PyClassImplsBuilder<'a> {
let default_slots = &self.default_slots;
let freelist_slots = self.freelist_slots();

let deprecations = &self.attr.deprecations;

quote! {
impl _pyo3::impl_::pyclass::PyClassImpl for #cls {
const DOC: &'static str = #doc;
const IS_GC: bool = #is_gc;
const IS_BASETYPE: bool = #is_basetype;
const IS_SUBCLASS: bool = #is_subclass;

Expand All @@ -919,6 +918,7 @@ impl<'a> PyClassImplsBuilder<'a> {
fn for_all_items(visitor: &mut dyn ::std::ops::FnMut(& _pyo3::impl_::pyclass::PyClassItems)) {
use _pyo3::impl_::pyclass::*;
let collector = PyClassImplCollector::<Self>::new();
#deprecations;
static INTRINSIC_ITEMS: PyClassItems = PyClassItems {
methods: &[#(#default_methods),*],
slots: &[#(#default_slots),* #(#freelist_slots),*],
Expand Down Expand Up @@ -981,26 +981,6 @@ impl<'a> PyClassImplsBuilder<'a> {
Vec::new()
}
}

/// Enforce at compile time that PyGCProtocol is implemented
fn impl_gc(&self) -> TokenStream {
let cls = self.cls;
let attr = self.attr;
if attr.is_gc {
let closure_name = format!("__assertion_closure_{}", cls);
let closure_token = syn::Ident::new(&closure_name, Span::call_site());
quote! {
fn #closure_token() {
use _pyo3::class;

fn _assert_implements_protocol<'p, T: _pyo3::class::PyGCProtocol<'p>>() {}
_assert_implements_protocol::<#cls>();
}
}
} else {
quote! {}
}
}
Comment on lines -984 to -1003
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check isn't needed on Python 3.11, however it is needed on older Python versions else #[pyclass(gc)] without __traverse__ will cause segfaults.

I think a better solution might be to deprecate #[pyclass(gc)] and then in src/pyclass.rs we can check for the presence of __traverse__ to decide whether to set the Py_TPFLAGS_HAVE_GC flag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a commit which deprecates #[pyclass(gc)] as suggested.

}

fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream {
Expand Down