py/objtype: Ensures native attr handlers process subclass attribute stores#18594
py/objtype: Ensures native attr handlers process subclass attribute stores#18594cnadler86 wants to merge 1 commit into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #18594 +/- ##
=======================================
Coverage 98.51% 98.51%
=======================================
Files 176 176
Lines 22903 22910 +7
=======================================
+ Hits 22562 22569 +7
Misses 341 341 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code size report: |
2ac14e6 to
f402456
Compare
|
It would be nice to to have additional tests without a |
Yes. but the problem is that i need a cpython built in with an attr handler for that. I could extend a test with something like this: Will then use this and more or less test hat the properties of the subclass work as expected. What do you think? |
|
Hmm... this is a bit tricky. That isn't a valid test since the base class is not implement in C in MicroPython. I looked at all of the builtin types in MicroPython that have an |
2042cb8 to
22e6986
Compare
Yes, I thought custom classes will not be the right approach. A possibility would be to extend the At the moment I just added some additional tests. They don't bring more coverage, and IMO could be deleted, but I left them here for discussion at the moment. |
|
Hi everyone, I wanted to politely follow up on this PR. It's been a few weeks since I submitted this fix for the native attribute handler issue in subclasses. I'd be happy to address any feedback or make adjustments if needed. Could someone let me know if there are any concerns with the approach, or if additional tests/documentation would be helpful? Thanks for taking the time to review this, and I appreciate your work on MicroPython! Best regards, |
|
Thanks @cnadler86 for the PR. I think MicroPython already implements the correct behaviour here, namely that stores to an instance go to the top-level instance, and are not delegated to base classes. Loads are similar in that the top-level instance is searched first. Only if that fails does the search continue in the base classes. Since a store will always succeed at the top-level, there's no need to go to the base class for a store. Your original bug report was about inheriting from So I'd be inclined to close this PR. |
|
Hi @dpgeorge, I have on c-level the following property definition (only as example // Property handler
static void camera_obj_property(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in);
if (dest[0] == MP_OBJ_NULL) {
// Load (reading)
switch (attr) {
case MP_QSTR_frame_size:
dest[0] = MP_OBJ_NEW_SMALL_INT(mp_camera_hal_get_frame_size(self));
break;
default:
// Delegate to locals_dict
dest[1] = MP_OBJ_SENTINEL;
return;
}
} else if (dest[0] == MP_OBJ_SENTINEL && dest[1] != MP_OBJ_NULL) {
// Store (writing)
switch (attr) {
case MP_QSTR_frame_size:
mp_camera_hal_set_frame_size(self, mp_obj_get_int(dest[1]));
break;
default:
// Delegate to locals_dict for methods not handled here
return;
}
// Success - indicate attribute was stored
dest[0] = MP_OBJ_NULL;
}
...
MP_DEFINE_CONST_OBJ_TYPE(
camera_type,
MP_QSTR_Camera,
MP_TYPE_FLAG_NONE,
make_new, mp_camera_make_new,
print, mp_camera_hal_print,
attr, camera_obj_property,
locals_dict, &camera_camera_locals_dict
);Then, in python I subclass from my C-Level class (e.g. in modul from camera import Camera as _Camera
class Camera(_Camera):Then, if I instantiate a Camera object from the acamera module, the properties inherited from _Camera are not settable. I can get get them, but not use the c-level setter. Instead, they are stored in the Of course, there is always a work around for this, but I found the behaviour as you mention unexpected. Not sure if c-python would handle such a situation the same way. I just used |
|
Thanks for the code snippet. That definitely helps to explain the issue. After some testing I did find a way to exhibit this behaviour in CPython. Here's the code: import socket
class MySocket(socket.socket):
pass
s = MySocket()
print(s.__dict__)
s.foo = 123
print(s.__dict__)
print(s.proto)
s.proto = 456That gives This CPython tests shows that CPython does indeed delegate stores first to the underlying native object. Only if there's no such attribute does it then create a new entry in the locals Using the same test but storing Still... adding this to MicroPython will add code size, and more importantly give a performance hit to stores for all user classes. Need to consider if it's worth it. |
756d218 to
451c3f0
Compare
|
|
Fixes incorrect delegation of attribute assignment in subclasses of native types with custom attribute handlers by ensuring the handler is invoked before storing attributes in the instance dictionary. Adds tests to verify correct behavior for both native and subclassed types. Signed-off-by: Christopher Nadler <christopher.nadler@gmail.com>
|
I changed the test path to have no PR dependencies. |
| # Test 1: Basic subclass functionality | ||
| e1 = MyException("hello", "world") | ||
| assert e1.args == ("hello", "world"), "Subclass args failed" | ||
| print("Subclass creation: OK") |
There was a problem hiding this comment.
If this test runs the same under CPython (and I think it does) then it's simpler (and consistent with other tests) to just do print(e1.args) here, instead of the assert and the print of "Subclass creation: OK" (eg if it fails in the future it'll at least print out the incorrect value of e1.args instead of just failing the assert).
| e2 = MyException("Test PASS") | ||
| e2.custom_attr = "test" # type: ignore | ||
| assert e2.custom_attr == "test", "Custom attribute failed" # type: ignore | ||
| print("Dict fallback: OK") |
There was a problem hiding this comment.
As above, these two lines could simply be print(e2.custom_attr).
Summary
Problem Description
When a Python class inherits from a native type that uses an
attrhandler toimplement properties, setting attributes on instances fails to call the native
property setters. Instead, the values are stored in the instance's members
dictionary, shadowing the native properties.
Root Cause
In
objtype.c:mp_obj_instance_store_attr()did not check whether the native base type hasan
attrhandler before storing attributes in the instance's memberdictionary.
mp_obj_class_lookup()already delegates to the nativesubobject's
attrhandler, but there was no equivalent for STORE operations.Solution
Modified
mp_obj_instance_store_attr()to give the native base'sattrhandlera chance to handle the store/delete before the value is written to the
instance dict:
the instance's type has a native base class with an
attrhandler.attrprotocol (the same convention used bymp_store_attr):dest[0]ispassed as
MP_OBJ_SENTINELanddest[1]holds the value.performs the store and marks it handled (leaves
dest[0] != MP_OBJ_SENTINEL),and we return success. If it leaves
dest[0] == MP_OBJ_SENTINEL, it doesn'town the attribute, so we fall through to the normal property/descriptor checks
and dict storage.
rejecting an invalid value) propagates to the caller, exactly as in CPython
for attribute assignment. No special-casing or catching of exceptions is
needed.
This keeps the implementation small and consistent with how native
attrhandlers already signal "not handled" elsewhere in the codebase (e.g. the
exception type's handler in
objexcept.c).Fixes #18592
Testing
tests/misc/cexample_subclass.py(extended)Uses
cexample.AdvancedTimer— a native type whoseattrhandler implements.secondsas a read/write property and follows the sentinel protocol for everyother attribute. This is a dedicated, well-behaved C-level test object (built
into the unix
coveragevariant), independent of any module-specific quirks.It verifies, discriminatingly (these assertions fail on the pre-fix code):
self.seconds = 123, the store wasdelegated to the native setter and is not shadowed in the instance dict
(
"seconds" not in self.__dict__).handler doesn't own (
self.extra = "hello") falls through to normal instance__dict__storage and reads back correctly.Coverage of the added
objtype.cblock was confirmed with gcov: both outcomesof the delegation branch are exercised by this single test (the handler-handled
path and the fall-through path).
tests/basics/subclass_native_attrs.pyException-based regression test that runs on all ports: verifies that native
attribute access (
.args) keeps working on a subclass while arbitrary Pythonattributes still fall back to the instance dict.
Notes
AttributeError/KeyErrorfrom the handler (original approach):An earlier version wrapped the handler call in
nlr_push/nlr_popand, onAttributeError/KeyError, fell back to dict storage. This was dropped perreview: it's more complex, swallows genuine errors, and is not CPython-like.
The sentinel protocol already provides a clean "not handled" signal, and real
errors (e.g. a setter's
TypeError) should propagate.uctypes. It was replaced because theuctypesattrhandler raisesKeyErrorfor unknown fields instead of leaving the sentinel, which coupledthe test to a uctypes-specific quirk.
cexample.AdvancedTimerkeeps the testindependent and discriminating.
attrhandler raises (rather thanleaving the sentinel) for unknown attributes will, by design, propagate that
exception in subclasses too instead of falling back to the dict. This matches
the handler's own contract; well-behaved handlers that follow the sentinel
protocol get dict fall-through for free.