Skip to content

🐛 fix: build the frozendict backport the way the built-in does; document subclassing - #85

Merged
nstarman merged 4 commits into
GalacticDynamics:mainfrom
nstarman:claude/frozendict-subclassing
Oct 16, 2026
Merged

nstarman merged 4 commits into
GalacticDynamics:mainfrom
nstarman:claude/frozendict-subclassing

Conversation

@nstarman

Copy link
Copy Markdown
Contributor

xmmutablemap 0.3.0 broke both known subclassers, galax (CompositePotential, AbstractCompositePhaseSpaceCoordinate) and coordinax (KinematicSpace). Each is an equinox.Module that also subclassed ImmutableMap, called ImmutableMap.__init__, and declared the private _data as a field. That can't work once ImmutableMap is a frozendict, which gets its contents in __new__, and they are being fixed downstream (GalacticDynamics/coordinax#1047, GalacticDynamics/coordinax#1048). Looking into it turned up two places where the backport doesn't behave like the 3.15 built-in, plus a missing doc.

Commits

  1. 🐛 Name-mangle the backport's storage slot (_data → _frozendict__data). The built-in has no _data, so a subclass is free to set its own; on the backport that rebound the slot and replaced the mapping's contents. Pickles are unaffected: they rebuild through __new__ from the data.
  2. 🐛 Fill the storage with object.__setattr__. The built-in fills itself in C, so a subclass's __setattr__ never runs during construction: a frozen-dataclass subclass constructs on 3.15 but raised FrozenInstanceError on the backport. As a result, an equinox.Module subclass now behaves the same on every Python: an empty mapping, as on 3.15, instead of failing only on older Pythons.
  3. 📝 README: a "Subclassing" section. Override __new__ to transform input; register the subclass as a PyTree node (subclasses are opaque leaves otherwise); and a warning against combining it with an equinox.Module or a dataclass, saying what to do instead. Also fixes the tree_unflatten doctest, which passed tree_flatten's (children, keys) in swapped order and documented the resulting ImmutableMap({1: 'a', 2: 'b'}).

Testing

Each of the two fixes adds a test that fails on the backport and passes on the built-in. The full suite passes on 3.10, 3.12, 3.14 and 3.15.

🤖 Generated with Claude Code

nstarman and others added 4 commits October 15, 2026 20:42
The backport kept its contents in a slot named `_data`. The built-in has no
such attribute, so on Python 3.15 a subclass is free to use `_data` for
itself; on the backport, assigning it rebound the slot and replaced the
mapping's contents. galax and coordinax both declared a `_data` field on
their subclasses.

Name-mangle the slot to `_frozendict__data`, so `self._data` on a subclass
is just another attribute, as with the built-in. Pickles are unaffected:
they rebuild through `__new__` from the data, not from slot state.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The built-in fills itself in C, so a subclass that overrides `__setattr__`
constructs normally: a frozen dataclass subclass works on Python 3.15. The
backport assigned its storage with `self.__data = ...`, which goes through
the subclass's `__setattr__` and raised `FrozenInstanceError` there instead.

Assign it with `object.__setattr__`. This also makes an `equinox.Module`
subclass behave the same on every Python: its `__new__` drops the arguments,
so it is an empty mapping, as on 3.15, rather than failing only on older
Pythons.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Since 0.3.0 `ImmutableMap` is a `frozendict`, which takes its contents in
`__new__`. Say so in a README section on subclassing: override `__new__` to
transform input, register the subclass as a PyTree node, and don't combine
it with an `equinox.Module` or a dataclass, which galax and coordinax both
did (via `ImmutableMap.__init__` and the private `_data`, now gone).

The `tree_unflatten` doctest passed `tree_flatten`'s `(children, keys)` as
`(aux_data, children)`, and documented the swapped result
`ImmutableMap({1: 'a', 2: 'b'})`; the `jax.tree.unflatten` example passed
that pair as the leaves. Both now round-trip `d`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Pylint's protected-access fires on `other.__data` now that the slot is
name-mangled; `other` is a `frozendict` too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@8135abc). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff            @@
##             main       #85   +/-   ##
========================================
  Coverage        ?   100.00%           
========================================
  Files           ?         3           
  Lines           ?        98           
  Branches        ?         0           
========================================
  Hits            ?        98           
  Misses          ?         0           
  Partials        ?         0           

☔ View full report in Codecov by Harness.
📢 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.

@nstarman nstarman added this to the v0.3.x milestone Oct 16, 2026
@nstarman
nstarman merged commit b59878f into GalacticDynamics:main Oct 16, 2026
21 checks passed
@nstarman
nstarman deleted the claude/frozendict-subclassing branch October 16, 2026 01:39
nstarman added a commit that referenced this pull request Oct 16, 2026
…lt-in does; document subclassing (#86)

Co-authored-by: Nathaniel Starkman <nstarman@users.noreply.github.com>
nstarman added a commit to nstarman/galax that referenced this pull request Oct 16, 2026
0.3.1 builds its pre-3.15 `frozendict` backport the way the built-in does
(GalacticDynamics/xmmutablemap#85), so galax now behaves the same on every
supported Python. The lock already had 0.3.1; only the specifiers move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nstarman added a commit to nstarman/galax that referenced this pull request Oct 16, 2026
The release with GalacticDynamics/xmmutablemap#85. galax no longer depends
on how `ImmutableMap` stores its contents, so this is a floor on the fixed
release rather than a functional need. The lock already had 0.3.1; only the
specifier moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant