Skip to content

Run post_init for the class being instantiated - #166

Merged
ESultanik merged 2 commits into
masterfrom
149-post-init-self
Sep 9, 2026
Merged

ESultanik merged 2 commits into
masterfrom
149-post-init-self

Conversation

@ESultanik

Copy link
Copy Markdown
Collaborator

Closes #149

DataClassNode.__init__ called post_init() only on the entries of _DATA_CLASS_ANCESTORS, which excludes the
class being constructed. A subclass that used the documented extension point instead of overriding __init__ never
had its callback run, and nothing reported the omission: the callback began firing only once some other class
subclassed it. The ancestor list was also in __mro__ order, most derived first, so ancestor callbacks ran in the
reverse of constructor order, contradicting the base-up initialization the post_init() docstring described.

Each class in the hierarchy that defines its own post_init is now recorded at class-definition time, least derived
first and ending with the class itself. Selecting on __dict__ keeps an implementation that a subclass inherits
without overriding from running once per inheriting class, so subclasses of a data class still get exactly one call.

docs/builders.rst carried a note documenting the old behavior as a limitation; it is replaced with the inherited
implementation rule.

No DataClassNode subclass in the package defines post_init (graphtage/ast.py and graphtage/pydiff.py use
__init__ overrides), so no in-tree node changes behavior.

Validation

Three tests in test/test_dataclasses.py were confirmed to fail against the unfixed code and pass after the fix:

  • test_post_init_runs_once_per_implementation asserts the call order and count for a three-level hierarchy where the
    middle class inherits its post_init (expected [("Base", "Derived"), ("Derived", "Derived")]; the unfixed code
    produced [] for the first instantiation).
  • test_post_init_runs_for_direct_subclass covers the UnquotedName example from the docs.
  • test_inheritance was strengthened: both Foo.post_init and Bar.post_init set distinct flags, so the assertion no
    longer passes when only the ancestor's callback runs. It previously passed by accident.

The reproducer from the issue now prints [('Node1', 'Node1')] for the direct instantiation, and a diamond hierarchy
calls each implementation once in base-first order.

Local CI:

  • ruff check graphtage test docs bindist: passed
  • pytest: 142 passed
  • make -C docs html SPHINXOPTS="-W --keep-going": build succeeded
  • uv lock --check: fails in this sandbox on an unmodified checkout of master as well, because of a global
    exclude-newer timestamp. No dependency files were touched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GypKU5KdLfs2Cf8kS2TzJa

ESultanik and others added 2 commits September 9, 2026 10:17
DataClassNode.__init__ called post_init only on the entries of
_DATA_CLASS_ANCESTORS, a list that deliberately excludes the class being
constructed. A class that used the documented extension point instead of
overriding __init__ therefore never had its callback run, and the omission was
silent: the callback started firing only once some other class subclassed it.

The ancestor list was also in __mro__ order, most derived first, so ancestor
callbacks ran in the reverse of constructor order, contradicting the base-up
initialization the post_init docstring described.

Each class in the hierarchy that defines its own post_init is now recorded at
class-definition time, least derived first and ending with the class itself.
Selecting on __dict__ keeps an implementation that a subclass inherits without
overriding from running once per inheriting class, which preserves the single
call that subclasses of a data class already got.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GypKU5KdLfs2Cf8kS2TzJa
@ESultanik
ESultanik merged commit 153b5fe into master Sep 9, 2026
12 checks passed
@ESultanik
ESultanik deleted the 149-post-init-self branch September 9, 2026 14:39
ESultanik added a commit that referenced this pull request Sep 9, 2026
The slot accumulation added in the previous commit was written against an
earlier `__init_subclass__`, where `ancestors` was in plain `__mro__` order
and had to be reversed to merge annotations base-first. PR #166 has since
rebuilt that list from `reversed(cls.__mro__)`, so it already arrives
base-first and reversing it again inverted the merge order: a diamond
`class D(B, C)` produced slots in `B`-then-`C` order rather than the
declaration order the ancestors imply.

Because none of the edited lines overlapped, the change applied cleanly on
top of #166 while its meaning changed underneath it, and the accompanying
test failed on the merge commit.

Deriving `_SLOTS` from the merged annotations also left `new_slots` written
but never read. Ruff does not report it, because `new_slots.append(...)`
counts as a reference for F841.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GypKU5KdLfs2Cf8kS2TzJa
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.

DataClassNode.post_init() never runs for the class being instantiated

1 participant