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.
Files changed (127) hide show
  1. simplibs/regex/__init__.py +171 -0
  2. simplibs/regex/base_class/Regex.py +328 -0
  3. simplibs/regex/base_class/_Precedence.py +43 -0
  4. simplibs/regex/base_class/__init__.py +18 -0
  5. simplibs/regex/compiler/RegexPattern.py +220 -0
  6. simplibs/regex/compiler/__init__.py +16 -0
  7. simplibs/regex/compiler/_validations/__init__.py +18 -0
  8. simplibs/regex/compiler/_validations/raise_invalid_pattern_error.py +38 -0
  9. simplibs/regex/compiler/_validations/raise_regex_pattern_invalid_flags_error.py +20 -0
  10. simplibs/regex/compiler/_validations/raise_regex_pattern_invalid_node_error.py +20 -0
  11. simplibs/regex/containers/Alternation.py +106 -0
  12. simplibs/regex/containers/Conditional.py +113 -0
  13. simplibs/regex/containers/Group.py +191 -0
  14. simplibs/regex/containers/Lookaround.py +145 -0
  15. simplibs/regex/containers/Repeat.py +185 -0
  16. simplibs/regex/containers/Sequence.py +100 -0
  17. simplibs/regex/containers/__init__.py +31 -0
  18. simplibs/regex/containers/_validations/__init__.py +67 -0
  19. simplibs/regex/containers/_validations/alternation_and_sequence/__init__.py +16 -0
  20. simplibs/regex/containers/_validations/alternation_and_sequence/raise_node_param_not_regex_error.py +30 -0
  21. simplibs/regex/containers/_validations/alternation_and_sequence/raise_requires_at_least_one_node_error.py +30 -0
  22. simplibs/regex/containers/_validations/conditional/__init__.py +22 -0
  23. simplibs/regex/containers/_validations/conditional/raise_conditional_invalid_id_type_error.py +20 -0
  24. simplibs/regex/containers/_validations/conditional/raise_conditional_invalid_name_error.py +20 -0
  25. simplibs/regex/containers/_validations/conditional/raise_conditional_invalid_numeric_id_error.py +20 -0
  26. simplibs/regex/containers/_validations/conditional/raise_conditional_no_not_regex_error.py +20 -0
  27. simplibs/regex/containers/_validations/conditional/raise_conditional_yes_not_regex_error.py +20 -0
  28. simplibs/regex/containers/_validations/group/__init__.py +28 -0
  29. simplibs/regex/containers/_validations/group/raise_group_atomic_and_flags_conflict_error.py +20 -0
  30. simplibs/regex/containers/_validations/group/raise_group_atomic_and_name_conflict_error.py +20 -0
  31. simplibs/regex/containers/_validations/group/raise_group_flags_off_without_flags_error.py +20 -0
  32. simplibs/regex/containers/_validations/group/raise_group_flags_require_non_capturing_error.py +20 -0
  33. simplibs/regex/containers/_validations/group/raise_group_inner_not_regex_error.py +20 -0
  34. simplibs/regex/containers/_validations/group/raise_group_name_and_flags_conflict_error.py +20 -0
  35. simplibs/regex/containers/_validations/group/raise_group_name_requires_capturing_error.py +20 -0
  36. simplibs/regex/containers/_validations/group/raise_invalid_group_name_error.py +33 -0
  37. simplibs/regex/containers/_validations/lookaround/__init__.py +18 -0
  38. simplibs/regex/containers/_validations/lookaround/raise_lookaround_inner_not_regex_error.py +20 -0
  39. simplibs/regex/containers/_validations/lookaround/raise_lookaround_invalid_direction_error.py +20 -0
  40. simplibs/regex/containers/_validations/lookaround/raise_variable_length_lookbehind_error.py +30 -0
  41. simplibs/regex/containers/_validations/repeat/__init__.py +20 -0
  42. simplibs/regex/containers/_validations/repeat/raise_repeat_inner_not_regex_error.py +20 -0
  43. simplibs/regex/containers/_validations/repeat/raise_repeat_invalid_mode_error.py +20 -0
  44. simplibs/regex/containers/_validations/repeat/raise_repeat_max_less_than_min_error.py +20 -0
  45. simplibs/regex/containers/_validations/repeat/raise_repeat_min_negative_error.py +20 -0
  46. simplibs/regex/containers/enums/LookaroundDirection.py +23 -0
  47. simplibs/regex/containers/enums/RepeatMode.py +23 -0
  48. simplibs/regex/containers/enums/__init__.py +19 -0
  49. simplibs/regex/elements/Anchor.py +81 -0
  50. simplibs/regex/elements/CharCode.py +116 -0
  51. simplibs/regex/elements/CharacterClass.py +130 -0
  52. simplibs/regex/elements/CharacterRange.py +94 -0
  53. simplibs/regex/elements/CharacterType.py +77 -0
  54. simplibs/regex/elements/GroupReference.py +94 -0
  55. simplibs/regex/elements/Literal.py +141 -0
  56. simplibs/regex/elements/RawPattern.py +119 -0
  57. simplibs/regex/elements/__init__.py +34 -0
  58. simplibs/regex/elements/_helpers/__init__.py +15 -0
  59. simplibs/regex/elements/_helpers/char_class_escape.py +34 -0
  60. simplibs/regex/elements/_validations/__init__.py +58 -0
  61. simplibs/regex/elements/_validations/anorch/__init__.py +14 -0
  62. simplibs/regex/elements/_validations/anorch/raise_anchor_invalid_kind_error.py +20 -0
  63. simplibs/regex/elements/_validations/char_code/__init__.py +20 -0
  64. simplibs/regex/elements/_validations/char_code/raise_char_code_invalid_kind_error.py +20 -0
  65. simplibs/regex/elements/_validations/char_code/raise_char_code_invalid_named_value_error.py +20 -0
  66. simplibs/regex/elements/_validations/char_code/raise_char_code_invalid_type_error.py +21 -0
  67. simplibs/regex/elements/_validations/char_code/raise_char_code_out_of_range_error.py +21 -0
  68. simplibs/regex/elements/_validations/character_class/__init__.py +18 -0
  69. simplibs/regex/elements/_validations/character_class/raise_char_class_empty_error.py +20 -0
  70. simplibs/regex/elements/_validations/character_class/raise_char_class_item_not_allowed_error.py +20 -0
  71. simplibs/regex/elements/_validations/character_class/raise_multi_char_literal_in_char_class_error.py +20 -0
  72. simplibs/regex/elements/_validations/character_range/__init__.py +18 -0
  73. simplibs/regex/elements/_validations/character_range/raise_character_range_invalid_boundary_error.py +20 -0
  74. simplibs/regex/elements/_validations/character_range/raise_character_range_no_standalone_pattern_error.py +20 -0
  75. simplibs/regex/elements/_validations/character_range/raise_character_range_start_after_end_error.py +20 -0
  76. simplibs/regex/elements/_validations/character_type/__init__.py +14 -0
  77. simplibs/regex/elements/_validations/character_type/raise_character_type_invalid_kind_error.py +20 -0
  78. simplibs/regex/elements/_validations/group_reference/__init__.py +18 -0
  79. simplibs/regex/elements/_validations/group_reference/raise_group_reference_invalid_identifier_error.py +20 -0
  80. simplibs/regex/elements/_validations/group_reference/raise_group_reference_invalid_type_error.py +20 -0
  81. simplibs/regex/elements/_validations/group_reference/raise_group_reference_numeric_out_of_range_error.py +20 -0
  82. simplibs/regex/elements/_validations/literal/__init__.py +18 -0
  83. simplibs/regex/elements/_validations/literal/raise_literal_empty_error.py +20 -0
  84. simplibs/regex/elements/_validations/literal/raise_literal_invalid_type_error.py +20 -0
  85. simplibs/regex/elements/_validations/literal/raise_literal_not_single_char_error.py +20 -0
  86. simplibs/regex/elements/enums/AnchorKind.py +30 -0
  87. simplibs/regex/elements/enums/CharCodeKind.py +28 -0
  88. simplibs/regex/elements/enums/CharacterTypeKind.py +30 -0
  89. simplibs/regex/elements/enums/__init__.py +21 -0
  90. simplibs/regex/flags/Flag.py +54 -0
  91. simplibs/regex/flags/__init__.py +16 -0
  92. simplibs/regex/presets/__init__.py +16 -0
  93. simplibs/regex/presets/anchors/END.py +17 -0
  94. simplibs/regex/presets/anchors/END_STRING.py +16 -0
  95. simplibs/regex/presets/anchors/NON_WORD_BOUNDARY.py +17 -0
  96. simplibs/regex/presets/anchors/START.py +17 -0
  97. simplibs/regex/presets/anchors/START_STRING.py +17 -0
  98. simplibs/regex/presets/anchors/WORD_BOUNDARY.py +17 -0
  99. simplibs/regex/presets/anchors/__init__.py +25 -0
  100. simplibs/regex/presets/character_types/DIGIT.py +18 -0
  101. simplibs/regex/presets/character_types/NON_DIGIT.py +18 -0
  102. simplibs/regex/presets/character_types/NON_WHITESPACE.py +18 -0
  103. simplibs/regex/presets/character_types/NON_WORD.py +18 -0
  104. simplibs/regex/presets/character_types/WHITESPACE.py +18 -0
  105. simplibs/regex/presets/character_types/WORD.py +18 -0
  106. simplibs/regex/presets/character_types/__init__.py +25 -0
  107. simplibs/regex/presets/groups/ATOMIC_GROUP.py +19 -0
  108. simplibs/regex/presets/groups/NAMED_GROUP.py +20 -0
  109. simplibs/regex/presets/groups/NON_CAPTURING.py +19 -0
  110. simplibs/regex/presets/groups/__init__.py +19 -0
  111. simplibs/regex/presets/lookaround/LOOKAHEAD.py +19 -0
  112. simplibs/regex/presets/lookaround/LOOKBEHIND.py +19 -0
  113. simplibs/regex/presets/lookaround/NEGATIVE_LOOKAHEAD.py +19 -0
  114. simplibs/regex/presets/lookaround/NEGATIVE_LOOKBEHIND.py +19 -0
  115. simplibs/regex/presets/lookaround/__init__.py +21 -0
  116. simplibs/regex/presets/quantifiers/AT_LEAST.py +22 -0
  117. simplibs/regex/presets/quantifiers/BETWEEN.py +25 -0
  118. simplibs/regex/presets/quantifiers/EXACTLY.py +21 -0
  119. simplibs/regex/presets/quantifiers/ONE_OR_MORE.py +21 -0
  120. simplibs/regex/presets/quantifiers/OPTIONAL.py +19 -0
  121. simplibs/regex/presets/quantifiers/ZERO_OR_MORE.py +20 -0
  122. simplibs/regex/presets/quantifiers/__init__.py +25 -0
  123. simplibs_regex-0.1.0.dist-info/METADATA +298 -0
  124. simplibs_regex-0.1.0.dist-info/RECORD +127 -0
  125. simplibs_regex-0.1.0.dist-info/WHEEL +5 -0
  126. simplibs_regex-0.1.0.dist-info/licenses/LICENSE +21 -0
  127. 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
+ """