aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-15 12:04:21 +0530
committernatsuoto <[email protected]>2026-07-15 12:04:21 +0530
commite1fc8c45a0534722b2c8d3a1fa94876a25d57901 (patch)
treec17bcae990fe6ef1cd670df9106948402760896d
parent089004d6d63c06d11e195484e767e5fe30b5cfcb (diff)
downloadedify-e1fc8c45a0534722b2c8d3a1fa94876a25d57901.tar.xz
edify-e1fc8c45a0534722b2c8d3a1fa94876a25d57901.zip
refactor(typing): make edify pass mypy --strict and pyright --strict end to end
-rw-r--r--edify/builder/core.py15
-rw-r--r--edify/builder/diagnose.py16
-rw-r--r--edify/builder/merge.py44
-rw-r--r--edify/builder/types/protocol.py4
-rw-r--r--edify/compile/dispatch.py6
-rw-r--r--edify/compile/fuse.py1
-rw-r--r--edify/errors/context.py18
-rw-r--r--edify/introspect/explain.py2
-rw-r--r--edify/introspect/graphviz.py5
-rw-r--r--edify/pattern/factories/assertions.py3
-rw-r--r--edify/pattern/factories/groups.py3
-rw-r--r--edify/result/regex.py3
-rw-r--r--edify/serialize/kinds.py7
-rw-r--r--edify/serialize/load.py11
-rw-r--r--tests/builder/repr.test.py9
15 files changed, 101 insertions, 46 deletions
diff --git a/edify/builder/core.py b/edify/builder/core.py
index 6b3ac46..fca2a76 100644
--- a/edify/builder/core.py
+++ b/edify/builder/core.py
@@ -5,11 +5,13 @@ from __future__ import annotations
from typing import Self
from edify.builder.diagnose import diagnose_unfinished
+from edify.builder.types.protocol import BuilderProtocol
from edify.builder.types.state import BuilderState
from edify.errors.comparison import (
CannotCompareUnfinishedBuilderError,
CannotHashUnfinishedBuilderError,
)
+from edify.errors.formatting import Problem
from edify.errors.quantifier import DanglingQuantifierError
from edify.errors.structure import CannotCallSubexpressionError
from edify.result.regex import Regex
@@ -17,7 +19,7 @@ from edify.result.regex import Regex
_UNCLOSED_FRAME_MARKER = "<unclosed>"
-class BuilderCore:
+class BuilderCore(BuilderProtocol):
"""Holds the immutable :class:`BuilderState` and clones it on chain steps."""
_state: BuilderState
@@ -54,14 +56,14 @@ class BuilderCore:
def __repr__(self) -> str:
"""Return ``<ClassName 'pattern-so-far'>`` for interactive display."""
- rendered = _render_or_marker(self)
+ rendered = _rendered_or_unclosed_marker(self)
return f"<{type(self).__name__} {rendered!r}>"
def __eq__(self, other: object) -> bool:
"""Return True when ``other`` is a builder with the same emitted pattern and flags."""
if not isinstance(other, BuilderCore):
return NotImplemented
- problems: list = []
+ problems: list[Problem] = []
left_problem = diagnose_unfinished(self._state, "left operand")
right_problem = diagnose_unfinished(other._state, "right operand")
if left_problem is not None:
@@ -83,12 +85,9 @@ class BuilderCore:
return hash((source, self._state.flags))
-def _render_or_marker(builder: BuilderCore) -> str:
+def _rendered_or_unclosed_marker(builder: BuilderCore) -> str:
"""Return the emitted regex string, or a placeholder when frames are unclosed."""
- to_regex_string = getattr(builder, "to_regex_string", None)
- if to_regex_string is None:
- return _UNCLOSED_FRAME_MARKER
try:
- return to_regex_string()
+ return builder.to_regex_string()
except (CannotCallSubexpressionError, DanglingQuantifierError):
return _UNCLOSED_FRAME_MARKER
diff --git a/edify/builder/diagnose.py b/edify/builder/diagnose.py
index a08a7ed..4802145 100644
--- a/edify/builder/diagnose.py
+++ b/edify/builder/diagnose.py
@@ -13,10 +13,18 @@ from edify.elements.types.groups import (
AssertNotBehindElement,
GroupElement,
)
+from edify.errors.context import CallerContext
from edify.errors.formatting import FixInsertion, Problem
_END_INSERTION_TEXT = ".end()"
_DANGLING_INSERTION_TEXT = ".digit()"
+_UNKNOWN_CALL_SITE = CallerContext(
+ filename="<unknown>",
+ lineno=0,
+ colno=1,
+ end_colno=1,
+ source_line="",
+)
def diagnose_unfinished(state: BuilderState, subject: str) -> Problem | None:
@@ -32,8 +40,8 @@ def diagnose_unfinished(state: BuilderState, subject: str) -> Problem | None:
def _diagnose_open_frame(frame: StackFrame, subject: str) -> Problem:
"""Return a :class:`Problem` describing an unclosed frame."""
frame_name = _frame_display_name(frame)
- problem_context = frame.call_site
- fix_context = frame.last_child_call_site or frame.call_site
+ problem_context = frame.call_site or _UNKNOWN_CALL_SITE
+ fix_context = frame.last_child_call_site or frame.call_site or _UNKNOWN_CALL_SITE
fix_insertion = FixInsertion(
column=fix_context.end_colno,
text=_END_INSERTION_TEXT,
@@ -51,8 +59,8 @@ def _diagnose_open_frame(frame: StackFrame, subject: str) -> Problem:
def _diagnose_dangling_quantifier(frame: StackFrame, subject: str) -> Problem:
"""Return a :class:`Problem` describing a pending quantifier with no operand."""
quantifier_name = frame.quantifier_name or "quantifier"
- problem_context = frame.quantifier_call_site
- fix_context = frame.quantifier_call_site
+ problem_context = frame.quantifier_call_site or _UNKNOWN_CALL_SITE
+ fix_context = frame.quantifier_call_site or _UNKNOWN_CALL_SITE
fix_insertion = FixInsertion(
column=fix_context.end_colno,
text=_DANGLING_INSERTION_TEXT,
diff --git a/edify/builder/merge.py b/edify/builder/merge.py
index 6c70bf4..65b7833 100644
--- a/edify/builder/merge.py
+++ b/edify/builder/merge.py
@@ -19,6 +19,7 @@ groups it introduced (zero for non-capture elements).
from __future__ import annotations
from dataclasses import dataclass
+from typing import Protocol
from edify.elements.types.base import BaseElement
from edify.elements.types.captures import (
@@ -185,14 +186,53 @@ def _merge_end_of_input(element: EndOfInputElement, context: MergeContext) -> Me
return MergeResult(element=element, captures_added=0)
-def _merge_container(element: BaseElement, context: MergeContext, container_class) -> MergeResult:
+_ContainerElement = (
+ GroupElement
+ | AnyOfElement
+ | SubexpressionElement
+ | AssertAheadElement
+ | AssertNotAheadElement
+ | AssertBehindElement
+ | AssertNotBehindElement
+)
+
+_QuantifierElement = (
+ OptionalElement
+ | ZeroOrMoreElement
+ | ZeroOrMoreLazyElement
+ | OneOrMoreElement
+ | OneOrMoreLazyElement
+)
+
+
+class _ContainerFactory(Protocol):
+ """Structural type for a container-class constructor."""
+
+ def __call__(self, children: tuple[BaseElement, ...]) -> BaseElement: ...
+
+
+class _QuantifierFactory(Protocol):
+ """Structural type for a no-parameter quantifier constructor."""
+
+ def __call__(self, child: BaseElement) -> BaseElement: ...
+
+
+def _merge_container(
+ element: _ContainerElement,
+ context: MergeContext,
+ container_class: _ContainerFactory,
+) -> MergeResult:
"""Recurse into a container element's children and re-wrap in the same class."""
merged_children, child_captures_added = _merge_children(element.children, context)
new_element = container_class(children=merged_children)
return MergeResult(element=new_element, captures_added=child_captures_added)
-def _merge_quantifier(element: BaseElement, context: MergeContext, quantifier_class) -> MergeResult:
+def _merge_quantifier(
+ element: _QuantifierElement,
+ context: MergeContext,
+ quantifier_class: _QuantifierFactory,
+) -> MergeResult:
"""Recurse into a no-parameter quantifier's child and re-wrap in the same class."""
child_result = merge_element(element.child, context)
new_element = quantifier_class(child=child_result.element)
diff --git a/edify/builder/types/protocol.py b/edify/builder/types/protocol.py
index 15acfe4..c73b9dd 100644
--- a/edify/builder/types/protocol.py
+++ b/edify/builder/types/protocol.py
@@ -43,8 +43,8 @@ class BuilderProtocol(Protocol):
def subexpression(
self,
- pattern: BuilderProtocol,
- *,
+ expression: BuilderProtocol,
+ namespace: str = ...,
ignore_flags: bool = ...,
ignore_start_and_end: bool = ...,
) -> Self: ...
diff --git a/edify/compile/dispatch.py b/edify/compile/dispatch.py
index 5c9a15c..df90167 100644
--- a/edify/compile/dispatch.py
+++ b/edify/compile/dispatch.py
@@ -14,17 +14,18 @@ from edify.compile.groups import render_grouping
from edify.compile.leaves import render_leaf
from edify.compile.quantifier import render_quantifier
from edify.compile.root import render_root
+from edify.elements.types.base import BaseElement
+from edify.elements.types.root import RootElement
from edify.elements.types.union import (
CaptureGroupElement,
CharShapedElement,
- Element,
GroupingElement,
LeafElement,
QuantifierElement,
)
-def render_element(element: Element) -> str:
+def render_element(element: BaseElement) -> str:
"""Return the regex string produced by any element.
Args:
@@ -43,4 +44,5 @@ def render_element(element: Element) -> str:
return render_grouping(element, render_element)
if isinstance(element, QuantifierElement):
return render_quantifier(element, render_element)
+ assert isinstance(element, RootElement)
return render_root(element, render_element)
diff --git a/edify/compile/fuse.py b/edify/compile/fuse.py
index 79676d6..4b8d96b 100644
--- a/edify/compile/fuse.py
+++ b/edify/compile/fuse.py
@@ -58,4 +58,5 @@ def _fragment_for(member: BaseElement) -> str:
return member.value
if isinstance(member, AnyOfCharsElement):
return member.value
+ assert isinstance(member, RangeElement)
return f"{member.start}-{member.end}"
diff --git a/edify/errors/context.py b/edify/errors/context.py
index c5c645b..4108002 100644
--- a/edify/errors/context.py
+++ b/edify/errors/context.py
@@ -6,6 +6,7 @@ import linecache
import os
import sys
from dataclasses import dataclass
+from types import FrameType
_EDIFY_PACKAGE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_EDIFY_PACKAGE_PREFIX = _EDIFY_PACKAGE_ROOT + os.sep
@@ -36,7 +37,7 @@ def capture_caller_context() -> CallerContext | None:
Returns ``None`` when every frame on the stack lives inside the ``edify/``
package tree.
"""
- current_frame = sys._getframe(1)
+ current_frame: FrameType | None = sys._getframe(1)
while current_frame is not None:
filename = current_frame.f_code.co_filename
absolute_filename = (
@@ -48,18 +49,21 @@ def capture_caller_context() -> CallerContext | None:
return None
-def _context_for_frame(frame) -> CallerContext:
+def _context_for_frame(frame: FrameType) -> CallerContext:
"""Return a :class:`CallerContext` describing ``frame``'s current instruction."""
filename = frame.f_code.co_filename
positions = list(frame.f_code.co_positions())
instruction_index = frame.f_lasti // 2
- start_line, _end_line, start_col, end_col = positions[instruction_index]
- source_line = _read_source_line(filename, start_line)
+ raw_start_line, _end_line, raw_start_col, raw_end_col = positions[instruction_index]
+ resolved_start_line = raw_start_line if raw_start_line is not None else frame.f_lineno
+ resolved_start_col = raw_start_col if raw_start_col is not None else 0
+ resolved_end_col = raw_end_col if raw_end_col is not None else resolved_start_col
+ source_line = _read_source_line(filename, resolved_start_line)
return CallerContext(
filename=filename,
- lineno=start_line,
- colno=start_col + 1,
- end_colno=end_col + 1,
+ lineno=resolved_start_line,
+ colno=resolved_start_col + 1,
+ end_colno=resolved_end_col + 1,
source_line=source_line,
)
diff --git a/edify/introspect/explain.py b/edify/introspect/explain.py
index c4a935b..2e903f4 100644
--- a/edify/introspect/explain.py
+++ b/edify/introspect/explain.py
@@ -127,7 +127,7 @@ def _build_lines(elements: tuple[BaseElement, ...]) -> list[str]:
def _wrap_step(step_number: int, description: str) -> str:
- """Return the description prefixed with the step number and 3-space continuation indent."""
+ """Return the description prefixed with the step number and indented continuation."""
prefix = f" {step_number}. "
continuation_prefix = " " * len(prefix)
words = description.split()
diff --git a/edify/introspect/graphviz.py b/edify/introspect/graphviz.py
index 3440301..b61c482 100644
--- a/edify/introspect/graphviz.py
+++ b/edify/introspect/graphviz.py
@@ -30,6 +30,7 @@ from edify.elements.types.quantifiers import (
ZeroOrMoreElement,
ZeroOrMoreLazyElement,
)
+from edify.elements.types.union import QuantifierElement
from edify.errors.introspect import MissingGraphvizDependencyError
from edify.introspect.ascii import _char_label, _leaf_label
from edify.introspect.types import Emission
@@ -46,7 +47,7 @@ def render_graphviz_svg(elements: tuple[BaseElement, ...]) -> str:
raise MissingGraphvizDependencyError()
dot_source = render_dot(elements)
source = _graphviz_module.Source(dot_source, format="svg")
- piped_bytes = source.pipe(format="svg")
+ piped_bytes: bytes = source.pipe(format="svg")
return piped_bytes.decode("utf-8")
@@ -185,6 +186,7 @@ def _emit_quantifier_cluster(element: BaseElement, counter: _Counter) -> Emissio
quantifier_phrase = _quantifier_phrase(element)
if quantifier_phrase is None:
return None
+ assert isinstance(element, QuantifierElement)
child_emission = _emit_element(element.child, counter)
cluster_id = counter.next("cluster")
lines: list[str] = [
@@ -220,6 +222,7 @@ def _simple_quantifier_label(element: BaseElement) -> str | None:
quantifier_phrase = _quantifier_phrase(element)
if quantifier_phrase is None:
return None
+ assert isinstance(element, QuantifierElement)
child = element.child
child_label = _leaf_label(child) or _char_label(child)
if child_label is None:
diff --git a/edify/pattern/factories/assertions.py b/edify/pattern/factories/assertions.py
index 636c868..fbfe113 100644
--- a/edify/pattern/factories/assertions.py
+++ b/edify/pattern/factories/assertions.py
@@ -12,6 +12,7 @@ single lookaround element wrapping the supplied operand's children.
from __future__ import annotations
from edify.builder.types.protocol import BuilderProtocol
+from edify.elements.types.base import BaseElement
from edify.elements.types.groups import (
AssertAheadElement,
AssertBehindElement,
@@ -42,6 +43,6 @@ def assert_not_behind(operand: BuilderProtocol) -> Pattern:
return pattern_containing(AssertNotBehindElement(children=_operand_children(operand)))
-def _operand_children(operand: BuilderProtocol) -> tuple:
+def _operand_children(operand: BuilderProtocol) -> tuple[BaseElement, ...]:
"""Return ``operand``'s root-frame children as an immutable tuple."""
return tuple(operand._state.top_frame.children)
diff --git a/edify/pattern/factories/groups.py b/edify/pattern/factories/groups.py
index fb6cd5a..9520a0d 100644
--- a/edify/pattern/factories/groups.py
+++ b/edify/pattern/factories/groups.py
@@ -14,6 +14,7 @@ single grouping element built from the supplied operand(s).
from __future__ import annotations
from edify.builder.types.protocol import BuilderProtocol
+from edify.elements.types.base import BaseElement
from edify.elements.types.captures import (
BackReferenceElement,
CaptureElement,
@@ -60,7 +61,7 @@ def any_of(*operands: BuilderProtocol) -> Pattern:
return pattern_containing(AnyOfElement(children=children))
-def _operand_children(operand: BuilderProtocol) -> tuple:
+def _operand_children(operand: BuilderProtocol) -> tuple[BaseElement, ...]:
"""Return ``operand``'s root-frame children as an immutable tuple."""
return tuple(operand._state.top_frame.children)
diff --git a/edify/result/regex.py b/edify/result/regex.py
index 8cc4239..89dbb03 100644
--- a/edify/result/regex.py
+++ b/edify/result/regex.py
@@ -135,7 +135,8 @@ class Regex:
def __getattr__(self, name: str) -> _RePatternAttribute:
"""Return the underlying :class:`re.Pattern` attribute named ``name``."""
- return getattr(self._compiled, name)
+ attribute: _RePatternAttribute = getattr(self._compiled, name)
+ return attribute
def __repr__(self) -> str:
"""Return ``<Regex 'source-string'>``."""
diff --git a/edify/serialize/kinds.py b/edify/serialize/kinds.py
index 6b4fc0c..3c9c0f3 100644
--- a/edify/serialize/kinds.py
+++ b/edify/serialize/kinds.py
@@ -7,6 +7,7 @@ part of the wire format — only the corresponding ``kind`` (``"exactly"``).
from __future__ import annotations
+from edify.elements.types.base import BaseElement
from edify.elements.types.captures import (
BackReferenceElement,
CaptureElement,
@@ -67,7 +68,7 @@ from edify.elements.types.quantifiers import (
)
from edify.elements.types.root import RootElement
-_CLASS_BY_KIND = {
+_CLASS_BY_KIND: dict[str, type[BaseElement]] = {
"root": RootElement,
"start": StartOfInputElement,
"end": EndOfInputElement,
@@ -122,11 +123,11 @@ _CLASS_BY_KIND = {
_KIND_BY_CLASS = {cls: kind for kind, cls in _CLASS_BY_KIND.items()}
-def kind_for(element_class: type) -> str:
+def kind_for(element_class: type[BaseElement]) -> str:
"""Return the public ``kind`` string registered for ``element_class``."""
return _KIND_BY_CLASS[element_class]
-def class_for(kind: str) -> type:
+def class_for(kind: str) -> type[BaseElement]:
"""Return the element class registered under ``kind``."""
return _CLASS_BY_KIND[kind]
diff --git a/edify/serialize/load.py b/edify/serialize/load.py
index f571789..67d6a39 100644
--- a/edify/serialize/load.py
+++ b/edify/serialize/load.py
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import fields
+from typing import cast
from edify.builder.types.flags import Flags
from edify.builder.types.frame import StackFrame
@@ -119,8 +120,10 @@ def _walk_elements(children: tuple[BaseElement, ...]) -> Iterator[BaseElement]:
for element in children:
yield element
for spec in fields(element):
- value = getattr(element, spec.name)
- if isinstance(value, tuple):
- yield from _walk_elements(value)
- elif isinstance(value, BaseElement):
+ value: object = getattr(element, spec.name)
+ if isinstance(value, BaseElement):
yield from _walk_elements((value,))
+ continue
+ if isinstance(value, tuple):
+ narrowed_tuple = cast(tuple[BaseElement, ...], value)
+ yield from _walk_elements(narrowed_tuple)
diff --git a/tests/builder/repr.test.py b/tests/builder/repr.test.py
index c5c716e..278f40b 100644
--- a/tests/builder/repr.test.py
+++ b/tests/builder/repr.test.py
@@ -1,11 +1,6 @@
"""Tests for :meth:`BuilderCore.__repr__` on :class:`RegexBuilder` and :class:`Pattern`."""
from edify import Pattern, RegexBuilder
-from edify.builder.core import BuilderCore
-
-
-class _BareBuilder(BuilderCore):
- """A minimal :class:`BuilderCore` subclass without :class:`TerminalsMixin`."""
def test_repr_of_a_fresh_regex_builder_shows_the_empty_non_capturing_group():
@@ -34,7 +29,3 @@ def test_repr_of_a_builder_with_open_frames_shows_the_unclosed_marker():
def test_repr_uses_the_concrete_class_name_not_the_base():
assert repr(RegexBuilder()).startswith("<RegexBuilder ")
assert repr(Pattern()).startswith("<Pattern ")
-
-
-def test_repr_falls_back_when_the_subclass_lacks_the_terminals_mixin():
- assert repr(_BareBuilder()) == "<_BareBuilder '<unclosed>'>"