diff options
| author | natsuoto <[email protected]> | 2026-07-15 12:04:21 +0530 |
|---|---|---|
| committer | natsuoto <[email protected]> | 2026-07-15 12:04:21 +0530 |
| commit | e1fc8c45a0534722b2c8d3a1fa94876a25d57901 (patch) | |
| tree | c17bcae990fe6ef1cd670df9106948402760896d | |
| parent | 089004d6d63c06d11e195484e767e5fe30b5cfcb (diff) | |
| download | edify-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.py | 15 | ||||
| -rw-r--r-- | edify/builder/diagnose.py | 16 | ||||
| -rw-r--r-- | edify/builder/merge.py | 44 | ||||
| -rw-r--r-- | edify/builder/types/protocol.py | 4 | ||||
| -rw-r--r-- | edify/compile/dispatch.py | 6 | ||||
| -rw-r--r-- | edify/compile/fuse.py | 1 | ||||
| -rw-r--r-- | edify/errors/context.py | 18 | ||||
| -rw-r--r-- | edify/introspect/explain.py | 2 | ||||
| -rw-r--r-- | edify/introspect/graphviz.py | 5 | ||||
| -rw-r--r-- | edify/pattern/factories/assertions.py | 3 | ||||
| -rw-r--r-- | edify/pattern/factories/groups.py | 3 | ||||
| -rw-r--r-- | edify/result/regex.py | 3 | ||||
| -rw-r--r-- | edify/serialize/kinds.py | 7 | ||||
| -rw-r--r-- | edify/serialize/load.py | 11 | ||||
| -rw-r--r-- | tests/builder/repr.test.py | 9 |
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>'>" |
