Skip to content

Commit

Permalink
pyclass: deprecate gc option
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Feb 15, 2022
1 parent 676295b commit 79123b3
Show file tree
Hide file tree
Showing 13 changed files with 71 additions and 60 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -70,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
13 changes: 0 additions & 13 deletions guide/src/class/protocols.md
Expand Up @@ -445,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
17 changes: 9 additions & 8 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 @@ -825,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 @@ -903,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 @@ -918,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
12 changes: 5 additions & 7 deletions pyo3-macros/src/lib.rs
Expand Up @@ -86,12 +86,11 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream {
/// | Parameter | Description |
/// | :- | :- |
/// | <span style="white-space: pre">`name = "python_name"`</span> | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. |
/// | <span style="white-space: pre">`freelist = N`</span> | Implements a [free list][10] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
/// | `gc` | Participate in Python's [garbage collection][5]. Required if your type contains references to other Python objects. If you don't (or incorrectly) implement this, contained Python objects may be hidden from Python's garbage collector and you may leak memory. Note that leaking memory, while undesirable, [is safe behavior][7].|
/// | <span style="white-space: pre">`freelist = N`</span> | Implements a [free list][9] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
/// | `weakref` | Allows this class to be [weakly referenceable][6]. |
/// | <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][4] |
/// | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. |
/// | `unsendable` | Required if your struct is not [`Send`][3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][8] with [`Arc`][9]. By using `unsendable`, your class will panic when accessed by another thread.|
/// | `unsendable` | Required if your struct is not [`Send`][3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][7] with [`Arc`][8]. By using `unsendable`, your class will panic when accessed by another thread.|
/// | <span style="white-space: pre">`module = "module_name"`</span> | Python code will see the class as being defined in this module. Defaults to `builtins`. |
///
/// For more on creating Python classes,
Expand All @@ -103,10 +102,9 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream {
/// [4]: ../prelude/struct.PyAny.html
/// [5]: https://pyo3.rs/latest/class/protocols.html#garbage-collector-integration
/// [6]: https://docs.python.org/3/library/weakref.html
/// [7]: https://doc.rust-lang.org/nomicon/leaking.html
/// [8]: std::rc::Rc
/// [9]: std::sync::Arc
/// [10]: https://en.wikipedia.org/wiki/Free_list
/// [7]: std::rc::Rc
/// [8]: std::sync::Arc
/// [9]: https://en.wikipedia.org/wiki/Free_list
#[proc_macro_attribute]
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
use syn::Item;
Expand Down
6 changes: 6 additions & 0 deletions src/impl_/deprecations.rs
Expand Up @@ -2,3 +2,9 @@

#[deprecated(since = "0.15.0", note = "use `fn __call__` instead of `#[call]`")]
pub const CALL_ATTRIBUTE: () = ();

#[deprecated(
since = "0.16.0",
note = "implement a `__traverse__` `#[pymethod]` instead of using `gc` option"
)]
pub const PYCLASS_GC_OPTION: () = ();
3 changes: 0 additions & 3 deletions src/impl_/pyclass.rs
Expand Up @@ -149,9 +149,6 @@ pub trait PyClassImpl: Sized {
/// Class doc string
const DOC: &'static str = "\0";

/// #[pyclass(gc)]
const IS_GC: bool = false;

/// #[pyclass(subclass)]
const IS_BASETYPE: bool = false;

Expand Down
59 changes: 35 additions & 24 deletions src/pyclass.rs
@@ -1,6 +1,7 @@
//! `PyClass` and related traits.
use crate::{
callback::IntoPyCallbackOutput,
exceptions::PyTypeError,
ffi,
impl_::pyclass::{
assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, PyClassDict,
Expand Down Expand Up @@ -53,7 +54,6 @@ where
T::dict_offset(),
T::weaklist_offset(),
&T::for_all_items,
T::IS_GC,
T::IS_BASETYPE,
)
} {
Expand All @@ -74,7 +74,6 @@ unsafe fn create_type_object_impl(
dict_offset: Option<ffi::Py_ssize_t>,
weaklist_offset: Option<ffi::Py_ssize_t>,
for_all_items: &dyn Fn(&mut dyn FnMut(&PyClassItems)),
is_gc: bool,
is_basetype: bool,
) -> PyResult<*mut ffi::PyTypeObject> {
let mut slots = Vec::new();
Expand Down Expand Up @@ -117,28 +116,32 @@ unsafe fn create_type_object_impl(
let mut has_new = false;
let mut has_getitem = false;
let mut has_setitem = false;
let mut has_gc_methods = false;
let mut has_traverse = false;
let mut has_clear = false;

// Before Python 3.9, need to patch in buffer methods manually (they don't work in slots)
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
let mut buffer_procs: ffi::PyBufferProcs = Default::default();

for_all_items(&mut |items| {
for slot in items.slots {
has_new |= slot.slot == ffi::Py_tp_new;
has_getitem |= slot.slot == ffi::Py_mp_subscript;
has_setitem |= slot.slot == ffi::Py_mp_ass_subscript;
has_gc_methods |= slot.slot == ffi::Py_tp_clear || slot.slot == ffi::Py_tp_traverse;
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
if slot.slot == ffi::Py_bf_getbuffer {
// Safety: slot.pfunc is a valid function pointer
buffer_procs.bf_getbuffer = Some(std::mem::transmute(slot.pfunc));
}

#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
if slot.slot == ffi::Py_bf_releasebuffer {
// Safety: slot.pfunc is a valid function pointer
buffer_procs.bf_releasebuffer = Some(std::mem::transmute(slot.pfunc));
match slot.slot {
ffi::Py_tp_new => has_new = true,
ffi::Py_mp_subscript => has_getitem = true,
ffi::Py_mp_ass_subscript => has_setitem = true,
ffi::Py_tp_traverse => has_traverse = true,
ffi::Py_tp_clear => has_clear = true,
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
ffi::Py_bf_getbuffer => {
// Safety: slot.pfunc is a valid function pointer
buffer_procs.bf_getbuffer = Some(std::mem::transmute(slot.pfunc));
}
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
ffi::Py_bf_releasebuffer => {
// Safety: slot.pfunc is a valid function pointer
buffer_procs.bf_releasebuffer = Some(std::mem::transmute(slot.pfunc));
}
_ => {}
}
}
slots.extend_from_slice(items.slots);
Expand Down Expand Up @@ -170,14 +173,21 @@ unsafe fn create_type_object_impl(
push_slot(&mut slots, ffi::Py_tp_new, no_constructor_defined as _);
}

if has_clear && !has_traverse {
return Err(PyTypeError::new_err(format!(
"`#[pyclass]` {} implements __clear__ without __traverse__",
name
)));
}

// Add empty sentinel at the end
push_slot(&mut slots, 0, ptr::null_mut());

let mut spec = ffi::PyType_Spec {
name: py_class_qualified_name(module_name, name)?,
basicsize: basicsize as c_int,
itemsize: 0,
flags: py_class_flags(has_gc_methods, is_gc, is_basetype),
flags: py_class_flags(has_traverse, is_basetype),
slots: slots.as_mut_ptr(),
};

Expand Down Expand Up @@ -287,12 +297,13 @@ fn py_class_qualified_name(module_name: Option<&str>, class_name: &str) -> PyRes
.into_raw())
}

fn py_class_flags(has_gc_methods: bool, is_gc: bool, is_basetype: bool) -> c_uint {
let mut flags = if has_gc_methods || is_gc {
ffi::Py_TPFLAGS_DEFAULT | ffi::Py_TPFLAGS_HAVE_GC
} else {
ffi::Py_TPFLAGS_DEFAULT
};
fn py_class_flags(is_gc: bool, is_basetype: bool) -> c_uint {
let mut flags = ffi::Py_TPFLAGS_DEFAULT;

if is_gc {
flags |= ffi::Py_TPFLAGS_HAVE_GC;
}

if is_basetype {
flags |= ffi::Py_TPFLAGS_BASETYPE;
}
Expand Down
4 changes: 2 additions & 2 deletions tests/test_gc.rs
Expand Up @@ -128,7 +128,7 @@ fn gc_integration() {
assert!(drop_called.load(Ordering::Relaxed));
}

#[pyclass(gc)]
#[pyclass]
struct GcIntegration2 {}

#[pymethods]
Expand Down Expand Up @@ -215,7 +215,7 @@ fn inheritance_with_new_methods_with_drop() {
assert!(drop_called2.load(Ordering::Relaxed));
}

#[pyclass(gc)]
#[pyclass]
struct TraversableClass {
traversed: AtomicBool,
}
Expand Down
4 changes: 2 additions & 2 deletions tests/test_gc_pyproto.rs
Expand Up @@ -130,7 +130,7 @@ fn gc_integration() {
assert!(drop_called.load(Ordering::Relaxed));
}

#[pyclass(gc)]
#[pyclass]
struct GcIntegration2 {}

#[pyproto]
Expand Down Expand Up @@ -217,7 +217,7 @@ fn inheritance_with_new_methods_with_drop() {
assert!(drop_called2.load(Ordering::Relaxed));
}

#[pyclass(gc)]
#[pyclass]
struct TraversableClass {
traversed: AtomicBool,
}
Expand Down
3 changes: 3 additions & 0 deletions tests/ui/deprecations.rs
Expand Up @@ -11,6 +11,9 @@ impl DeprecatedCall {
fn deprecated_call(&self) {}
}

#[pyclass(gc)]
struct DeprecatedGc;

fn main() {

}
6 changes: 6 additions & 0 deletions tests/ui/deprecations.stderr
Expand Up @@ -9,3 +9,9 @@ note: the lint level is defined here
|
1 | #![deny(deprecated)]
| ^^^^^^^^^^

error: use of deprecated constant `pyo3::impl_::deprecations::PYCLASS_GC_OPTION`: implement a `__traverse__` `#[pymethod]` instead of using `gc` option
--> tests/ui/deprecations.rs:14:11
|
14 | #[pyclass(gc)]
| ^^

0 comments on commit 79123b3

Please sign in to comment.