Add #[classattr] methods to define Python class attributes#905
Add #[classattr] methods to define Python class attributes#905kngwyu merged 4 commits intoPyO3:masterfrom
#[classattr] methods to define Python class attributes#905Conversation
|
Thanks for this PR, a way to add class attributes like this would be great. Couple questions (disclaimer, I haven't read the code yet):
Can these things ever be settable? Maybe a pair of names like
Did you explore mechanisms to prevent returning Also, I wonder whether we could use syntax like associated constants instead of functions. But I think they're only stable for traits, not structs? |
|
It would be nice if we could also use associated constants (which are stable, #[pymethods]
impl MyClass {
#[attribute]
pub const MY_CONST: &str = "blah";
#[attribute]
pub fn my_const2(_py: Python) -> Vec<u8> {
vec![1, 2, 3]
}
} |
|
Interesting, thank you @scalexm!
|
|
Haven't dug into the implementation, but it's not clear - can you modify these at runtime? Either way I would add a test for the expected behavior if you try to assign a value to it. Also, it's not immediately clear: Is the function evaluated when the class is constructed, or every time it is accessed? |
In the current implementation, we just do
They are settable in Python.
Agreed. |
|
Thanks for the reviews.
Yes, that’s probably better. Always called that class attributes but indeed the official Python documentation calls them class variables :)
Python does support it, in which case an exception is raised when the module is imported. It’s indeed not clear how useful it is, so I guess we can disable it for now (only accept About using associated constants, I can open a follow-up PR. |
Nice, I was so sure they were stable but then when looking at documentation to confirm this last night I got bogged down in the docs for traits xD
If they're settable in Python, but we don't want to allow setting these in Rust, I suggest we make it so that an exception is raised if a user attempts to set these attributes from Python. Otherwise the original Rust value and current Python value may become out of sync? |
|
For comparison, here's what happens with a Cython cdef class MyClass:
attr = 3So it seems like they are called "attributes" and that it is reasonable for them to be read-only in both Python and Rust. I think |
| } | ||
| } | ||
|
|
||
| #[test] |
There was a problem hiding this comment.
Mentioned this in my comment, but adding an in-line note so it doesn't get lost: I think it would be a good idea to add tests for the error cases, to ensure that they raise the correct exception.
Could be done here or it could be done in the examples/rustapi_module tests, which are written in Python and where you can use pytest.raises.
Thank you for the feedback.
I checked the code that Cython produced and what it does is the same as this PR. They set if (PyDict_SetItem((PyObject *)__pyx_ptype_4demo_MyClass->tp_dict, __pyx_n_s_attr, __pyx_int_3) < 0) __PYX_ERR(1, 2, __pyx_L1_error)But I don't understand how it makes the |
Because Python enforces that, for classes defined using the C API that PyO3 uses, type objects can't be changed (I'm assuming since they're shared across all interpreters in the program) and |
|
+1 for
Nice. I guess should make sure there are tests in this PR to verify that trying to modify a classattr raises an exception as described. |
|
Current status:
Next steps:
|
tests/test_class_attributes.rs
Outdated
| let gil = Python::acquire_gil(); | ||
| let py = gil.python(); | ||
| let typeobj = py.get_type::<Foo>(); | ||
| py_expect_exception!(py, typeobj, "typeobj.foo = 6", TypeError); |
|
Thank you for the update!
Me too, so the top candidate is
Yeah, please place some docs in
👍 |
|
For naming, I'm happy with any of the options other than |
| if self_.is_some() || !arguments.is_empty() { | ||
| return Err(syn::Error::new_spanned( | ||
| name, | ||
| "Class attribute methods cannot take arguments", |
There was a problem hiding this comment.
It may be helpful to optionally allow one argument, of type Python, for those who want to create python types as class attributes. See pymethod/impl_wrap_getter as an example of how we allow this for getters.
One caveat I am not sure about though: as we're currently in the middle of creating a type object, is it safe to run arbitrary Python code?
There was a problem hiding this comment.
is it safe to run arbitrary Python code?
Maybe this code can cause SIGSEGV.
Oh I was wrong. This code works. Still investigating this can cause some odd errors, though.
#[pymethods]
impl MyClass {
#[classattr]
fn foo() -> MyClass { ... }
}I'll open a small PR to prevent this.
| } | ||
|
|
||
| // class attributes | ||
| if !attrs.is_empty() { |
There was a problem hiding this comment.
Considering a recursive case(e.g.,
#[pymethods]
impl MyClass {
#[classattr]
fn foo() -> MyClass { ... }
}), could you please move this code after PyType_Ready?
Then an incomplete type object is never used.
There was a problem hiding this comment.
That was precisely one of my use cases indeed. But I think it’s still possible to return MyOtherClass where MyOtherClass has not yet been initialized?
There was a problem hiding this comment.
But I think it’s still possible to return MyOtherClass where MyOtherClass has not yet been initialized?
Yes, but I'm not sure there's no corner case 🤔
There was a problem hiding this comment.
Is it safe to modify type_object after PyType_Ready ?
There was a problem hiding this comment.
I realized that it cannot be a problem since PyCell::new doesn't use the type object at all.
Thanks @scalexm
There was a problem hiding this comment.
I’ll add a few « recursive » test cases to be sure that we don’t break that assumption in the future.
There was a problem hiding this comment.
Is it safe to modify
type_objectafterPyType_Ready?
no, it's not safe, according to the C API docs
#[attribute] methods to define Python class attributes#[classattr] methods to define Python class attributes
|
LGTM, thank you for the update! |
Here is my approach to #662: we add an attribute on methods defined in
#[pymethods]impl blocks to allow defining class attributes (also named class variables).A class attribute method cannot take arguments, and must return
impl IntoPy<PyObject>.For example, given the following Python code:
a translation in Rust with
pyo3could be given by: