Open mejrs opened 2 years ago
Hmm interesting. I suspect this is a behavioural change we missed when we switched to "heap types" to support abi3 - see https://pyo3.rs/v0.16.4/migration.html#runtime-changes-to-support-the-cpython-limited-api
Type objects are now mutable - Python code can set attributes on them.
I'm unsure if this is something we should aim to fix, or just accept that it's been this way for a year and a half and update documentation?
I'm unsure if this is something we should aim to fix
I think so yes. I would be really surprised if I wrote:
#[pymethods]
impl MyClass {
#[classattr]
fn my_attribute() -> String {
"hello".to_string()
}
}
and python code was able to change it. IMO that kind of behaviour should be an opt-in. Maybe something like (to be bikeshedded) #[pyo3(changeable_from_python]
.
Can you overwrite methods from pymethods
by reassigning them on the class? If yes, I don't see why class attrs are different.
For what it's worth, a quick search suggests the type flag we'd want to use to implement this is only present in python 3.10: https://docs.python.org/3/c-api/typeobj.html#Py_TPFLAGS_IMMUTABLETYPE
I think for older versions than 3.10 the only way to protect would be a custom metaclass?
We could set tp_setattro
on the type object to point to a function that just returns an error.
Can you overwrite methods from
pymethods
by reassigning them on the class? If yes, I don't see why class attrs are different.
Yes
We could set
tp_setattro
on the type object to point to a function that just returns an error.
I think not on the type but on the type's type, right? i.e. metaclass 😊
Right. So this means we effectively have to support metaclasses?
On Python < 3.10, we can set this inside create_type_object_impl
:
unsafe extern "C" fn error_setattrofunc(_slf: *mut ffi::PyObject, _attr: *mut ffi::PyObject, _value: *mut ffi::PyObject) -> c_int {
let py = Python::assume_gil_acquired();
PyTypeError::new_err("cannot set '...' attribute of immutable type '...'").restore(py);
-1
}
(*(*type_object).ob_type).tp_setattro = Some(error_setattrofunc);
I've always thought of native classes being immutable as a natural default and an advantage. I would like to restore this behaviour, if you all agree.
Edit: Or is this mutating type
? 🤔
I've always thought of native classes being immutable as a natural default and an advantage. I would like to restore this behaviour, if you all agree.
I agree it would be nice to restore this behaviour, but I think it's potentially hard - AFAIK we can't achieve it for the older Pythons without writing a custom metaclass.
I think writing a custom metaclass isn't fun even in pure-Python. There's also a question of where our custom metaclass would live - would it be global data shared amongst all PyO3 objects? How would it interact if multiple packages with different versions of PyO3 are all in use?
If you're feeling brave, please do stab at this! It might take a few attempts for us to figure out what works nicely.
Edit: Or is this mutating type?
Precisely :). If we had out own metaclass, *(*type_object).ob_type)
would be our metaclass instead of type
.
I agree it would be nice to restore this behaviour, but I think it's potentially hard - AFAIK we can't achieve it for the older Pythons without writing a custom metaclass.
That, and I also think it's unnecessary. "C extension" classes behaving different from normal classes was always an artifact of the non-heaptype implementation. Monkey-patching methods and attributes is a fact of life in Python, and oftentimes even helpful.
Monkey-patching methods and attributes is a fact of life in Python, and oftentimes even helpful.
I think that largely depends on perspective. It's certainly nice when you, as a user, just want to make something work in a dirty way. As a library author it's terrifying that people can just reach into your implementation details and change stuff. When cpython did the static-> heap type conversion, the new mutability was considered a problem.
Anyway: I don't think we can do this with limited api. As we wouldn't be able to do it consistently we might as well ditch the metaclass idea and only do it on 3.10+ with the Py_TPFLAGS_IMMUTABLETYPE
flag. I still think that is preferable over current, but I'm not a fan of the inconsistency.
Also: this gave a me an idea for a new crate :)
Perhaps we should have a #[pyclass(immutable_type_object)]
opt-in? We could then document that It only works properly on 3.10+?
Coming back to this, I think we should just add the opt in and leave the default unchanged. I think doing so is a relatively small macro addition to set the flag, so I'll mark this as a good first issue.
The guide states:
But this is in fact possible: