Skip to content

py/objtype: Add support for __set_name__. (no-self-modification version) #17693

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

AJMansfield
Copy link
Contributor

@AJMansfield AJMansfield commented Jul 16, 2025

Summary

This PR implements the feature described in #15501, adding support for the __set_name__ data model method.

In particular, this implementation leverages the existing is_fixed flag bit on micropython's flag type to avoid the potential modify-while-iterating hazard in the simplest possible way, i.e. by preventing __set_name__ from modifying the owner class.

Testing

This PR includes and passes the unit test originally submitted in #15500 to verify the feature's absence, as well as additional tests to specifically verify that attempting to mutate the descriptor's owner triggers an error.

Trade-offs and Alternatives

This is the least powerful of the four ways I've tried implementing this (just allowing the hazard in #15503, or copying a list of calls to make in #16806, or making a private copy of the dict in #16816), as the ability for a descriptor to add siblings or transparently delete or replace itself isn't an especially-rarely-used functionality in descriptor-heavy code.

The functionality in question can, though, for the most part be replaced by inheriting the owner class from a suitable parent class with an __init_subclass__ method that handles this on behalf of its descriptors. This will need to be documented as a cpydiff workaround, though that can only really be considered a workaround once we have actual __init_subclass__ (#15511) or at least documented workarounds to that method's nonfunctionality (#16786).

As an alternative, maybe this PR could be combined with either #16806 or #16816 with a feature flag that defaults to this minimal 'disallow modifying the owner' implementation, but can be set for platforms/applications where the full cost of making the extra copies is tolerable?

Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
Eliminate the potential iterate-while-modifying hazard in the calls to
user __set_name__ functions by marking the locals_dict as fixed during
the __set_name__ loop.

Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
Copy link

codecov bot commented Jul 16, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.44%. Comparing base (df05cae) to head (16d4e44).
Report is 22 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #17693   +/-   ##
=======================================
  Coverage   98.44%   98.44%           
=======================================
  Files         171      171           
  Lines       22192    22219   +27     
=======================================
+ Hits        21847    21874   +27     
  Misses        345      345           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +168 +0.020% standard[incl +32(data)]
      stm32:  +108 +0.027% PYBV10
     mimxrt:  +112 +0.030% TEENSY40
        rp2:  +120 +0.013% RPI_PICO_W
       samd:  +136 +0.051% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:  +117 +0.026% VIRT_RV32

@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Jul 18, 2025
@dpgeorge
Copy link
Member

I like this approach, it's nice and simple, and safe.

the ability for a descriptor to add siblings or transparently delete or replace itself isn't an especially-rarely-used functionality in descriptor-heavy code.

Do you think we'd be severely limiting this feature by disallowing locals dict updates?

@AJMansfield
Copy link
Contributor Author

Do you think we'd be severely limiting this feature by disallowing locals dict updates?

The use cases aren't overwhelming, so there's definitely merit to just disallowing locals dict updates; or maybe having a feature flag for this.

Just to describe a few, though:

Some descriptors need to coordinate with other descriptor instances on the same class (e.g. a serialization or ORM libraries); in these cases, it's common to have the first descriptor create an attribute on the class to hold a data structure that they all register in. (One of my own libraries use owner class attrs like this for the tables it uses to work out what registers are contiguous, for I2C transaction pooling.)

class Registered:
    def __set_name__(self, owner, name):
        try:
            owner.__registry[name] = self
        except AttributeError:
            owner.__registry = {name: self}

In CPython, a library writer can also do this using a mixin class, with an __init_subclass__ method --- but that's extra boilerplate that users easily forget, and limits extensibility since the mixin class needs to know how all of the descriptors should be processed. (Also, Micropython doesn't support multiple inheritance, so regular mixins are already out for a lot of use cases.)

It's also potentially a performance optimization in the case where a descriptor can replace itself with a concrete value at class-creation time, to avoid the indirection of dispatching access through a getter:

class Configured:
    def __set_name__(self, owner, name):
        concrete_value = query_config_database(name)
        setattr(owner, name, concrete_value)

class MyApp:
    TIME_BETWEEN_WIDGETS: int = Configured()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
py-core Relates to py/ directory in source
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy