diff --git a/bengal/assets/manifest.py b/bengal/assets/manifest.py index c99579556..fcfc93a65 100644 --- a/bengal/assets/manifest.py +++ b/bengal/assets/manifest.py @@ -81,7 +81,7 @@ def _isoformat(timestamp: float | None) -> str | None: return datetime.fromtimestamp(timestamp, tz=UTC).isoformat().replace("+00:00", "Z") -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class AssetManifestEntry: """ Manifest entry for a single logical asset. diff --git a/bengal/build/provenance/filter.py b/bengal/build/provenance/filter.py index da15207d4..ccf1413f3 100644 --- a/bengal/build/provenance/filter.py +++ b/bengal/build/provenance/filter.py @@ -43,7 +43,7 @@ from bengal.protocols.core import PageLike -@dataclass +@dataclass(frozen=True, slots=True) class ProvenanceFilterResult: """Result of provenance-based filtering.""" diff --git a/bengal/build/provenance/types.py b/bengal/build/provenance/types.py index 448cc2c38..2361ee984 100644 --- a/bengal/build/provenance/types.py +++ b/bengal/build/provenance/types.py @@ -122,7 +122,7 @@ def summary(self) -> str: return f"Provenance({', '.join(parts)}) → {self.combined_hash}" -@dataclass +@dataclass(frozen=True, slots=True) class ProvenanceRecord: """ Stored provenance record with metadata. diff --git a/bengal/cache/build_cache/autodoc_content_cache.py b/bengal/cache/build_cache/autodoc_content_cache.py index 66bc2d0cb..1c68e3f42 100644 --- a/bengal/cache/build_cache/autodoc_content_cache.py +++ b/bengal/cache/build_cache/autodoc_content_cache.py @@ -32,7 +32,7 @@ logger = get_logger(__name__) -@dataclass +@dataclass(frozen=True, slots=True) class CachedModuleInfo: """Cached parsed module data. diff --git a/bengal/cache/build_cache/fingerprint.py b/bengal/cache/build_cache/fingerprint.py index 38493592e..7da243101 100644 --- a/bengal/cache/build_cache/fingerprint.py +++ b/bengal/cache/build_cache/fingerprint.py @@ -33,7 +33,7 @@ logger = get_logger(__name__) -@dataclass +@dataclass(frozen=True, slots=True) class FileFingerprint: """ Fast file change detection using mtime + size, with optional hash verification. diff --git a/bengal/cache/utils/stats.py b/bengal/cache/utils/stats.py index 75ae7472f..ad6297b8e 100644 --- a/bengal/cache/utils/stats.py +++ b/bengal/cache/utils/stats.py @@ -19,12 +19,24 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, NotRequired, TypedDict if TYPE_CHECKING: from collections.abc import Callable +class TaxonomyStatsDict(TypedDict): + """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: NotRequired[int] + + def compute_validity_stats[V]( entries: dict[str, V], is_valid: Callable[[V], bool], @@ -106,7 +118,7 @@ def compute_taxonomy_stats( is_valid: Callable[[Any], bool], get_page_paths: Callable[[Any], list[str]], serialize: Callable[[Any], dict[str, Any]] | None = None, -) -> dict[str, Any]: +) -> TaxonomyStatsDict: """ Compute statistics for taxonomy-type caches. diff --git a/bengal/cli/dashboard/widgets/phase_plan.py b/bengal/cli/dashboard/widgets/phase_plan.py index ae757b795..a1c8842ce 100644 --- a/bengal/cli/dashboard/widgets/phase_plan.py +++ b/bengal/cli/dashboard/widgets/phase_plan.py @@ -29,7 +29,7 @@ from textual.app import ComposeResult -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class BuildPhase: """ Represents a build phase with status. diff --git a/bengal/config/build_options_resolver.py b/bengal/config/build_options_resolver.py index d8b6dface..96957fa13 100644 --- a/bengal/config/build_options_resolver.py +++ b/bengal/config/build_options_resolver.py @@ -41,7 +41,7 @@ from bengal.orchestration.build.options import BuildOptions -@dataclass +@dataclass(frozen=True, slots=True) class CLIFlags: """ Flags explicitly passed via CLI (None = not passed). diff --git a/bengal/content/sources/notion.py b/bengal/content/sources/notion.py index 6088c0bf9..51399e03f 100644 --- a/bengal/content/sources/notion.py +++ b/bengal/content/sources/notion.py @@ -390,77 +390,78 @@ def _blocks_to_markdown(self, blocks: list[dict[str, Any]]) -> str: block_type = block.get("type") block_data = block.get(str(block_type) if block_type else "", {}) - if block_type == "paragraph": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(text) - - elif block_type == "heading_1": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(f"# {text}") - - elif block_type == "heading_2": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(f"## {text}") - - elif block_type == "heading_3": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(f"### {text}") - - elif block_type == "bulleted_list_item": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(f"- {text}") - - elif block_type == "numbered_list_item": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(f"1. {text}") - - elif block_type == "to_do": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - checked = "x" if block_data.get("checked") else " " - lines.append(f"- [{checked}] {text}") - - elif block_type == "toggle": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(f"
{text}") - # Note: nested blocks not handled in this simple implementation - lines.append("
") - - elif block_type == "code": - code = self._rich_text_to_md(block_data.get("rich_text", [])) - lang = block_data.get("language", "") - lines.append(f"```{lang}") - lines.append(code) - lines.append("```") - - elif block_type == "quote": - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(f"> {text}") - - elif block_type == "callout": - icon = block_data.get("icon", {}).get("emoji", "💡") - text = self._rich_text_to_md(block_data.get("rich_text", [])) - lines.append(f"> {icon} {text}") - - elif block_type == "divider": - lines.append("---") - - elif block_type == "image": - image_data = block_data - url = image_data.get("external", {}).get("url") or image_data.get("file", {}).get( - "url" - ) - caption = self._rich_text_to_md(image_data.get("caption", [])) - if url: - lines.append(f"![{caption}]({url})") + match block_type: + case "paragraph": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(text) + + case "heading_1": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(f"# {text}") + + case "heading_2": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(f"## {text}") + + case "heading_3": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(f"### {text}") + + case "bulleted_list_item": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(f"- {text}") + + case "numbered_list_item": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(f"1. {text}") + + case "to_do": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + checked = "x" if block_data.get("checked") else " " + lines.append(f"- [{checked}] {text}") + + case "toggle": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(f"
{text}") + # Note: nested blocks not handled in this simple implementation + lines.append("
") + + case "code": + code = self._rich_text_to_md(block_data.get("rich_text", [])) + lang = block_data.get("language", "") + lines.append(f"```{lang}") + lines.append(code) + lines.append("```") + + case "quote": + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(f"> {text}") + + case "callout": + icon = block_data.get("icon", {}).get("emoji", "💡") + text = self._rich_text_to_md(block_data.get("rich_text", [])) + lines.append(f"> {icon} {text}") + + case "divider": + lines.append("---") + + case "image": + image_data = block_data + url = image_data.get("external", {}).get("url") or image_data.get( + "file", {} + ).get("url") + caption = self._rich_text_to_md(image_data.get("caption", [])) + if url: + lines.append(f"![{caption}]({url})") - elif block_type == "bookmark": - url = block_data.get("url", "") - caption = self._rich_text_to_md(block_data.get("caption", [])) - lines.append(f"[{caption or url}]({url})") + case "bookmark": + url = block_data.get("url", "") + caption = self._rich_text_to_md(block_data.get("caption", [])) + lines.append(f"[{caption or url}]({url})") - elif block_type == "equation": - expression = block_data.get("expression", "") - lines.append(f"$$\n{expression}\n$$") + case "equation": + expression = block_data.get("expression", "") + lines.append(f"$$\n{expression}\n$$") # Add blank line between blocks lines.append("") @@ -524,56 +525,57 @@ def _extract_properties(self, page: dict[str, Any]) -> dict[str, Any]: prop = properties[notion_prop] prop_type = prop.get("type") - if prop_type == "title": - frontmatter[fm_key] = self._rich_text_to_md(prop.get("title", [])) + match prop_type: + case "title": + frontmatter[fm_key] = self._rich_text_to_md(prop.get("title", [])) - elif prop_type == "rich_text": - frontmatter[fm_key] = self._rich_text_to_md(prop.get("rich_text", [])) + case "rich_text": + frontmatter[fm_key] = self._rich_text_to_md(prop.get("rich_text", [])) - elif prop_type == "date": - date_obj = prop.get("date") - if date_obj: - frontmatter[fm_key] = date_obj.get("start") + case "date": + date_obj = prop.get("date") + if date_obj: + frontmatter[fm_key] = date_obj.get("start") - elif prop_type == "multi_select": - frontmatter[fm_key] = [opt["name"] for opt in prop.get("multi_select", [])] + case "multi_select": + frontmatter[fm_key] = [opt["name"] for opt in prop.get("multi_select", [])] - elif prop_type == "select": - select_obj = prop.get("select") - if select_obj: - frontmatter[fm_key] = select_obj.get("name") + case "select": + select_obj = prop.get("select") + if select_obj: + frontmatter[fm_key] = select_obj.get("name") - elif prop_type == "checkbox": - frontmatter[fm_key] = prop.get("checkbox", False) + case "checkbox": + frontmatter[fm_key] = prop.get("checkbox", False) - elif prop_type == "number": - frontmatter[fm_key] = prop.get("number") + case "number": + frontmatter[fm_key] = prop.get("number") - elif prop_type == "url": - frontmatter[fm_key] = prop.get("url") + case "url": + frontmatter[fm_key] = prop.get("url") - elif prop_type == "email": - frontmatter[fm_key] = prop.get("email") + case "email": + frontmatter[fm_key] = prop.get("email") - elif prop_type == "phone_number": - frontmatter[fm_key] = prop.get("phone_number") + case "phone_number": + frontmatter[fm_key] = prop.get("phone_number") - elif prop_type == "people": - people = prop.get("people", []) - frontmatter[fm_key] = [p.get("name", p.get("id")) for p in people] + case "people": + people = prop.get("people", []) + frontmatter[fm_key] = [p.get("name", p.get("id")) for p in people] - elif prop_type == "files": - files = prop.get("files", []) - urls = [] - for f in files: - url = f.get("external", {}).get("url") or f.get("file", {}).get("url") - if url: - urls.append(url) - frontmatter[fm_key] = urls + case "files": + files = prop.get("files", []) + urls = [] + for f in files: + url = f.get("external", {}).get("url") or f.get("file", {}).get("url") + if url: + urls.append(url) + frontmatter[fm_key] = urls - elif prop_type == "status": - status_obj = prop.get("status") - if status_obj: - frontmatter[fm_key] = status_obj.get("name") + case "status": + status_obj = prop.get("status") + if status_obj: + frontmatter[fm_key] = status_obj.get("name") return frontmatter diff --git a/bengal/content_types/templates.py b/bengal/content_types/templates.py index 0891f2995..d94423d19 100644 --- a/bengal/content_types/templates.py +++ b/bengal/content_types/templates.py @@ -23,7 +23,7 @@ logger = get_logger(__name__) #: Page type literals for template resolution -PageType = Literal["home", "list", "single"] +type PageType = Literal["home", "list", "single"] def classify_page(page: PageLike) -> PageType: diff --git a/bengal/core/author.py b/bengal/core/author.py index acba36a7c..8eab29b2f 100644 --- a/bengal/core/author.py +++ b/bengal/core/author.py @@ -48,7 +48,18 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any +from typing import Any, TypedDict + + +class AuthorDict(TypedDict): + """Serialized form of Author for template context.""" + + name: str + email: str + bio: str + avatar: str + url: str + social: dict[str, str] @dataclass(frozen=True, slots=True) @@ -129,7 +140,7 @@ def mastodon(self) -> str: """Shortcut for Mastodon handle.""" return self.social.get("mastodon", "") - def to_dict(self) -> dict[str, Any]: + def to_dict(self) -> AuthorDict: """ Convert to dictionary for template context or serialization. diff --git a/bengal/core/version.py b/bengal/core/version.py index ac0e13b2e..378f476cd 100644 --- a/bengal/core/version.py +++ b/bengal/core/version.py @@ -62,7 +62,7 @@ import threading from dataclasses import dataclass, field from pathlib import Path -from typing import Any +from typing import Any, TypedDict @dataclass @@ -158,6 +158,18 @@ class GitVersionConfig: parallel_builds: int = 4 +class VersionDict(TypedDict): + """Serialized form of Version for template context.""" + + id: str + label: str + latest: bool + deprecated: bool + url_prefix: str + release_date: str | None + end_of_life: str | None + + @dataclass class Version: """ @@ -219,7 +231,7 @@ def url_prefix(self) -> str: return "" return f"/{self.id}" - def to_dict(self) -> dict[str, Any]: + def to_dict(self) -> VersionDict: """ Convert to dictionary for template context. diff --git a/bengal/effects/tracer.py b/bengal/effects/tracer.py index af6e0bd73..d5ec65fca 100644 --- a/bengal/effects/tracer.py +++ b/bengal/effects/tracer.py @@ -20,10 +20,18 @@ from collections import defaultdict from contextlib import suppress from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict from bengal.effects.effect import Effect + +class EffectTracerDict(TypedDict): + """Serialized form of EffectTracer state.""" + + effects: list[dict[str, Any]] + fingerprints: dict[str, str] + + if TYPE_CHECKING: from bengal.snapshots.types import SiteSnapshot @@ -305,7 +313,7 @@ def get_changed_files(self, root_path: Path) -> set[Path]: # --- Persistence --- - def to_dict(self) -> dict[str, Any]: + def to_dict(self) -> EffectTracerDict: """Serialize tracer state to dict.""" with self._lock: return { diff --git a/bengal/errors/context.py b/bengal/errors/context.py index 54dd31896..7cf5292d4 100644 --- a/bengal/errors/context.py +++ b/bengal/errors/context.py @@ -148,7 +148,7 @@ def should_aggregate(self) -> bool: return self in (ErrorSeverity.ERROR, ErrorSeverity.WARNING) -@dataclass +@dataclass(frozen=True, slots=True) class RelatedFile: """ A file related to an error for debugging context. @@ -171,7 +171,7 @@ def __str__(self) -> str: return f"{self.role}: {path_str}" -@dataclass +@dataclass(frozen=True, slots=True) class ErrorDebugPayload: """ Machine-parseable debug context for AI troubleshooting. diff --git a/bengal/errors/dev_server.py b/bengal/errors/dev_server.py index 473d55a8c..26a063af0 100644 --- a/bengal/errors/dev_server.py +++ b/bengal/errors/dev_server.py @@ -77,7 +77,7 @@ ) -@dataclass +@dataclass(frozen=True, slots=True) class FileChange: """ Record of a file change that may have caused an error. diff --git a/bengal/errors/exceptions.py b/bengal/errors/exceptions.py index 4e36b99f9..945e1f918 100644 --- a/bengal/errors/exceptions.py +++ b/bengal/errors/exceptions.py @@ -68,7 +68,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, NotRequired, TypedDict if TYPE_CHECKING: from bengal.errors.codes import ErrorCode @@ -80,6 +80,21 @@ from bengal.protocols.build import BuildPhase +class ErrorDict(TypedDict): + """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: NotRequired[dict[str, Any]] + + class BengalError(Exception): """ Base exception for all Bengal errors. @@ -196,7 +211,7 @@ def _format_message(self) -> str: return "\n".join(parts) - def to_dict(self) -> dict[str, Any]: + def to_dict(self) -> ErrorDict: """ Convert to dictionary for JSON serialization. diff --git a/bengal/errors/handlers.py b/bengal/errors/handlers.py index 687a7d305..462e36536 100644 --- a/bengal/errors/handlers.py +++ b/bengal/errors/handlers.py @@ -64,7 +64,7 @@ logger = get_logger(__name__) -@dataclass +@dataclass(frozen=True, slots=True) class ContextAwareHelp: """ Container for context-aware error help information. diff --git a/bengal/errors/session.py b/bengal/errors/session.py index 9f15c93a6..789e492ba 100644 --- a/bengal/errors/session.py +++ b/bengal/errors/session.py @@ -81,7 +81,7 @@ from bengal.errors.codes import ErrorCode -@dataclass +@dataclass(frozen=True, slots=True) class ErrorOccurrence: """ Record of a single error occurrence. @@ -104,7 +104,7 @@ class ErrorOccurrence: build_phase: str | None = None -@dataclass +@dataclass(frozen=True, slots=True) class ErrorPattern: """ Aggregated pattern for similar errors. diff --git a/bengal/errors/suggestions.py b/bengal/errors/suggestions.py index d93acee10..91bd31416 100644 --- a/bengal/errors/suggestions.py +++ b/bengal/errors/suggestions.py @@ -83,7 +83,7 @@ from typing import Any -@dataclass +@dataclass(frozen=True, slots=True) class ActionableSuggestion: """ A structured, actionable suggestion for fixing an error. diff --git a/bengal/errors/traceback/config.py b/bengal/errors/traceback/config.py index 10eb732e1..3f98f90b7 100644 --- a/bengal/errors/traceback/config.py +++ b/bengal/errors/traceback/config.py @@ -90,7 +90,7 @@ class TracebackStyle(Enum): from bengal.errors.traceback.renderer import TracebackRenderer -@dataclass +@dataclass(frozen=True, slots=True) class TracebackConfig: """ Configuration for traceback display and Rich installation. diff --git a/bengal/errors/traceback/renderer.py b/bengal/errors/traceback/renderer.py index 13f233943..6e01d4f5c 100644 --- a/bengal/errors/traceback/renderer.py +++ b/bengal/errors/traceback/renderer.py @@ -64,7 +64,7 @@ logger = get_logger(__name__) -@dataclass +@dataclass(frozen=True, slots=True) class TracebackRenderer: """ Base class for traceback renderers. diff --git a/bengal/health/link_registry.py b/bengal/health/link_registry.py index 8ccf51a28..06a56cf2c 100644 --- a/bengal/health/link_registry.py +++ b/bengal/health/link_registry.py @@ -30,7 +30,7 @@ from bengal.protocols import SiteLike -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class LinkRegistry: """ Immutable registry of all valid link targets in a site. diff --git a/bengal/health/types.py b/bengal/health/types.py index 79527db84..90aeeb2bc 100644 --- a/bengal/health/types.py +++ b/bengal/health/types.py @@ -27,7 +27,7 @@ # ============================================================================= # Status literal type matching CheckStatus enum -CheckStatusLiteral = Literal["success", "info", "suggestion", "warning", "error"] +type CheckStatusLiteral = Literal["success", "info", "suggestion", "warning", "error"] class CheckResultDict(TypedDict, total=False): diff --git a/bengal/orchestration/build/inputs.py b/bengal/orchestration/build/inputs.py index d5cc4eca9..d2ea6ea92 100644 --- a/bengal/orchestration/build/inputs.py +++ b/bengal/orchestration/build/inputs.py @@ -22,7 +22,7 @@ from bengal.server.build_executor import BuildRequest -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class BuildInput: """ Complete, serializable record of all inputs to a build. diff --git a/bengal/orchestration/render/output_collector_diagnostics.py b/bengal/orchestration/render/output_collector_diagnostics.py index e5b5ec38c..1ef8dff34 100644 --- a/bengal/orchestration/render/output_collector_diagnostics.py +++ b/bengal/orchestration/render/output_collector_diagnostics.py @@ -48,7 +48,7 @@ class OutputCollectorSource(Enum): } -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class OutputCollectorDiagnostic: """Structured diagnostic for output_collector propagation failure.""" diff --git a/bengal/output/icons.py b/bengal/output/icons.py index b4d836fc4..796685220 100644 --- a/bengal/output/icons.py +++ b/bengal/output/icons.py @@ -25,7 +25,7 @@ from dataclasses import dataclass -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class IconSet: """ Icon set for CLI output. diff --git a/bengal/parsing/backends/patitas/directives/builtins/misc.py b/bengal/parsing/backends/patitas/directives/builtins/misc.py index 5dbf61956..d61e0be8f 100644 --- a/bengal/parsing/backends/patitas/directives/builtins/misc.py +++ b/bengal/parsing/backends/patitas/directives/builtins/misc.py @@ -52,7 +52,7 @@ class SiteContext(Protocol): current_language: str | None -SiteContextGetter = Callable[[], tuple[SiteContext | None, Any | None]] +type SiteContextGetter = Callable[[], tuple[SiteContext | None, Any | None]] # ============================================================================= diff --git a/bengal/parsing/backends/patitas/directives/builtins/navigation.py b/bengal/parsing/backends/patitas/directives/builtins/navigation.py index c524c6629..d758e5071 100644 --- a/bengal/parsing/backends/patitas/directives/builtins/navigation.py +++ b/bengal/parsing/backends/patitas/directives/builtins/navigation.py @@ -62,7 +62,7 @@ class PageContext(Protocol): # Type alias for page context getter -PageContextGetter = Callable[[], PageContext | None] +type PageContextGetter = Callable[[], PageContext | None] # ============================================================================= diff --git a/bengal/parsing/backends/patitas/renderers/html.py b/bengal/parsing/backends/patitas/renderers/html.py index cabffab3c..4d55459de 100644 --- a/bengal/parsing/backends/patitas/renderers/html.py +++ b/bengal/parsing/backends/patitas/renderers/html.py @@ -343,19 +343,20 @@ def _extract_plain_text(self, children: Sequence[Inline]) -> str: """ parts: list[str] = [] for child in children: - if isinstance(child, Text): - content = child.content - # Apply text transformer if present (e.g., variable substitution) - if self._text_transformer: - content = self._text_transformer(content) - parts.append(content) - elif isinstance(child, CodeSpan): - parts.append(child.code) - elif isinstance(child, (Emphasis, Strong, Strikethrough, Link)): - parts.append(self._extract_plain_text(child.children)) - elif isinstance(child, Math): - parts.append(child.content) - # Skip: Image, LineBreak, SoftBreak, HtmlInline, Role, FootnoteRef + match child: + case Text(): + content = child.content + # Apply text transformer if present (e.g., variable substitution) + if self._text_transformer: + content = self._text_transformer(content) + parts.append(content) + case CodeSpan(): + parts.append(child.code) + case Emphasis() | Strong() | Strikethrough() | Link(): + parts.append(self._extract_plain_text(child.children)) + case Math(): + parts.append(child.content) + # Skip: Image, LineBreak, SoftBreak, HtmlInline, Role, FootnoteRef return "".join(parts) def _get_unique_slug(self, text: str) -> str: diff --git a/bengal/parsing/backends/patitas/renderers/inline.py b/bengal/parsing/backends/patitas/renderers/inline.py index d26dfd937..4268e9986 100644 --- a/bengal/parsing/backends/patitas/renderers/inline.py +++ b/bengal/parsing/backends/patitas/renderers/inline.py @@ -37,7 +37,7 @@ ) # Type alias for inline render handler -InlineHandler = Callable[[Any, StringBuilder, Callable], None] +type InlineHandler = Callable[[Any, StringBuilder, Callable], None] def render_text(node: Text, sb: StringBuilder, render_children: Callable) -> None: diff --git a/bengal/rendering/highlighting/theme_resolver.py b/bengal/rendering/highlighting/theme_resolver.py index a5a767ed3..bf899355e 100644 --- a/bengal/rendering/highlighting/theme_resolver.py +++ b/bengal/rendering/highlighting/theme_resolver.py @@ -28,7 +28,7 @@ ] # Type alias for CSS class style -CssClassStyle = Literal["semantic", "pygments"] +type CssClassStyle = Literal["semantic", "pygments"] # Mapping from Bengal site palettes to Rosettes syntax themes. # When syntax_highlighting.theme is "auto", we look up the site's diff --git a/bengal/rendering/plugins/cross_references.py b/bengal/rendering/plugins/cross_references.py index 80a819026..7bf4787e5 100644 --- a/bengal/rendering/plugins/cross_references.py +++ b/bengal/rendering/plugins/cross_references.py @@ -36,7 +36,7 @@ logger = get_logger(__name__) # Type alias for cross-version dependency callback -CrossVersionTracker = Callable[[Path, str, str], None] +type CrossVersionTracker = Callable[[Path, str, str], None] __all__ = ["CrossReferencePlugin"] diff --git a/bengal/rendering/template_functions/navigation/models.py b/bengal/rendering/template_functions/navigation/models.py index f31f3e8d2..bac310aa8 100644 --- a/bengal/rendering/template_functions/navigation/models.py +++ b/bengal/rendering/template_functions/navigation/models.py @@ -30,7 +30,7 @@ from typing import Any -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class BreadcrumbItem: """ Single breadcrumb in navigation trail. @@ -68,7 +68,7 @@ def get(self, key: str, default: Any = None) -> Any: return getattr(self, key, default) -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class PaginationItem: """ Single page in pagination. @@ -110,7 +110,7 @@ def get(self, key: str, default: Any = None) -> Any: return getattr(self, key, default) -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class PaginationInfo: """ Complete pagination data structure. @@ -149,7 +149,7 @@ def get(self, key: str, default: Any = None) -> Any: return getattr(self, key, default) -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class TocGroupItem: """ Grouped TOC item for collapsible sections. @@ -196,7 +196,7 @@ def get(self, key: str, default: Any = None) -> Any: return getattr(self, key, default) -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class AutoNavItem: """ Auto-discovered navigation item. diff --git a/bengal/server/asgi_app.py b/bengal/server/asgi_app.py index f37cb40cf..bcefb823f 100644 --- a/bengal/server/asgi_app.py +++ b/bengal/server/asgi_app.py @@ -26,13 +26,13 @@ from pathlib import Path # ASGI app type: async (scope, receive, send) -> None -ASGIApp = Callable[..., Any] +type ASGIApp = Callable[..., Any] # Type for request callback: (method, path, status, duration_ms) -> None -RequestCallback = Callable[[str, str, int, float], None] +type RequestCallback = Callable[[str, str, int, float], None] # Getter for lazy callback resolution (set after backend creation) -RequestCallbackGetter = Callable[[], RequestCallback | None] +type RequestCallbackGetter = Callable[[], RequestCallback | None] def create_bengal_dev_app( diff --git a/bengal/server/reload_controller.py b/bengal/server/reload_controller.py index 391ded31a..b7bc82bd9 100644 --- a/bengal/server/reload_controller.py +++ b/bengal/server/reload_controller.py @@ -86,7 +86,7 @@ class HashEntry: digest: str -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class SnapshotEntry: """ Immutable file metadata for snapshot comparison. diff --git a/bengal/themes/config.py b/bengal/themes/config.py index 6c266bca4..812f0fd8b 100644 --- a/bengal/themes/config.py +++ b/bengal/themes/config.py @@ -49,7 +49,7 @@ logger = get_logger(__name__) -@dataclass +@dataclass(frozen=True, slots=True) class FeatureFlags: """ Feature flags organized by category. @@ -142,7 +142,7 @@ def from_dict(cls, data: dict[str, Any]) -> FeatureFlags: ) -@dataclass +@dataclass(frozen=True, slots=True) class AppearanceConfig: """ Appearance configuration for theme mode and color palette. @@ -191,7 +191,7 @@ def from_dict(cls, data: dict[str, Any]) -> AppearanceConfig: ) -@dataclass +@dataclass(frozen=True, slots=True) class IconConfig: """ Icon library configuration with semantic aliases. @@ -254,7 +254,7 @@ def to_dict(self) -> dict[str, Any]: } -@dataclass +@dataclass(frozen=True, slots=True) class HeaderConfig: """ Header layout and behavior configuration. @@ -316,7 +316,7 @@ def to_dict(self) -> dict[str, Any]: } -@dataclass +@dataclass(frozen=True, slots=True) class ThemeConfig: """ Complete theme configuration loaded from theme.yaml. diff --git a/bengal/themes/swizzle.py b/bengal/themes/swizzle.py index aaacdc62c..70618236f 100644 --- a/bengal/themes/swizzle.py +++ b/bengal/themes/swizzle.py @@ -85,7 +85,7 @@ class ModificationStatus(Enum): CHECKSUM_ERROR = "checksum_error" # Could not compute checksum -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class SwizzleRecord: """ Immutable record of a swizzled template's provenance. diff --git a/bengal/themes/tokens.py b/bengal/themes/tokens.py index ee49e59dd..7173be11c 100644 --- a/bengal/themes/tokens.py +++ b/bengal/themes/tokens.py @@ -98,7 +98,7 @@ def surface(self) -> str: ... def background(self) -> str: ... -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class BengalPalette: """ Bengal color palette with semantic color tokens. @@ -166,7 +166,7 @@ class BengalPalette: BENGAL_PALETTE = BengalPalette() -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class BengalMascots: """ Bengal brand mascots and status icons for terminal output. @@ -235,7 +235,7 @@ class BengalMascots: BENGAL_MASCOT = BengalMascots() -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class PaletteVariant: """ Named color palette variant for theming. diff --git a/bengal/utils/concurrency/workers.py b/bengal/utils/concurrency/workers.py index ff22a35a8..a69639287 100644 --- a/bengal/utils/concurrency/workers.py +++ b/bengal/utils/concurrency/workers.py @@ -77,7 +77,7 @@ class Environment(Enum): PRODUCTION = "production" -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class WorkloadProfile: """Tuning profile for a workload type. diff --git a/changelog.d/py314-pattern-modernization.changed.md b/changelog.d/py314-pattern-modernization.changed.md new file mode 100644 index 000000000..4372bdd65 --- /dev/null +++ b/changelog.d/py314-pattern-modernization.changed.md @@ -0,0 +1 @@ +Adopt Python 3.14+ patterns: freeze 38 dataclasses with `frozen=True, slots=True`, convert 10 type aliases to PEP 695 `type` syntax, replace 3 if/elif dispatch chains with `match/case`, and add 5 TypedDict definitions for `to_dict()` return types.