Skip to content

Fix Config subclass direct access returning env Field - #313

Open
claude-tmgbedu wants to merge 2 commits into
mainfrom
task/config-env-direct-access
Open

claude-tmgbedu wants to merge 2 commits into
mainfrom
task/config-env-direct-access

Conversation

@claude-tmgbedu

Copy link
Copy Markdown

Fixes task #3504.

Root cause

env() returns a dataclasses.Field with a default_factory; it only becomes a value when the class is processed by @dataclass. A Config subclass without @dataclass was never processed, so DatabaseConfig().default fell through to the class attribute — the raw Field.

Fix

_ConfigMeta.__call__ now lazily applies dataclass at first instantiation to every unprocessed Config class in the MRO (base first, so undecorated parents' fields are inherited). Decorated classes are untouched (eager __init_subclass__ processing was rejected: re-decoration turns every env() field into a required argument). Casting follows existing env() rules (int/bool by default type).

ADR 035 added and indexed.

Tests

  • tests/support/test_config_without_dataclass.py: unset/set variable, int/bool casting, kwargs override, undecorated parent+child, decorated class not reprocessed (5/6 fail on main).
  • Full suite (--ignore=tests/masoniteorm/postgres): 3294 passed, coverage 91.4%.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SchgSwsSKsBpqmyLiFEuM1

@codecov

codecov Bot commented Oct 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@claude-tmgbedu claude-tmgbedu left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Verdict: REQUEST CHANGES. GitHub won't let me request changes on a PR from the same account, so this is posted as a comment.

Suite passes locally (3294 passed, Postgres ignored). The direct repro is fixed and the decorated-only path is unchanged. One inheritance case regresses, though.

  1. support/config.py ensure_dataclass: a decorated child of an undecorated parent loses the parent's fields (regression).

    class P(Config):
        host: str = env("E_HOST", "localhost")
    @dataclass
    class C(P):
        port: int = env("E_PORT", 80)
    C().host   # AttributeError; fields(C) == ['port']

    @dataclass on C runs before P has been processed, so C never inherits host. On the first C() call, ensure_dataclass processes P, which deletes the Field class attribute. On main this returned the raw Field; now it raises AttributeError. The ADR's claim that undecorated parents are "processed first" only holds when every class in the chain is undecorated.
    Fix: process undecorated Config ancestors when a subclass is defined, for example in _ConfigMeta.__init__, or in Config.__init_subclass__, over cls.__mro__[1:]. The parent's body is complete by then and the child's decorator hasn't run yet. Only the leaf would stay lazy. Add a test for this case.

  2. The Config base itself is mutated on first instantiation. reversed(cls.__mro__) includes Config, so the first X() turns Config into a dataclass (__eq__ is added and __hash__ becomes None). That is global, order-dependent state. Either skip Config (klass is not Config) or make Config an explicit @dataclass up front.

  3. Thread safety (non-blocking, but say how it's handled). The check-then-decorate is not atomic. I couldn't trigger a race in 300 tries with 8 threads, but two threads constructing a never-instantiated class at the same moment would both run dataclass(klass). Either guard it with a threading.Lock (double-checked), or record in the ADR why concurrent double-processing is harmless. Also, dataclass(Q); dataclass(Q); Q() worked for me on CPython 3.15, which contradicts the reason the ADR gives for rejecting alternative 2. Please re-check that claim, or state which Python versions it applies to.

The ADR, its index entry and the no-comments convention all look fine otherwise.

… base plain

Decorated children now inherit fields from undecorated parents, Config itself
is never a dataclass, automatic processing keeps identity hashing, and a lock
prevents concurrent double processing.

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

2 participants