refactor: adopt Python 3.14+ patterns across 39 files#232
Conversation
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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
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
typestatements. - Replace a few if/elif dispatch chains with
match/caseand add newTypedDictreturn shapes for selectedto_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 returnErrorDict, but the localresultis explicitly typed asdict[str, Any]. That annotation defeats most of the benefit of returning aTypedDictand can also cause TypedDict incompatibility in stricter type checkers. Prefer typingresultasErrorDict(or letting the dict literal be inferred asErrorDict) 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 returnsTaxonomyStatsDict, butstatsis annotated asdict[str, Any]and returned directly. This loses TypedDict checking/auto-complete and may be rejected by stricter checkers. Prefer typingstatsasTaxonomyStatsDict(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.
| 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] | ||
|
|
There was a problem hiding this comment.
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.
| 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 | ||
|
|
There was a problem hiding this comment.
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).
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>
Summary
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.)typesyntax (21/21 now migrated)match/casein the Notion content loader and Patitas HTML rendererAuthorDict,VersionDict,ErrorDict,TaxonomyStatsDict,EffectTracerDict) for high-valueto_dict()return typesTest plan
🤖 Generated with Claude Code