-
Notifications
You must be signed in to change notification settings - Fork 770
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add #[classattr]
methods to define Python class attributes
#905
Conversation
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!
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments, but generally looks good.
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 = 3
So 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice 👍
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kngwyu is your conclusion that this is safe?
if !methods.is_empty() { | ||
methods.push(ffi::PyMethodDef_INIT); | ||
type_object.tp_methods = Box::into_raw(methods.into_boxed_slice()) as *mut _; | ||
} | ||
|
||
// class attributes | ||
if !attrs.is_empty() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it safe to modify type_object
after PyType_Ready
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it safe to modify
type_object
afterPyType_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
pyo3
could be given by: