Skip to content

refactor: convert SessionSummary to frozen dataclass (#1067)#1069

Open
microsasa wants to merge 2 commits intomainfrom
fix/1067-session-summary-frozen-dataclass-21a98bd2162d708c
Open

refactor: convert SessionSummary to frozen dataclass (#1067)#1069
microsasa wants to merge 2 commits intomainfrom
fix/1067-session-summary-frozen-dataclass-21a98bd2162d708c

Conversation

@microsasa
Copy link
Copy Markdown
Owner

Closes #1067

Summary

Converts SessionSummary from a Pydantic BaseModel to a @dataclasses.dataclass(frozen=True, slots=True) to align with the coding guideline: "Internal intermediate state uses frozen dataclasses with slots=True".

SessionSummary is internal intermediate state — populated by build_session_summary from already-validated SessionEvent objects, never parsed directly from JSON. Making it a frozen dataclass prevents accidental field mutation on cached instances.

Changes

File Change
src/copilot_usage/models.py Replace BaseModel with frozen dataclass; move @model_validator to __post_init__; use dataclasses.field for mutable defaults; remove unused Self and model_validator imports
src/copilot_usage/parser.py Replace summary.model_copy(update={...}) with dataclasses.replace(summary, ...)
tests/copilot_usage/test_models.py Update invariant tests to expect ValueError (not ValidationError); add TestSessionSummaryFrozen class with FrozenInstanceError, __post_init__ validation, and dataclasses.replace tests
tests/copilot_usage/test_parser.py Replace model_dump() comparison with direct dataclass equality
src/copilot_usage/docs/architecture.md Update models.py description to reflect the change

Testing

  • Added TestSessionSummaryFrozen with 3 tests:
    • Field assignment raises FrozenInstanceError
    • __post_init__ raises ValueError when active_model_calls > model_calls
    • dataclasses.replace produces a new instance with updated fields
  • Updated existing invariant tests to match new ValueError exception type

Warning

⚠️ Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • astral.sh
  • pypi.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "astral.sh"
    - "pypi.org"

See Network Configuration for more information.

Generated by Issue Implementer · ● 20.7M ·

…taclass

Convert SessionSummary to @dataclasses.dataclass(frozen=True, slots=True)
to align with the coding guideline: 'Internal intermediate state uses
frozen dataclasses with slots=True'.

Changes:
- models.py: Replace BaseModel with frozen dataclass, move model_validator
  logic to __post_init__, use dataclasses.field for mutable defaults
- parser.py: Replace model_copy(update={...}) with dataclasses.replace()
- test_models.py: Update invariant tests to expect ValueError instead of
  ValidationError, add FrozenInstanceError and dataclasses.replace tests
- test_parser.py: Replace model_dump() comparison with direct equality
- architecture.md: Update models.py description

Closes #1067

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 24, 2026 07:30
@microsasa microsasa added the aw Created by agentic workflow label Apr 24, 2026
@microsasa microsasa enabled auto-merge April 24, 2026 07:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors copilot_usage’s internal session aggregation type by converting SessionSummary from a Pydantic BaseModel to a frozen, slotted stdlib dataclass, aligning internal-state modeling with the project’s “Pydantic at the boundary” direction while keeping invariant validation.

Changes:

  • Converted SessionSummary to @dataclasses.dataclass(frozen=True, slots=True) and moved invariants to __post_init__.
  • Updated parser/cache update logic to use dataclasses.replace(...) instead of Pydantic model_copy(update=...).
  • Adjusted tests and docs to reflect dataclass semantics (equality, exception types, frozen behavior).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/copilot_usage/models.py Converts SessionSummary to a frozen dataclass and enforces invariants in __post_init__; updates mutable defaults to dataclasses.field(default_factory=...).
src/copilot_usage/parser.py Updates cached-summary name refresh to use dataclasses.replace(...) for immutable SessionSummary.
tests/copilot_usage/test_models.py Updates invariant tests to ValueError and adds coverage for frozen/dataclass replace behavior.
tests/copilot_usage/test_parser.py Replaces model_dump()-based comparisons with direct dataclass equality for cached summaries.
src/copilot_usage/docs/architecture.md Updates architecture docs to reflect SessionSummary as a frozen dataclass rather than a Pydantic model.

Comment on lines +438 to +439
shutdown_cycles: list[tuple[datetime | None, SessionShutdownData]] = (
dataclasses.field(default_factory=lambda: [])
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

shutdown_cycles uses dataclasses.field(default_factory=lambda: []). Prefer default_factory=list (or default_factory=list[tuple[...]] not possible) to avoid an unnecessary lambda and keep the field factory picklable/readable; also the extra parentheses around the field(...) call can be dropped for clarity.

Suggested change
shutdown_cycles: list[tuple[datetime | None, SessionShutdownData]] = (
dataclasses.field(default_factory=lambda: [])
shutdown_cycles: list[tuple[datetime | None, SessionShutdownData]] = dataclasses.field(
default_factory=list

Copilot uses AI. Check for mistakes.
@microsasa microsasa added the aw-ci-fix-attempted CI fixer has attempted to fix CI label Apr 24, 2026
Change default_factory=dict to default_factory=lambda: {} for the
model_metrics field on SessionSummary.  Per coding guidelines,
dataclass fields must use lambda factories for mutable defaults to
avoid a known pyright strict-mode false-positive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Commit pushed: 38ea4c8

Generated by CI Fixer

@microsasa
Copy link
Copy Markdown
Owner Author

CI Fix

Root cause: model_metrics field on SessionSummary used default_factory=dict in the dataclasses.field() call. Per coding guidelines, dataclass fields must use default_factory=lambda: {} (not bare dict) to avoid a known pyright strict-mode false-positive.

Fix: Changed default_factory=dictdefault_factory=lambda: {} in src/copilot_usage/models.py.

Note: Could not run make check locally — the agent environment firewall blocks pypi.org and astral.sh, preventing tool installation. Fix is based on static analysis of the code against pyright strict-mode requirements documented in the coding guidelines.

Warning

⚠️ Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • astral.sh
  • pypi.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "astral.sh"
    - "pypi.org"

See Network Configuration for more information.

Generated by CI Fixer · ● 12.9M ·

@microsasa
Copy link
Copy Markdown
Owner Author

❌ Pipeline orchestrator: CI still failing after ci-fixer attempt. Marking as stuck for human review.

@microsasa
Copy link
Copy Markdown
Owner Author

CI fix already attempted once — stopping to prevent loops. Manual intervention needed.

Generated by CI Fixer · ● 438.3K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aw Created by agentic workflow aw-ci-fix-attempted CI fixer has attempted to fix CI aw-pr-stuck:ci

Projects

None yet

2 participants