Skip to content

Find a class's lazy counterpart through the base/lazy registry - #42

Merged
trissim merged 1 commit into
mainfrom
fix/35-lazy-registry
Oct 12, 2026
Merged

trissim merged 1 commit into
mainfrom
fix/35-lazy-registry

Conversation

@trissim

@trissim trissim commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #35

What changed

  • abbreviation.__call__ no longer builds f"{LAZY_CONFIG_PREFIX}{cls.__name__}" and reads it off sys.modules[cls.__module__]. Nothing in the library looks a lazy class up by name any more.
  • The base/lazy relation is owned by register_lazy_type_mapping (the existing owner; it already wrote both directions and already raised on a second counterpart in either direction). It is called once, when _create_lazy_dataclass_unified creates the lazy class. get_base_type_for_lazy and get_lazy_type_for_base are the two reads.
  • A class's group abbreviation is declared once, on the class itself (declare_group_abbreviation), and read on demand by get_group_abbreviation, which maps a lazy class to its base through the registry and then walks the base's MRO for the nearest own declaration. @abbreviation(...) and abbreviation= on the global-default decorator write that same declaration, so the order of the two decorators does not matter.
  • The lazy class name is produced in one place, the factory (LAZY_CLASS_NAME_PREFIX, which existed but was unused). The three other spellings (f"Lazy{...}" for nested fields, the make_lazy_simple default, LAZY_CONFIG_PREFIX in the decorator) are gone, and the name is no longer carried in _pending_injections. make_lazy_simple(base) with no name returns the lazy class the base already owns, whatever it is called.

Inventory before creation: searched for _lazy_type_registry, _base_to_lazy_registry, register_lazy_type_mapping, get_lazy_type_for_base, LAZY_CONFIG_PREFIX, LAZY_CLASS_NAME_PREFIX, sys.modules, GROUP_ABBREVIATIONS_REGISTRY, _abbreviation. No new registry was added; one was removed.

Acceptance criteria

Criterion How it is met
No getattr(sys.modules[...], ...) lookup of a lazy class remains Removed; test_no_lazy_class_is_looked_up_by_constructed_name pins it. The four remaining sys.modules[...] uses in lazy_factory.py are writes that export a generated class or decorator to its module.
A test defines a config class inside a function, applies the decorators in both orders, and gets the abbreviation on both the base and the lazy class test_class_inside_function_gets_abbreviation_on_base_and_lazy, parametrised over both orders.

Further cases from the task: two same-named classes in one module, a renamed prefix (both in both decorator orders), the explicit abbreviation= parameter, a subclass with and without its own declaration (including through the inherit_as_none rebuild), two different abbreviations for one class raising, and the registry refusing a second counterpart in either direction.

Observed on origin/main (1fcdf12) with the new test file: 5 fail (same-named classes with @abbreviation innermost, renamed prefix in both orders, conflicting declarations, the source check). The function-local case passes on main when the class's module is importable, because the lazy class is still exported to that module; it failed only once a second class reused the name.

Paper results

  • [P1] Proposition 3.1: the counterpart is identified by the registered class, not by a name that happens to match a convention. Two same-named classes are distinct keys.
  • [P10] Proposition 6.1: the registry entry made at creation carries its source in both directions (lazy -> base, base -> lazy); an abbreviation is read from the declaring class instead of from a copy.
  • [P10] Proposition 6.2 and [P9] Theorem 6.2: a second, different counterpart or a second, different abbreviation raises instead of last-writer-wins. Module-attribute names shared by same-named classes are no longer used to answer any query.

Behaviour changes for callers

  • objectstate.lazy_factory.GROUP_ABBREVIATIONS_REGISTRY is removed. Read with get_group_abbreviation(config_type); declare with @abbreviation / abbreviation=.
    • formbridge/strategies/preview_formatting.py:26-34 imports it and walks it by hand; it must call objectstate.lazy_factory.get_group_abbreviation at release time.
    • pyqt-reactive/tests/test_preview_formatters.py:10,229,230,279 monkeypatches it; those tests must declare with @abbreviation instead.
  • objectstate.lazy_factory.LAZY_CONFIG_PREFIX is removed. No use found in OpenHCS, pyqt-reactive, pycodify or formbridge.
  • @abbreviation("x") together with abbreviation="y" on the same class now raises ValueError. Before, which one won depended on decorator order. OpenHCS uses abbreviation= at openhcs/core/config.py:596 and :987; neither class also carries @abbreviation as far as the grep shows.
  • @abbreviation applied to a lazy class declares on its base class.
  • make_lazy_simple(base) without a name no longer raises when the base already owns a lazy class with a non-default name; it returns that class. An explicit different name still raises.

Sites outside the library that build the lazy name

grep -rn 'LAZY_CONFIG_PREFIX\|f"Lazy{' over openhcs/, external/pyqt-reactive/src, external/pycodify/src: no hits. No caller builds the lazy name itself.

pycodify-generated .py pipelines

Not affected. pycodify spells a class by its __module__ and __qualname__ (pycodify/core.py:61, formatters.py) and has no knowledge of the Lazy prefix. Lazy classes keep the same name, the same __module__, and are still exported to that module under that name, so existing generated source imports the same objects.

Found outside this issue (not changed)

  • lazy_factory.py, _inject_all_pending_fields: the global config class is recreated with make_dataclass from an almost empty namespace, so an @abbreviation on the global class itself (OpenHCS core/config.py:210, "gpc") does not reach the recreated class. On main the entry was likewise keyed by the discarded class.
  • PREVIEW_LABEL_REGISTRY, FIELD_ABBREVIATIONS_REGISTRY and ALWAYS_VIEWABLE_FIELDS_REGISTRY still copy each base entry to the lazy class at decoration time, the same restated relation this PR removes for group abbreviations.
  • The lazy class is still exported to the base's module by name; two same-named classes overwrite each other's export. Nothing in the library reads it back.

Tests

pytest -n 2: 251 passed. ruff check src/ --select E9,F63,F7,F82: clean. scripts/check_ruff_added_lines.py --base origin/main: no diagnostics on added lines. OpenHCS and formbridge were not run against this branch.

🤖 Generated with Claude Code

The lazy class of a config class was found by building its name from a
prefix and reading that name off the module. The registry that already
maps lazy to base now answers both directions, and nothing looks a lazy
class up by name.

A class's group abbreviation is declared once, on the class, and read
on demand; a lazy class answers with its base's declaration. The
@abbreviation decorator and the abbreviation= parameter write the same
declaration, so decorator order no longer matters and two different
declarations for one class raise.

GROUP_ABBREVIATIONS_REGISTRY and LAZY_CONFIG_PREFIX are removed. The
factory alone names a lazy class.

Fixes #35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 04:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@trissim

trissim commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #50 (release/3.0.0-context-resolution), which merges this branch unchanged together with #41, #49, #40 and #42 for the 3.0.0 release. Left open; review and merge decisions happen on #50.

@trissim
trissim merged commit b6b0210 into main Oct 12, 2026
11 checks passed
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.

The lazy counterpart of a class is looked up by constructed name

2 participants