Skip to content

refactor: adopt Python 3.14+ patterns across 39 files#232

Merged
lbliii merged 3 commits intomainfrom
lbliii/py314-pattern-audit
Apr 14, 2026
Merged

refactor: adopt Python 3.14+ patterns across 39 files#232
lbliii merged 3 commits intomainfrom
lbliii/py314-pattern-audit

Conversation

@lbliii
Copy link
Copy Markdown
Owner

@lbliii lbliii commented Apr 14, 2026

Summary

  • Freeze 38 dataclasses with frozen=True, slots=True — config classes (CLIFlags, ThemeConfig, FeatureFlags), error value objects (ErrorOccurrence, RelatedFile, TracebackConfig), provenance/cache types (FileFingerprint, ProvenanceRecord), and navigation models (BreadcrumbItem, PaginationItem, etc.)
  • Convert all 10 remaining old-style type aliases to PEP 695 type syntax (21/21 now migrated)
  • Replace 3 if/elif dispatch chains (28 branches total) with match/case in the Notion content loader and Patitas HTML renderer
  • Add 5 TypedDict definitions (AuthorDict, VersionDict, ErrorDict, TaxonomyStatsDict, EffectTracerDict) for high-value to_dict() return types

Test plan

  • Full test suite passes (3500+ tests, 0 new failures)
  • All pre-commit hooks pass (ruff, ruff format, ty, cycle check)
  • Each mutation-audit verified no post-init field assignment before freezing
  • NavNode/NavTree intentionally excluded (verified post-init mutation)

🤖 Generated with Claude Code

Freeze 38 dataclasses with frozen=True/slots=True (config, error,
provenance, cache, theme, navigation models). Convert 10 old-style
type aliases to PEP 695 syntax. Replace 3 if/elif dispatch chains
(28 branches total) with match/case in Notion loader and HTML
renderer. Add 5 TypedDict definitions for to_dict() return types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 14, 2026 14:14
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

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

This PR modernizes the codebase to align with Python 3.14+ idioms, primarily by making many dataclass models immutable/slot-based, migrating remaining legacy type aliases to PEP 695 type syntax, and adopting match/case for a few dispatch-heavy code paths. It also introduces several TypedDict shapes intended to make to_dict() return types more precise.

Changes:

  • Freeze/slot a wide set of dataclasses to reduce accidental mutation and improve memory/perf characteristics.
  • Migrate remaining type aliases to PEP 695 type statements.
  • Replace a few if/elif dispatch chains with match/case and add new TypedDict return shapes for selected to_dict() methods.

Reviewed changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
changelog.d/py314-pattern-modernization.changed.md Changelog entry describing the Python 3.14+ modernization sweep.
bengal/utils/concurrency/workers.py Add slots=True to a frozen tuning-profile dataclass.
bengal/themes/tokens.py Make palette/mascot/token dataclasses frozen=True, slots=True.
bengal/themes/swizzle.py Slot/freeze swizzle provenance record.
bengal/themes/config.py Freeze/slot theme config models (flags/appearance/icons/header/theme).
bengal/server/reload_controller.py Slot/freeze snapshot entry model.
bengal/server/asgi_app.py Convert legacy type aliases to PEP 695 type syntax.
bengal/rendering/template_functions/navigation/models.py Freeze/slot navigation model dataclasses.
bengal/rendering/plugins/cross_references.py Convert cross-version tracker alias to PEP 695 type.
bengal/rendering/highlighting/theme_resolver.py Convert literal alias to PEP 695 type.
bengal/parsing/backends/patitas/renderers/inline.py Convert inline handler alias to PEP 695 type.
bengal/parsing/backends/patitas/renderers/html.py Replace inline plain-text extraction dispatch with match/case.
bengal/parsing/backends/patitas/directives/builtins/navigation.py Convert page context getter alias to PEP 695 type.
bengal/parsing/backends/patitas/directives/builtins/misc.py Convert site context getter alias to PEP 695 type.
bengal/output/icons.py Slot/freeze icon set dataclass.
bengal/orchestration/render/output_collector_diagnostics.py Slot/freeze diagnostic value object.
bengal/orchestration/build/inputs.py Slot/freeze build input record.
bengal/health/types.py Convert status literal alias to PEP 695 type.
bengal/health/link_registry.py Slot/freeze link registry container.
bengal/errors/traceback/renderer.py Slot/freeze renderer base container.
bengal/errors/traceback/config.py Slot/freeze traceback config model.
bengal/errors/suggestions.py Slot/freeze suggestion model.
bengal/errors/session.py Slot/freeze session value objects for occurrences/patterns.
bengal/errors/handlers.py Slot/freeze context-aware help container.
bengal/errors/exceptions.py Add ErrorDict TypedDict + update to_dict() return type.
bengal/errors/dev_server.py Slot/freeze dev-server file-change record.
bengal/errors/context.py Slot/freeze related-file + debug-payload value objects.
bengal/effects/tracer.py Add EffectTracerDict TypedDict + update to_dict() return type.
bengal/core/version.py Add VersionDict TypedDict + update to_dict() return type.
bengal/core/author.py Add AuthorDict TypedDict + update to_dict() return type.
bengal/content_types/templates.py Convert page type alias to PEP 695 type.
bengal/content/sources/notion.py Replace block/property dispatch chains with match/case.
bengal/config/build_options_resolver.py Slot/freeze CLIFlags dataclass.
bengal/cli/dashboard/widgets/phase_plan.py Slot/freeze build-phase model for dashboard.
bengal/cache/utils/stats.py Add TaxonomyStatsDict TypedDict + update taxonomy stats return type.
bengal/cache/build_cache/fingerprint.py Slot/freeze file fingerprint model.
bengal/cache/build_cache/autodoc_content_cache.py Slot/freeze cached module info record.
bengal/build/provenance/types.py Slot/freeze provenance record.
bengal/build/provenance/filter.py Slot/freeze provenance filter result.
bengal/assets/manifest.py Freeze/slot asset manifest entry model.
Comments suppressed due to low confidence (2)

