simplibs-regex 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simplibs/regex/__init__.py +171 -0
- simplibs/regex/base_class/Regex.py +328 -0
- simplibs/regex/base_class/_Precedence.py +43 -0
- simplibs/regex/base_class/__init__.py +18 -0
- simplibs/regex/compiler/RegexPattern.py +220 -0
- simplibs/regex/compiler/__init__.py +16 -0
- simplibs/regex/compiler/_validations/__init__.py +18 -0
- simplibs/regex/compiler/_validations/raise_invalid_pattern_error.py +38 -0
- simplibs/regex/compiler/_validations/raise_regex_pattern_invalid_flags_error.py +20 -0
- simplibs/regex/compiler/_validations/raise_regex_pattern_invalid_node_error.py +20 -0
- simplibs/regex/containers/Alternation.py +106 -0
- simplibs/regex/containers/Conditional.py +113 -0
- simplibs/regex/containers/Group.py +191 -0
- simplibs/regex/containers/Lookaround.py +145 -0
- simplibs/regex/containers/Repeat.py +185 -0
- simplibs/regex/containers/Sequence.py +100 -0
- simplibs/regex/containers/__init__.py +31 -0
- simplibs/regex/containers/_validations/__init__.py +67 -0
- simplibs/regex/containers/_validations/alternation_and_sequence/__init__.py +16 -0
- simplibs/regex/containers/_validations/alternation_and_sequence/raise_node_param_not_regex_error.py +30 -0
- simplibs/regex/containers/_validations/alternation_and_sequence/raise_requires_at_least_one_node_error.py +30 -0
- simplibs/regex/containers/_validations/conditional/__init__.py +22 -0
- simplibs/regex/containers/_validations/conditional/raise_conditional_invalid_id_type_error.py +20 -0
- simplibs/regex/containers/_validations/conditional/raise_conditional_invalid_name_error.py +20 -0
- simplibs/regex/containers/_validations/conditional/raise_conditional_invalid_numeric_id_error.py +20 -0
- simplibs/regex/containers/_validations/conditional/raise_conditional_no_not_regex_error.py +20 -0
- simplibs/regex/containers/_validations/conditional/raise_conditional_yes_not_regex_error.py +20 -0
- simplibs/regex/containers/_validations/group/__init__.py +28 -0
- simplibs/regex/containers/_validations/group/raise_group_atomic_and_flags_conflict_error.py +20 -0
- simplibs/regex/containers/_validations/group/raise_group_atomic_and_name_conflict_error.py +20 -0
- simplibs/regex/containers/_validations/group/raise_group_flags_off_without_flags_error.py +20 -0
- simplibs/regex/containers/_validations/group/raise_group_flags_require_non_capturing_error.py +20 -0
- simplibs/regex/containers/_validations/group/raise_group_inner_not_regex_error.py +20 -0
- simplibs/regex/containers/_validations/group/raise_group_name_and_flags_conflict_error.py +20 -0
- simplibs/regex/containers/_validations/group/raise_group_name_requires_capturing_error.py +20 -0
- simplibs/regex/containers/_validations/group/raise_invalid_group_name_error.py +33 -0
- simplibs/regex/containers/_validations/lookaround/__init__.py +18 -0
- simplibs/regex/containers/_validations/lookaround/raise_lookaround_inner_not_regex_error.py +20 -0
- simplibs/regex/containers/_validations/lookaround/raise_lookaround_invalid_direction_error.py +20 -0
- simplibs/regex/containers/_validations/lookaround/raise_variable_length_lookbehind_error.py +30 -0
- simplibs/regex/containers/_validations/repeat/__init__.py +20 -0
- simplibs/regex/containers/_validations/repeat/raise_repeat_inner_not_regex_error.py +20 -0
- simplibs/regex/containers/_validations/repeat/raise_repeat_invalid_mode_error.py +20 -0
- simplibs/regex/containers/_validations/repeat/raise_repeat_max_less_than_min_error.py +20 -0
- simplibs/regex/containers/_validations/repeat/raise_repeat_min_negative_error.py +20 -0
- simplibs/regex/containers/enums/LookaroundDirection.py +23 -0
- simplibs/regex/containers/enums/RepeatMode.py +23 -0
- simplibs/regex/containers/enums/__init__.py +19 -0
- simplibs/regex/elements/Anchor.py +81 -0
- simplibs/regex/elements/CharCode.py +116 -0
- simplibs/regex/elements/CharacterClass.py +130 -0
- simplibs/regex/elements/CharacterRange.py +94 -0
- simplibs/regex/elements/CharacterType.py +77 -0
- simplibs/regex/elements/GroupReference.py +94 -0
- simplibs/regex/elements/Literal.py +141 -0
- simplibs/regex/elements/RawPattern.py +119 -0
- simplibs/regex/elements/__init__.py +34 -0
- simplibs/regex/elements/_helpers/__init__.py +15 -0
- simplibs/regex/elements/_helpers/char_class_escape.py +34 -0
- simplibs/regex/elements/_validations/__init__.py +58 -0
- simplibs/regex/elements/_validations/anorch/__init__.py +14 -0
- simplibs/regex/elements/_validations/anorch/raise_anchor_invalid_kind_error.py +20 -0
- simplibs/regex/elements/_validations/char_code/__init__.py +20 -0
- simplibs/regex/elements/_validations/char_code/raise_char_code_invalid_kind_error.py +20 -0
- simplibs/regex/elements/_validations/char_code/raise_char_code_invalid_named_value_error.py +20 -0
- simplibs/regex/elements/_validations/char_code/raise_char_code_invalid_type_error.py +21 -0
- simplibs/regex/elements/_validations/char_code/raise_char_code_out_of_range_error.py +21 -0
- simplibs/regex/elements/_validations/character_class/__init__.py +18 -0
- simplibs/regex/elements/_validations/character_class/raise_char_class_empty_error.py +20 -0
- simplibs/regex/elements/_validations/character_class/raise_char_class_item_not_allowed_error.py +20 -0
- simplibs/regex/elements/_validations/character_class/raise_multi_char_literal_in_char_class_error.py +20 -0
- simplibs/regex/elements/_validations/character_range/__init__.py +18 -0
- simplibs/regex/elements/_validations/character_range/raise_character_range_invalid_boundary_error.py +20 -0
- simplibs/regex/elements/_validations/character_range/raise_character_range_no_standalone_pattern_error.py +20 -0
- simplibs/regex/elements/_validations/character_range/raise_character_range_start_after_end_error.py +20 -0
- simplibs/regex/elements/_validations/character_type/__init__.py +14 -0
- simplibs/regex/elements/_validations/character_type/raise_character_type_invalid_kind_error.py +20 -0
- simplibs/regex/elements/_validations/group_reference/__init__.py +18 -0
- simplibs/regex/elements/_validations/group_reference/raise_group_reference_invalid_identifier_error.py +20 -0
- simplibs/regex/elements/_validations/group_reference/raise_group_reference_invalid_type_error.py +20 -0
- simplibs/regex/elements/_validations/group_reference/raise_group_reference_numeric_out_of_range_error.py +20 -0
- simplibs/regex/elements/_validations/literal/__init__.py +18 -0
- simplibs/regex/elements/_validations/literal/raise_literal_empty_error.py +20 -0
- simplibs/regex/elements/_validations/literal/raise_literal_invalid_type_error.py +20 -0
- simplibs/regex/elements/_validations/literal/raise_literal_not_single_char_error.py +20 -0
- simplibs/regex/elements/enums/AnchorKind.py +30 -0
- simplibs/regex/elements/enums/CharCodeKind.py +28 -0
- simplibs/regex/elements/enums/CharacterTypeKind.py +30 -0
- simplibs/regex/elements/enums/__init__.py +21 -0
- simplibs/regex/flags/Flag.py +54 -0
- simplibs/regex/flags/__init__.py +16 -0
- simplibs/regex/presets/__init__.py +16 -0
- simplibs/regex/presets/anchors/END.py +17 -0
- simplibs/regex/presets/anchors/END_STRING.py +16 -0
- simplibs/regex/presets/anchors/NON_WORD_BOUNDARY.py +17 -0
- simplibs/regex/presets/anchors/START.py +17 -0
- simplibs/regex/presets/anchors/START_STRING.py +17 -0
- simplibs/regex/presets/anchors/WORD_BOUNDARY.py +17 -0
- simplibs/regex/presets/anchors/__init__.py +25 -0
- simplibs/regex/presets/character_types/DIGIT.py +18 -0
- simplibs/regex/presets/character_types/NON_DIGIT.py +18 -0
- simplibs/regex/presets/character_types/NON_WHITESPACE.py +18 -0
- simplibs/regex/presets/character_types/NON_WORD.py +18 -0
- simplibs/regex/presets/character_types/WHITESPACE.py +18 -0
- simplibs/regex/presets/character_types/WORD.py +18 -0
- simplibs/regex/presets/character_types/__init__.py +25 -0
- simplibs/regex/presets/groups/ATOMIC_GROUP.py +19 -0
- simplibs/regex/presets/groups/NAMED_GROUP.py +20 -0
- simplibs/regex/presets/groups/NON_CAPTURING.py +19 -0
- simplibs/regex/presets/groups/__init__.py +19 -0
- simplibs/regex/presets/lookaround/LOOKAHEAD.py +19 -0
- simplibs/regex/presets/lookaround/LOOKBEHIND.py +19 -0
- simplibs/regex/presets/lookaround/NEGATIVE_LOOKAHEAD.py +19 -0
- simplibs/regex/presets/lookaround/NEGATIVE_LOOKBEHIND.py +19 -0
- simplibs/regex/presets/lookaround/__init__.py +21 -0
- simplibs/regex/presets/quantifiers/AT_LEAST.py +22 -0
- simplibs/regex/presets/quantifiers/BETWEEN.py +25 -0
- simplibs/regex/presets/quantifiers/EXACTLY.py +21 -0
- simplibs/regex/presets/quantifiers/ONE_OR_MORE.py +21 -0
- simplibs/regex/presets/quantifiers/OPTIONAL.py +19 -0
- simplibs/regex/presets/quantifiers/ZERO_OR_MORE.py +20 -0
- simplibs/regex/presets/quantifiers/__init__.py +25 -0
- simplibs_regex-0.1.0.dist-info/METADATA +298 -0
- simplibs_regex-0.1.0.dist-info/RECORD +127 -0
- simplibs_regex-0.1.0.dist-info/WHEEL +5 -0
- simplibs_regex-0.1.0.dist-info/licenses/LICENSE +21 -0
- simplibs_regex-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# ======================================================================
|
|
2
|
+
# Base Class
|
|
3
|
+
# ======================================================================
|
|
4
|
+
from .base_class.Regex import Regex
|
|
5
|
+
|
|
6
|
+
# ======================================================================
|
|
7
|
+
# Compiler
|
|
8
|
+
# ======================================================================
|
|
9
|
+
from .compiler.RegexPattern import RegexPattern
|
|
10
|
+
|
|
11
|
+
# ======================================================================
|
|
12
|
+
# Elements & Enums
|
|
13
|
+
# ======================================================================
|
|
14
|
+
from .elements.Anchor import Anchor
|
|
15
|
+
from .elements.enums.AnchorKind import AnchorKind
|
|
16
|
+
from .elements.CharacterClass import CharacterClass
|
|
17
|
+
from .elements.CharacterRange import CharacterRange
|
|
18
|
+
from .elements.CharacterType import CharacterType
|
|
19
|
+
from .elements.enums.CharacterTypeKind import CharacterTypeKind
|
|
20
|
+
from .elements.CharCode import CharCode
|
|
21
|
+
from .elements.enums.CharCodeKind import CharCodeKind
|
|
22
|
+
from .elements.GroupReference import GroupReference
|
|
23
|
+
from .elements.Literal import Literal
|
|
24
|
+
from .elements.RawPattern import RawPattern
|
|
25
|
+
|
|
26
|
+
# ======================================================================
|
|
27
|
+
# Containers & Enums
|
|
28
|
+
# ======================================================================
|
|
29
|
+
from .containers.Alternation import Alternation
|
|
30
|
+
from .containers.Conditional import Conditional
|
|
31
|
+
from .containers.Group import Group
|
|
32
|
+
from .containers.Lookaround import Lookaround
|
|
33
|
+
from .containers.enums.LookaroundDirection import LookaroundDirection
|
|
34
|
+
from .containers.Repeat import Repeat
|
|
35
|
+
from .containers.enums.RepeatMode import RepeatMode
|
|
36
|
+
from .containers.Sequence import Sequence
|
|
37
|
+
|
|
38
|
+
# ======================================================================
|
|
39
|
+
# Flags
|
|
40
|
+
# ======================================================================
|
|
41
|
+
from .flags.Flag import Flag
|
|
42
|
+
|
|
43
|
+
# ======================================================================
|
|
44
|
+
# Presets (Anchors)
|
|
45
|
+
# ======================================================================
|
|
46
|
+
from .presets.anchors.END import END
|
|
47
|
+
from .presets.anchors.END_STRING import END_STRING
|
|
48
|
+
from .presets.anchors.NON_WORD_BOUNDARY import NON_WORD_BOUNDARY
|
|
49
|
+
from .presets.anchors.START import START
|
|
50
|
+
from .presets.anchors.START_STRING import START_STRING
|
|
51
|
+
from .presets.anchors.WORD_BOUNDARY import WORD_BOUNDARY
|
|
52
|
+
|
|
53
|
+
# ======================================================================
|
|
54
|
+
# Presets (Character Types)
|
|
55
|
+
# ======================================================================
|
|
56
|
+
from .presets.character_types.DIGIT import DIGIT
|
|
57
|
+
from .presets.character_types.NON_DIGIT import NON_DIGIT
|
|
58
|
+
from .presets.character_types.NON_WHITESPACE import NON_WHITESPACE
|
|
59
|
+
from .presets.character_types.NON_WORD import NON_WORD
|
|
60
|
+
from .presets.character_types.WHITESPACE import WHITESPACE
|
|
61
|
+
from .presets.character_types.WORD import WORD
|
|
62
|
+
|
|
63
|
+
# ======================================================================
|
|
64
|
+
# Presets (Groups)
|
|
65
|
+
# ======================================================================
|
|
66
|
+
from .presets.groups.ATOMIC_GROUP import ATOMIC_GROUP
|
|
67
|
+
from .presets.groups.NAMED_GROUP import NAMED_GROUP
|
|
68
|
+
from .presets.groups.NON_CAPTURING import NON_CAPTURING
|
|
69
|
+
|
|
70
|
+
# ======================================================================
|
|
71
|
+
# Presets (Lookaround)
|
|
72
|
+
# ======================================================================
|
|
73
|
+
from .presets.lookaround.LOOKAHEAD import LOOKAHEAD
|
|
74
|
+
from .presets.lookaround.LOOKBEHIND import LOOKBEHIND
|
|
75
|
+
from .presets.lookaround.NEGATIVE_LOOKAHEAD import NEGATIVE_LOOKAHEAD
|
|
76
|
+
from .presets.lookaround.NEGATIVE_LOOKBEHIND import NEGATIVE_LOOKBEHIND
|
|
77
|
+
|
|
78
|
+
# ======================================================================
|
|
79
|
+
# Presets (Quantifiers)
|
|
80
|
+
# ======================================================================
|
|
81
|
+
from .presets.quantifiers.AT_LEAST import AT_LEAST
|
|
82
|
+
from .presets.quantifiers.BETWEEN import BETWEEN
|
|
83
|
+
from .presets.quantifiers.EXACTLY import EXACTLY
|
|
84
|
+
from .presets.quantifiers.ONE_OR_MORE import ONE_OR_MORE
|
|
85
|
+
from .presets.quantifiers.OPTIONAL import OPTIONAL
|
|
86
|
+
from .presets.quantifiers.ZERO_OR_MORE import ZERO_OR_MORE
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = [
|
|
90
|
+
# Base Class
|
|
91
|
+
"Regex",
|
|
92
|
+
# Compiler
|
|
93
|
+
"RegexPattern",
|
|
94
|
+
# Elements & Enums
|
|
95
|
+
"Anchor",
|
|
96
|
+
"AnchorKind",
|
|
97
|
+
"CharacterClass",
|
|
98
|
+
"CharacterRange",
|
|
99
|
+
"CharacterType",
|
|
100
|
+
"CharacterTypeKind",
|
|
101
|
+
"CharCode",
|
|
102
|
+
"CharCodeKind",
|
|
103
|
+
"GroupReference",
|
|
104
|
+
"Literal",
|
|
105
|
+
"RawPattern",
|
|
106
|
+
# Containers & Enums
|
|
107
|
+
"Alternation",
|
|
108
|
+
"Conditional",
|
|
109
|
+
"Group",
|
|
110
|
+
"Lookaround",
|
|
111
|
+
"LookaroundDirection",
|
|
112
|
+
"Repeat",
|
|
113
|
+
"RepeatMode",
|
|
114
|
+
"Sequence",
|
|
115
|
+
# Flags
|
|
116
|
+
"Flag",
|
|
117
|
+
# Presets - Anchors
|
|
118
|
+
"END",
|
|
119
|
+
"END_STRING",
|
|
120
|
+
"NON_WORD_BOUNDARY",
|
|
121
|
+
"START",
|
|
122
|
+
"START_STRING",
|
|
123
|
+
"WORD_BOUNDARY",
|
|
124
|
+
# Presets - Character Types
|
|
125
|
+
"DIGIT",
|
|
126
|
+
"NON_DIGIT",
|
|
127
|
+
"NON_WHITESPACE",
|
|
128
|
+
"NON_WORD",
|
|
129
|
+
"WHITESPACE",
|
|
130
|
+
"WORD",
|
|
131
|
+
# Presets - Groups
|
|
132
|
+
"ATOMIC_GROUP",
|
|
133
|
+
"NAMED_GROUP",
|
|
134
|
+
"NON_CAPTURING",
|
|
135
|
+
# Presets - Lookaround
|
|
136
|
+
"LOOKAHEAD",
|
|
137
|
+
"LOOKBEHIND",
|
|
138
|
+
"NEGATIVE_LOOKAHEAD",
|
|
139
|
+
"NEGATIVE_LOOKBEHIND",
|
|
140
|
+
# Presets - Quantifiers",
|
|
141
|
+
"AT_LEAST",
|
|
142
|
+
"BETWEEN",
|
|
143
|
+
"EXACTLY",
|
|
144
|
+
"ONE_OR_MORE",
|
|
145
|
+
"OPTIONAL",
|
|
146
|
+
"ZERO_OR_MORE",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_DESIGN_NOTES = """
|
|
151
|
+
# Simplibs Regex Library — Main Package Root
|
|
152
|
+
|
|
153
|
+
## Purpose
|
|
154
|
+
The `simplibs-regex` root package provides a clean, expressive,
|
|
155
|
+
and type-safe Domain Specific Language (DSL) for constructing,
|
|
156
|
+
composing, and compiling regular expressions in Python.
|
|
157
|
+
It decouples primitive AST nodes from structural containers
|
|
158
|
+
and ergonomic presets, ensuring robust IDE support
|
|
159
|
+
and fail-fast validation.
|
|
160
|
+
|
|
161
|
+
## Core Architecture & Root Packages Registry
|
|
162
|
+
|
|
163
|
+
| Sub-Package / Module | Type | Description |
|
|
164
|
+
| :------------------- | :--------------- | :---------------------------------------------------------------------------------- |
|
|
165
|
+
| `base_class` | Foundation | Provides the root abstract AST node class (`Regex`) and precedence management. |
|
|
166
|
+
| `compiler` | Compilation | Handles expression compilation, pattern generation, and execution (`RegexPattern`). |
|
|
167
|
+
| `elements` | Atomic AST Nodes | Foundational building blocks like literals, anchors, character classes, and codes. |
|
|
168
|
+
| `containers` | Higher-Order AST | Structural combinators like sequences, alternations, groups, and quantifiers. |
|
|
169
|
+
| `flags` | Configuration | Regular expression compilation flags (`Flag`). |
|
|
170
|
+
| `presets` | DSL Shortcuts | Ready-to-use syntactic helpers and atomic aliases for clean expression building. |
|
|
171
|
+
"""
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
# Inners
|
|
4
|
+
from ._Precedence import _Precedence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Regex(ABC):
|
|
8
|
+
"""Abstract base class for every node of the regex DSL.
|
|
9
|
+
|
|
10
|
+
Every concrete node (container or atom) inherits from this class and
|
|
11
|
+
implements `to_pattern` and, where a fixed length is knowable,
|
|
12
|
+
`fixed_length`. The class provides precedence-aware rendering via
|
|
13
|
+
`render` (Point 1), fixed-length introspection for lookbehind
|
|
14
|
+
validation (Point 3), a character-class-item opt-in marker (Point 4),
|
|
15
|
+
and `+`/`|` composition operators mirroring `Rule`'s `&`/`|`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# ----------------------------------------------------------------------
|
|
19
|
+
# Class-level contract every subclass declares
|
|
20
|
+
# ----------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
# Point 1 — binding power of THIS node's own top-level syntax, used by
|
|
23
|
+
# a parent's `render()` to decide whether this node needs wrapping.
|
|
24
|
+
# Defaults to ATOM (self-delimiting) — the loosest-binding containers
|
|
25
|
+
# (Sequence, Alternation) explicitly override this to a lower value.
|
|
26
|
+
_precedence: _Precedence = _Precedence.ATOM
|
|
27
|
+
|
|
28
|
+
# Point 4 — opt-in marker: True on nodes that are legal standalone
|
|
29
|
+
# items inside a CharacterClass (`[...]`), where escape semantics
|
|
30
|
+
# differ from the rest of the pattern (see CharacterClass's own
|
|
31
|
+
# validation, which checks this flag rather than a broad isinstance
|
|
32
|
+
# check against the whole Regex hierarchy). Defaults to False; only
|
|
33
|
+
# CharacterType, CharacterRange, CharCode and single-character
|
|
34
|
+
# Literal override it to True.
|
|
35
|
+
_usable_in_char_class: bool = False
|
|
36
|
+
|
|
37
|
+
# ----------------------------------------------------------------------
|
|
38
|
+
# 1) Abstract Interface (mandatory for subclasses)
|
|
39
|
+
# ----------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def to_pattern(self) -> str:
|
|
43
|
+
"""Return this node's own regex fragment, WITHOUT any wrapping
|
|
44
|
+
that depends on where it is embedded.
|
|
45
|
+
|
|
46
|
+
Containers call `child.render(self._precedence)` on their
|
|
47
|
+
children instead of `child.to_pattern()` directly, so that
|
|
48
|
+
wrapping decisions are made once, in one place (`render`), never
|
|
49
|
+
duplicated per-container. Implementations of `to_pattern` should
|
|
50
|
+
therefore never wrap their own output in `(?:...)` for precedence
|
|
51
|
+
reasons — only for syntax that is mandatory regardless of context
|
|
52
|
+
(e.g. `Group` always emits its own parentheses; that is not a
|
|
53
|
+
precedence wrap, it is the node's literal syntax).
|
|
54
|
+
"""
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
# ----------------------------------------------------------------------
|
|
58
|
+
# 2) Fixed-Length Introspection (Point 3 — lookbehind validation)
|
|
59
|
+
# ----------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def fixed_length(self) -> int | None:
|
|
62
|
+
"""Return this node's match length in characters if — and only
|
|
63
|
+
if — it is fixed regardless of input, otherwise `None`.
|
|
64
|
+
|
|
65
|
+
Used by `Lookaround` (direction=BEHIND) to fail fast at
|
|
66
|
+
construction time when Python's `re` would otherwise reject a
|
|
67
|
+
variable-length lookbehind, instead of surfacing that failure
|
|
68
|
+
only later at `re.compile()`.
|
|
69
|
+
|
|
70
|
+
Default: `None` (unknown / variable). A node that CAN determine
|
|
71
|
+
a fixed length overrides this:
|
|
72
|
+
* `Literal` -> `len(text)`
|
|
73
|
+
* `CharacterType` / `CharacterClass` / `Anchor` (non-empty-width
|
|
74
|
+
anchors return 0) -> a constant
|
|
75
|
+
* `Sequence` -> sum of children's fixed lengths, or `None` if any
|
|
76
|
+
child is `None`
|
|
77
|
+
* `Repeat` -> `min * inner_length` when `min == max` and inner has
|
|
78
|
+
a fixed length, else `None`
|
|
79
|
+
* `Alternation` -> the shared length if every branch has the SAME
|
|
80
|
+
fixed length, else `None` (Python's `re` does support
|
|
81
|
+
same-length alternation inside a lookbehind)
|
|
82
|
+
|
|
83
|
+
Never guess: returning a wrong non-`None` value here would let an
|
|
84
|
+
invalid lookbehind slip past construction-time validation only to
|
|
85
|
+
fail confusingly later, or (worse) silently compile something
|
|
86
|
+
other than what the user intended.
|
|
87
|
+
"""
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def needs_wrap_for_repeat(self) -> bool:
|
|
91
|
+
"""Return True if this node, despite rendering at ATOM precedence
|
|
92
|
+
(no wrap from `render`'s ordinary precedence comparison), still
|
|
93
|
+
needs an explicit `(?:...)` wrap before a `Repeat` quantifier can
|
|
94
|
+
be applied to it as a single unit.
|
|
95
|
+
|
|
96
|
+
Exists for exactly one case in the whole hierarchy: a
|
|
97
|
+
multi-character `Literal`. Quantifiers in `re` bind to the single
|
|
98
|
+
preceding token — a character, an escape sequence, a character
|
|
99
|
+
class, or a group — never to more than one character of plain
|
|
100
|
+
text. `Literal`'s own `_precedence` correctly stays at ATOM (it
|
|
101
|
+
needs no wrap when embedded in a `Sequence` or `Alternation`),
|
|
102
|
+
so `_precedence` alone cannot express this narrower,
|
|
103
|
+
repeat-specific constraint; this separate hook does.
|
|
104
|
+
|
|
105
|
+
Default: `False` — every other ATOM-precedence node (`Anchor`,
|
|
106
|
+
`CharacterType`, `CharacterClass`, `Group`, `Lookaround`,
|
|
107
|
+
`GroupReference`, `CharCode`) is already exactly one token by
|
|
108
|
+
construction and never needs this.
|
|
109
|
+
"""
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
def to_char_class_fragment(self) -> str:
|
|
113
|
+
"""Return this node's representation for use as a standalone item
|
|
114
|
+
inside a `CharacterClass` (`[...]`), where escaping rules differ
|
|
115
|
+
from the rest of a pattern (Point 4).
|
|
116
|
+
|
|
117
|
+
Default: identical to `to_pattern()` — correct for every node
|
|
118
|
+
whose escape syntax is genuinely the same inside and outside a
|
|
119
|
+
character class (`CharacterType`, `CharCode`). Only `Literal`
|
|
120
|
+
overrides this, since a character class only ever needs to
|
|
121
|
+
escape `] ^ - \\`, a different and much smaller set than the
|
|
122
|
+
general-purpose `re.escape` used by `Literal.to_pattern`.
|
|
123
|
+
|
|
124
|
+
Only ever called on nodes where `_usable_in_char_class` is
|
|
125
|
+
`True` — `CharacterClass.__init__` is responsible for that
|
|
126
|
+
check; this method itself does not re-validate it.
|
|
127
|
+
"""
|
|
128
|
+
return self.to_pattern()
|
|
129
|
+
|
|
130
|
+
# ----------------------------------------------------------------------
|
|
131
|
+
# 3) Precedence-Aware Rendering (Point 1)
|
|
132
|
+
# ----------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def render(self, parent_precedence: "_Precedence") -> str:
|
|
135
|
+
"""Render this node for embedding inside a parent whose own
|
|
136
|
+
binding power is `parent_precedence`.
|
|
137
|
+
|
|
138
|
+
Wraps `to_pattern()`'s output in a non-capturing group exactly
|
|
139
|
+
when this node's own `_precedence` is strictly lower than the
|
|
140
|
+
parent's — e.g. an `Alternation` embedded inside a `Sequence`
|
|
141
|
+
gets wrapped, because ALTERNATION < SEQUENCE. A node whose
|
|
142
|
+
precedence is ATOM (the default) is never wrapped, since it is
|
|
143
|
+
already self-delimiting.
|
|
144
|
+
|
|
145
|
+
This is the ONLY place precedence wrapping happens. Containers
|
|
146
|
+
(`Sequence`, `Repeat`, ...) must call `child.render(self._precedence)`
|
|
147
|
+
on every child instead of `child.to_pattern()` directly.
|
|
148
|
+
"""
|
|
149
|
+
fragment = self.to_pattern()
|
|
150
|
+
|
|
151
|
+
if self._precedence < parent_precedence:
|
|
152
|
+
return f"(?:{fragment})"
|
|
153
|
+
|
|
154
|
+
return fragment
|
|
155
|
+
|
|
156
|
+
# ----------------------------------------------------------------------
|
|
157
|
+
# 4) Public Interface
|
|
158
|
+
# ----------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def compile(self, flags: int = 0) -> Any:
|
|
161
|
+
"""Compile this node's top-level pattern via `re.compile`.
|
|
162
|
+
|
|
163
|
+
Top-level rendering always uses `to_pattern()` directly (never
|
|
164
|
+
`render()`) — there is no parent context to wrap against at the
|
|
165
|
+
root of the tree.
|
|
166
|
+
"""
|
|
167
|
+
import re
|
|
168
|
+
|
|
169
|
+
return re.compile(self.to_pattern(), flags)
|
|
170
|
+
|
|
171
|
+
# ----------------------------------------------------------------------
|
|
172
|
+
# 5) Operator-Based Composition (+, |)
|
|
173
|
+
# ----------------------------------------------------------------------
|
|
174
|
+
#
|
|
175
|
+
# `+` builds Sequence, `|` builds Alternation — deliberately different
|
|
176
|
+
# operators from Rule's `&`/`|`, since regex composition has no
|
|
177
|
+
# boolean AND: two nodes next to each other in a pattern concatenate,
|
|
178
|
+
# they do not both have to "pass". `|` is the one operator that keeps
|
|
179
|
+
# its meaning from Rule/Action, since regex alternation IS a direct
|
|
180
|
+
# analogue of logical OR.
|
|
181
|
+
|
|
182
|
+
def __add__(self, other: "Regex") -> "Regex":
|
|
183
|
+
"""Concatenate with another node via `node1 + node2`.
|
|
184
|
+
|
|
185
|
+
Equivalent to `Sequence(self, other)`. Returns `NotImplemented`
|
|
186
|
+
for anything that is not a `Regex` instance, letting Python fall
|
|
187
|
+
back to `other.__radd__(self)` or raise `TypeError` as usual.
|
|
188
|
+
"""
|
|
189
|
+
if isinstance(other, Regex):
|
|
190
|
+
from ..containers.Sequence import Sequence
|
|
191
|
+
|
|
192
|
+
return Sequence(self, other)
|
|
193
|
+
return NotImplemented
|
|
194
|
+
|
|
195
|
+
def __radd__(self, other: "Regex") -> "Regex":
|
|
196
|
+
"""Support `other + node` when `other` has no (or a declining) `__add__`."""
|
|
197
|
+
if isinstance(other, Regex):
|
|
198
|
+
from ..containers.Sequence import Sequence
|
|
199
|
+
|
|
200
|
+
return Sequence(other, self)
|
|
201
|
+
return NotImplemented
|
|
202
|
+
|
|
203
|
+
def __or__(self, other: "Regex") -> "Regex":
|
|
204
|
+
"""Combine with another node via alternation: `node1 | node2`.
|
|
205
|
+
|
|
206
|
+
Equivalent to `Alternation(self, other)`.
|
|
207
|
+
"""
|
|
208
|
+
if isinstance(other, Regex):
|
|
209
|
+
from ..containers.Alternation import Alternation
|
|
210
|
+
|
|
211
|
+
return Alternation(self, other)
|
|
212
|
+
return NotImplemented
|
|
213
|
+
|
|
214
|
+
def __ror__(self, other: "Regex") -> "Regex":
|
|
215
|
+
"""Support `other | node` when `other` has no (or a declining) `__or__`."""
|
|
216
|
+
if isinstance(other, Regex):
|
|
217
|
+
from ..containers.Alternation import Alternation
|
|
218
|
+
|
|
219
|
+
return Alternation(other, self)
|
|
220
|
+
return NotImplemented
|
|
221
|
+
|
|
222
|
+
def __repr__(self) -> str:
|
|
223
|
+
"""Return a concise unambiguous representation of the Regex node."""
|
|
224
|
+
return f"{self.__class__.__name__}({self.to_pattern()!r})"
|
|
225
|
+
|
|
226
|
+
_DESIGN_NOTES = """
|
|
227
|
+
# Regex — Base Abstract Class for the Regex DSL
|
|
228
|
+
|
|
229
|
+
## Relationship to Rule / Action
|
|
230
|
+
Same three-part shape: (1) an abstract production method every subclass
|
|
231
|
+
must implement (`to_pattern`, mirroring `is_valid`/`__call__`), (2) a
|
|
232
|
+
public convenience surface (`render`, `compile`), (3) operator-based
|
|
233
|
+
composition delegating to lazily-imported containers, exactly like
|
|
234
|
+
`Rule.__and__`/`Action.__and__` deferring to `AllOf`/`ParallelCompose`.
|
|
235
|
+
|
|
236
|
+
The one new piece with no Rule/Action equivalent is `fixed_length` —
|
|
237
|
+
regex composition has no analogue to "does this predicate pass", but it
|
|
238
|
+
does have "how wide is this fragment", and that question only exists
|
|
239
|
+
because `Lookaround(direction=BEHIND)` needs an answer to it at
|
|
240
|
+
construction time.
|
|
241
|
+
|
|
242
|
+
## Why `render()` lives on the base class, not repeated per container
|
|
243
|
+
Every container (`Sequence`, `Alternation`, `Repeat`, `Group`, ...) needs
|
|
244
|
+
the exact same decision procedure for wrapping a child: compare
|
|
245
|
+
precedences, wrap or don't. Putting that once on `Regex.render()` and
|
|
246
|
+
having containers call `child.render(self._precedence)` means the
|
|
247
|
+
wrapping RULE has exactly one implementation in the whole library — a
|
|
248
|
+
container never decides FOR ITSELF whether to wrap a child, it just
|
|
249
|
+
states its own precedence and asks the child to render into that
|
|
250
|
+
context. This mirrors how `AllOf.build_exception` doesn't reimplement
|
|
251
|
+
diagnostic formatting — it delegates to `build_child_exception`.
|
|
252
|
+
|
|
253
|
+
## Why `_precedence` defaults to ATOM rather than being abstract
|
|
254
|
+
Most nodes in the tree (`Literal`, `CharacterType`, `Anchor`,
|
|
255
|
+
`GroupReference`, `CharCode`, and every container that already emits its
|
|
256
|
+
own delimiters — `Group`, `CharacterClass`, `Lookaround`, `Conditional`)
|
|
257
|
+
are self-delimiting and never need wrapping. Only `Sequence`,
|
|
258
|
+
`Alternation`, and `Repeat` have a real precedence below ATOM and must
|
|
259
|
+
override the class attribute. Making ATOM the default means the large,
|
|
260
|
+
common case (leaf nodes) declares nothing extra — only the few
|
|
261
|
+
containers where it actually matters opt out of the default.
|
|
262
|
+
|
|
263
|
+
## Why `fixed_length` is a concrete method with a safe default, not abstract
|
|
264
|
+
Unlike `to_pattern` (every node MUST be renderable) or `is_valid` on
|
|
265
|
+
`Rule` (every rule MUST be evaluable), most nodes genuinely don't need an
|
|
266
|
+
exact answer to "how long am I" — only `Lookaround` ever asks the
|
|
267
|
+
question. Defaulting to `None` ("unknown/variable") is always SAFE: a
|
|
268
|
+
node that fails to override this when it actually has a fixed length
|
|
269
|
+
only loses the ability to be used inside a lookbehind directly (it can
|
|
270
|
+
still always be wrapped in an explicit, hand-verified way) — it can
|
|
271
|
+
never cause a wrong pattern to compile, because `None` is
|
|
272
|
+
construction-time-rejected by `Lookaround`, never silently accepted.
|
|
273
|
+
|
|
274
|
+
## Point 4 — `_usable_in_char_class` is a flag, not a separate hierarchy
|
|
275
|
+
Chosen deliberately over fully separate `CharacterClassItem`/`Atom` class
|
|
276
|
+
hierarchies (the alternative discussed as "variant 1" in the design
|
|
277
|
+
review): a single `Regex` hierarchy stays consistent with `Rule`'s own
|
|
278
|
+
single-hierarchy shape, and `CharacterClass.__init__` enforces the
|
|
279
|
+
restriction procedurally — via `isinstance(item, Regex) and
|
|
280
|
+
item._usable_in_char_class` — the same procedural-check pattern
|
|
281
|
+
`Rule.__and__` already uses for `__not_rule__`. This is intentionally
|
|
282
|
+
closer to variant 2 of the original write-up (runtime marker, not a
|
|
283
|
+
type-level split) — full static separation would require either
|
|
284
|
+
duplicating `CharacterType`/`CharCode` under two class hierarchies or
|
|
285
|
+
introducing a shared mixin/Protocol layer whose benefit (catching a
|
|
286
|
+
`CharacterClass(WordBoundary())` mistake at type-check time instead of
|
|
287
|
+
at construction time) did not outweigh the added structural complexity
|
|
288
|
+
for this first pass. `CharacterClass`'s own design notes will document
|
|
289
|
+
this decision again alongside its actual constructor.
|
|
290
|
+
|
|
291
|
+
## `compile()` uses `to_pattern()` directly, never `render()`
|
|
292
|
+
There is no parent node at the root of a tree, so there is no
|
|
293
|
+
`parent_precedence` to render against. Calling `render()` at the root
|
|
294
|
+
would require inventing a fake top-level precedence — `to_pattern()` is
|
|
295
|
+
the honest choice: the root always emits its own fragment unwrapped.
|
|
296
|
+
|
|
297
|
+
## `needs_wrap_for_repeat` — a narrower hook than `_precedence`, added for Point 2
|
|
298
|
+
`Repeat` needs to know something `_precedence` genuinely cannot express:
|
|
299
|
+
not "does this need parentheses to avoid changing meaning in a larger
|
|
300
|
+
expression" (that's precedence), but "is this exactly one quantifiable
|
|
301
|
+
token". Those two questions have the same answer for every ATOM node
|
|
302
|
+
except a multi-character `Literal`, which is why this is a separate
|
|
303
|
+
method with a safe default (`False`) rather than a new `_Precedence`
|
|
304
|
+
level — inventing a level for a single-class exception would have
|
|
305
|
+
forced every other atom to reason about a distinction that, for them,
|
|
306
|
+
never varies.
|
|
307
|
+
|
|
308
|
+
## `to_char_class_fragment` — a narrower rendering hook, added for Point 4
|
|
309
|
+
Same shape of decision as `needs_wrap_for_repeat`: the DEFAULT
|
|
310
|
+
implementation (delegate straight to `to_pattern`) is correct for the
|
|
311
|
+
common case (`CharacterType`, `CharCode` — genuinely the same escape
|
|
312
|
+
syntax either side of `[...]`), and only the one node where the two
|
|
313
|
+
contexts actually diverge (`Literal`) overrides it. Kept as a separate
|
|
314
|
+
method rather than a branch inside `to_pattern` itself, because
|
|
315
|
+
`to_pattern`'s job is "render me as a normal fragment" — conflating
|
|
316
|
+
that with "render me as a character-class item" would make every node
|
|
317
|
+
that never appears inside a class carry dead branching for a context
|
|
318
|
+
that can never apply to it.
|
|
319
|
+
|
|
320
|
+
## Why `+` for Sequence instead of reusing `&`
|
|
321
|
+
`Rule.__and__` means "both predicates must hold" — a genuine boolean AND.
|
|
322
|
+
Two regex nodes placed next to each other don't "both have to hold" in
|
|
323
|
+
that sense; they concatenate. Using `&` for that would silently imply a
|
|
324
|
+
semantics this DSL does not have (and `&` is already claimed by
|
|
325
|
+
`Action` for a different, execution-parallel meaning) — `+` reads
|
|
326
|
+
correctly as "these go one after another" and has no prior claimed
|
|
327
|
+
meaning in either sibling library.
|
|
328
|
+
"""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class _Precedence(IntEnum):
|
|
5
|
+
"""Binding-power levels used by `Regex.render()` to decide whether a
|
|
6
|
+
child node must be wrapped in a non-capturing group before being
|
|
7
|
+
embedded inside a parent node.
|
|
8
|
+
|
|
9
|
+
Ordering matches regex operator precedence, from loosest-binding to
|
|
10
|
+
tightest-binding. A child is wrapped whenever its own precedence is
|
|
11
|
+
STRICTLY LOWER than the precedence the parent renders it at — e.g. an
|
|
12
|
+
`Alternation` (ALTERNATION) embedded inside a `Sequence` (SEQUENCE)
|
|
13
|
+
gets wrapped, because ALTERNATION < SEQUENCE.
|
|
14
|
+
|
|
15
|
+
ATOM is intentionally the ceiling: anything already self-delimiting
|
|
16
|
+
(a single character, a `Group`, a `CharacterClass`, a `Lookaround`)
|
|
17
|
+
never needs an extra wrap, regardless of where it is embedded.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
ALTERNATION = 0 # A|B — loosest: spreads across the entire pattern if unwrapped
|
|
21
|
+
SEQUENCE = 1 # AB — concatenation
|
|
22
|
+
REPEAT = 2 # A*, A{m,n}, ... — binds to exactly one preceding unit
|
|
23
|
+
ATOM = 3 # single char, Group(...), CharacterClass, Lookaround, Anchor, ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_DESIGN_NOTES = """
|
|
27
|
+
# _Precedence — Operator Binding Power
|
|
28
|
+
|
|
29
|
+
## Purpose
|
|
30
|
+
Defines binding-power levels for `Regex.render()` to determine when a child
|
|
31
|
+
node requires wrapping in a non-capturing group.
|
|
32
|
+
|
|
33
|
+
## Precedence Hierarchy (Loosest to Tightest)
|
|
34
|
+
1. `ALTERNATION = 0` (`A|B`) — loosest binding.
|
|
35
|
+
2. `SEQUENCE = 1` (`AB`) — concatenation.
|
|
36
|
+
3. `REPEAT = 2` (`A*`, `A{m,n}`) — binds to one preceding unit.
|
|
37
|
+
4. `ATOM = 3` — ceiling (self-delimiting nodes like groups, character classes,
|
|
38
|
+
anchors, never need wrapping).
|
|
39
|
+
|
|
40
|
+
## Wrapping Rule
|
|
41
|
+
A child is wrapped if its precedence is strictly lower than the parent's
|
|
42
|
+
rendering context (`child_precedence < parent_precedence`).
|
|
43
|
+
"""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ._Precedence import _Precedence
|
|
2
|
+
from .Regex import Regex
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_DESIGN_NOTES = """
|
|
6
|
+
# Regex Base Class Sub-Package
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
The `base_class` package provides the foundational abstract syntax tree (AST) building blocks
|
|
10
|
+
for the regex library, including operator overloading, precedence levels, and fragment generation hooks.
|
|
11
|
+
|
|
12
|
+
## Internal Components Registry
|
|
13
|
+
|
|
14
|
+
| Component | Type | Description |
|
|
15
|
+
| :------------ | :--------------- | :--------------------------------------------------------------------------------- |
|
|
16
|
+
| `Regex` | Abstract Class | The root abstract class for all regex AST nodes, defining operators and hooks. |
|
|
17
|
+
| `_Precedence` | Internal Enum | Precedence levels used to determine when parentheses are required during rendering.|
|
|
18
|
+
"""
|