refactor: drop frozen=True from instruments; sentinel for FastAPIConfig.application by lesnik512 · Pull Request #101 · modern-python/lite-bootstrap · GitHub
Skip to content

refactor: drop frozen=True from instruments; sentinel for FastAPIConfig.application#101

Merged
lesnik512 merged 6 commits into
mainfrom
refactor/ref-6-frozen-setattr
Jun 1, 2026
Merged

refactor: drop frozen=True from instruments; sentinel for FastAPIConfig.application#101
lesnik512 merged 6 commits into
mainfrom
refactor/ref-6-frozen-setattr

Conversation

@lesnik512

@lesnik512 lesnik512 commented Jun 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related cleanups in one PR:

  • REF-6 cascade: LoggingInstrument and OpenTelemetryInstrument cached mutable runtime state via object.__setattr__ workarounds because they were declared frozen=True. Python's dataclass rules forbid surgically dropping frozen=True from one subclass while the parent stays frozen — the cascade is required. BaseInstrument and all 22 instrument subclasses lose frozen=True. The 4 object.__setattr__ call sites in LoggingInstrument and OpenTelemetryInstrument become plain self._x = ... assignments. Configs all keep frozen=True — only instruments lose immutability.
  • LOW-4: FastAPIConfig.application used default=None with a # ty: ignore[invalid-assignment]. Replaced with a typed sentinel _UNSET_FASTAPI_APP: typing.Final = typing.cast("fastapi.FastAPI", object()). The __post_init__ check changes from if not self.application: to if self.application is _UNSET_FASTAPI_APP:. Added an explicit None guard with ConfigurationError to prevent silent AttributeError if a caller explicitly passes None (was a tolerated type-violation pre-change; now fails cleanly).

12 files modified; 24 dataclass declarations lose frozen=True; 4 object.__setattr__ call sites simplified; 1 # ty: ignore removed; 1 explicit-None guard added.

No behavior change for typed callers. 128/128 tests pass.

Closes REF-6 and LOW-4 from an internal audit.

Why the cascade

Python's @dataclasses.dataclass enforces that a non-frozen dataclass cannot inherit from a frozen one (and vice versa) — TypeError at class definition. To drop frozen=True from LoggingInstrument (REF-6's stated target), BaseInstrument must also drop it, which propagates to every other instrument subclass. The sequencing spec's PR13 section called for a surgical 2-class change; the actual cascade is 24 classes.

Test plan

  • just test — 128/128.
  • just lint — clean (no # ty: ignore left in FastAPIConfig).
  • grep -n "frozen=True" lite_bootstrap/instruments/ lite_bootstrap/bootstrappers/ — only config classes match.
  • Reviewer: confirm LoggingInstrument's and OpenTelemetryInstrument's direct assignments preserve the try/finally exception safety from PR3.

Notes for reviewer

  • Pyright now flags bootstrap_config override invariance in framework subclasses (e.g., FastAPICorsInstrument's bootstrap_config: FastAPIConfig overriding CorsInstrument's bootstrap_config: CorsConfig). This is a Pyright-only concern about mutable-field covariance; ty check passes clean. Worth a separate look later if the team cares about Pyright; out of scope for this PR.
  • The explicit-None guard in FastAPIConfig.__post_init__ is added in response to code review — it catches the new behavioral edge case where application=None would otherwise hit AttributeError instead of building a fresh app like the pre-PR truthiness check did.

🤖 Generated with Claude Code

…ig.application

REF-6: LoggingInstrument and OpenTelemetryInstrument cached mutable
runtime state (_logger_factory, _tracer_provider) via
object.__setattr__ workarounds because the instruments were declared
frozen=True. The frozen claim was partly false — those two fields
mutated freely under the hood.

Python's dataclass rules forbid surgically dropping frozen=True from
a subclass while the parent remains frozen (TypeError at class
definition). The fix cascades through BaseInstrument and all 22
instrument subclasses: drop frozen=True from each. Configs stay
frozen — only instruments lose immutability. The 4 object.__setattr__
call sites in LoggingInstrument and OpenTelemetryInstrument
(bootstrap-cache + teardown-reset for each) become plain self._x = ...
assignments.

LOW-4: FastAPIConfig.application declared default=None with a
# ty: ignore[invalid-assignment] because the field is typed
fastapi.FastAPI (non-Optional). Replace with a typed sentinel:
_UNSET_FASTAPI_APP: typing.Final = typing.cast("fastapi.FastAPI", object())
The cast suppresses the type lie; the sentinel makes __post_init__'s
identity check (is _UNSET_FASTAPI_APP) clearer than the prior
truthiness check (not self.application). FastAPIConfig stays frozen,
so the object.__setattr__(self, "application", ...) in __post_init__
remains — only the default and the check change.

No behavior change. 128/128 tests pass.

Closes REF-6 and LOW-4 from the audit.
@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

@lesnik512 lesnik512 self-assigned this Jun 1, 2026
lesnik512 and others added 5 commits June 1, 2026 19:24
…one guard

Replace the local _UNSET_FASTAPI_APP cast with the UnsetType / UNSET pattern
from modern-di, exposed via lite_bootstrap.types. FastAPIConfig.application is
now honestly typed as fastapi.FastAPI | UnsetType; internal access sites
narrow through a _narrow_app helper so the public union doesn't leak into
instrument call chains. Adds a regression test for the explicit-None guard
that brings coverage back to 100%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The application field is typed as fastapi.FastAPI | UnsetType — None is not
in the type, so the runtime None check is defensive code for a type-violation
that callers cannot reach honestly. Trust the type system; drop the guard and
its regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The application field is typed as fastapi.FastAPI | UnsetType — None is not
in the type, so the runtime None check is defensive code for a type-violation
that callers cannot reach honestly. Trust the type system; drop the guard and
its regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap dev dep httpx -> httpx2 to match starlette's TestClient deprecation
notice, and switch the litestar OTEL span-naming test's path parameter to
the FromPath[int] form the framework now recommends. Drops 2 warnings from
the suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 merged commit d15c2de into main Jun 1, 2026
7 checks passed
@lesnik512 lesnik512 deleted the refactor/ref-6-frozen-setattr branch June 1, 2026 16:42
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