bengal/errors/exceptions.py:234

  • to_dict() is annotated to return ErrorDict, but the local result is explicitly typed as dict[str, Any]. That annotation defeats most of the benefit of returning a TypedDict and can also cause TypedDict incompatibility in stricter type checkers. Prefer typing result as ErrorDict (or letting the dict literal be inferred as ErrorDict) before returning it.
    def to_dict(self) -> ErrorDict:
        """
        Convert to dictionary for JSON serialization.

        Returns:
            Dictionary representation of the error
        """
        result: dict[str, Any] = {
            "type": self.__class__.__name__,
            "message": self.message,
            "code": str(self.code) if self.code else None,
            "file_path": str(self.file_path) if self.file_path else None,
            "line_number": self.line_number,
            "suggestion": self.suggestion,
            "build_phase": self.build_phase.value if self.build_phase else None,
            "severity": self.severity.value if self.severity else None,
            "related_files": [str(rf) for rf in self.related_files],
        }
        if self.debug_payload:
            result["debug_payload"] = self.debug_payload.to_dict()
        return result

bengal/cache/utils/stats.py:167

  • compute_taxonomy_stats() now returns TaxonomyStatsDict, but stats is annotated as dict[str, Any] and returned directly. This loses TypedDict checking/auto-complete and may be rejected by stricter checkers. Prefer typing stats as TaxonomyStatsDict (or constructing/returning a TypedDict-typed literal) so the function’s return annotation is actually enforced.
def compute_taxonomy_stats(
    tags: dict[str, Any],
    is_valid: Callable[[Any], bool],
    get_page_paths: Callable[[Any], list[str]],
    serialize: Callable[[Any], dict[str, Any]] | None = None,
) -> TaxonomyStatsDict:
    """
    Compute statistics for taxonomy-type caches.

    Args:
        tags: Dictionary of tag_slug → TagEntry
        is_valid: Function to check if tag is valid
        get_page_paths: Function to get page_paths from tag entry
        serialize: Optional function to serialize entry for size calculation

    Returns:
        Dictionary with taxonomy-specific stats
    """
    valid = sum(1 for e in tags.values() if is_valid(e))
    invalid = len(tags) - valid

    # Count unique pages and page-tag pairs
    unique_pages: set[str] = set()
    total_page_tag_pairs = 0

    for entry in tags.values():
        if is_valid(entry):
            paths = get_page_paths(entry)
            total_page_tag_pairs += len(paths)
            unique_pages.update(paths)

    avg_tags_per_page = 0.0
    if unique_pages:
        avg_tags_per_page = total_page_tag_pairs / len(unique_pages)

    stats: dict[str, Any] = {
        "total_tags": len(tags),
        "valid_tags": valid,
        "invalid_tags": invalid,
        "total_unique_pages": len(unique_pages),
        "total_page_tag_pairs": total_page_tag_pairs,
        "avg_tags_per_page": round(avg_tags_per_page, 2),
    }

    if serialize is not None:
        try:
            size = len(json.dumps([serialize(e) for e in tags.values()]))
            stats["cache_size_bytes"] = size
        except TypeError, ValueError:
            stats["cache_size_bytes"] = 0

    return stats

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread bengal/errors/exceptions.py Outdated
Comment on lines +83 to +96
class ErrorDict(TypedDict, total=False):
"""Serialized form of BengalError for JSON output."""

type: str
message: str
code: str | None
file_path: str | None
line_number: int | None
suggestion: str | None
build_phase: str | None
severity: str | None
related_files: list[str]
debug_payload: dict[str, Any]

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

ErrorDict is declared as TypedDict(total=False), which makes every key optional even though BengalError.to_dict() always emits most of these keys (with None values when absent). Consider making the always-present keys required and using typing.NotRequired[...] only for conditionally present keys (e.g. debug_payload) so callers get meaningful type guarantees.

Copilot uses AI. Check for mistakes.
Comment thread bengal/cache/utils/stats.py Outdated
Comment on lines +28 to +38
class TaxonomyStatsDict(TypedDict, total=False):
"""Statistics for taxonomy-type caches."""

total_tags: int
valid_tags: int
invalid_tags: int
total_unique_pages: int
total_page_tag_pairs: int
avg_tags_per_page: float
cache_size_bytes: int

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

TaxonomyStatsDict is defined with total=False, but compute_taxonomy_stats() always populates the core fields (total_tags, valid_tags, etc.) and only conditionally includes cache_size_bytes. Making all keys optional reduces type precision; consider using a regular TypedDict with NotRequired[int] for the optional cache_size_bytes key (pattern already used elsewhere in the repo).

Copilot uses AI. Check for mistakes.
Address PR review: ErrorDict and TaxonomyStatsDict now have required
keys by default, with NotRequired only for conditionally-present keys
(debug_payload, cache_size_bytes).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lbliii lbliii merged commit 582733c into main Apr 14, 2026
22 checks passed
@lbliii lbliii deleted the lbliii/py314-pattern-audit branch April 14, 2026 14: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.

2 participants