aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-01 16:53:15 +0530
committernatsuoto <[email protected]>2026-07-01 16:53:15 +0530
commita3a0b80effd34e1ee58e6644ea04e2df2f08dc4b (patch)
tree11609e05d57cff55dbbe0b81f21c7e2363b7011e
parente859ce24e5ffc95653023669f9edf4d3a0344a8e (diff)
downloadedify-a3a0b80effd34e1ee58e6644ea04e2df2f08dc4b.tar.xz
edify-a3a0b80effd34e1ee58e6644ea04e2df2f08dc4b.zip
feat: convenience char classes .letter() .uppercase() .lowercase() .alphanumeric()
-rw-r--r--edify/__init__.py8
-rw-r--r--edify/builder/mixins/classes.py24
-rw-r--r--edify/compile/leaves.py14
-rw-r--r--edify/elements/types/leaves.py20
-rw-r--r--edify/elements/types/union.py12
-rw-r--r--edify/pattern/__init__.py8
-rw-r--r--edify/pattern/classes.py4
-rw-r--r--tests/builder/builder.test.py24
-rw-r--r--tests/pattern/classes.test.py30
9 files changed, 143 insertions, 1 deletions
diff --git a/edify/__init__.py b/edify/__init__.py
index 430ac1c..9c0dee5 100644
--- a/edify/__init__.py
+++ b/edify/__init__.py
@@ -6,15 +6,19 @@ from edify.errors.syntax import EdifySyntaxError
from edify.pattern.anchors import END, START
from edify.pattern.boundaries import NON_WORD_BOUNDARY, WORD_BOUNDARY
from edify.pattern.classes import (
+ ALPHANUMERIC,
ANY_CHAR,
CARRIAGE_RETURN,
DIGIT,
+ LETTER,
+ LOWERCASE,
NEW_LINE,
NON_DIGIT,
NON_WHITESPACE,
NON_WORD,
NULL_BYTE,
TAB,
+ UPPERCASE,
WHITESPACE,
WORD,
)
@@ -61,10 +65,13 @@ def _resolve_installed_version() -> str:
__version__ = _resolve_installed_version()
__all__ = [
+ "ALPHANUMERIC",
"ANY_CHAR",
"CARRIAGE_RETURN",
"DIGIT",
"END",
+ "LETTER",
+ "LOWERCASE",
"NEW_LINE",
"NON_DIGIT",
"NON_WHITESPACE",
@@ -73,6 +80,7 @@ __all__ = [
"NULL_BYTE",
"START",
"TAB",
+ "UPPERCASE",
"WHITESPACE",
"WORD",
"WORD_BOUNDARY",
diff --git a/edify/builder/mixins/classes.py b/edify/builder/mixins/classes.py
index e754bb9..51fb4a0 100644
--- a/edify/builder/mixins/classes.py
+++ b/edify/builder/mixins/classes.py
@@ -11,9 +11,12 @@ from typing import Self
from edify.builder.types.protocol import BuilderProtocol
from edify.elements.types.leaves import (
+ AlphanumericElement,
AnyCharElement,
CarriageReturnElement,
DigitElement,
+ LetterElement,
+ LowercaseElement,
NewLineElement,
NonDigitElement,
NonWhitespaceCharElement,
@@ -21,6 +24,7 @@ from edify.elements.types.leaves import (
NonWordElement,
NullByteElement,
TabElement,
+ UppercaseElement,
WhitespaceCharElement,
WordBoundaryElement,
WordElement,
@@ -94,3 +98,23 @@ class ClassesMixin(BuilderProtocol):
"""Return a new builder with a ``\\0`` (null byte) appended."""
new_state = self._state.with_element_added_to_top(NullByteElement())
return self._with_state(new_state)
+
+ def letter(self) -> Self:
+ """Return a new builder with ``[a-zA-Z]`` (ASCII letter) appended."""
+ new_state = self._state.with_element_added_to_top(LetterElement())
+ return self._with_state(new_state)
+
+ def uppercase(self) -> Self:
+ """Return a new builder with ``[A-Z]`` (ASCII uppercase letter) appended."""
+ new_state = self._state.with_element_added_to_top(UppercaseElement())
+ return self._with_state(new_state)
+
+ def lowercase(self) -> Self:
+ """Return a new builder with ``[a-z]`` (ASCII lowercase letter) appended."""
+ new_state = self._state.with_element_added_to_top(LowercaseElement())
+ return self._with_state(new_state)
+
+ def alphanumeric(self) -> Self:
+ """Return a new builder with ``[a-zA-Z0-9]`` (ASCII alphanumeric) appended."""
+ new_state = self._state.with_element_added_to_top(AlphanumericElement())
+ return self._with_state(new_state)
diff --git a/edify/compile/leaves.py b/edify/compile/leaves.py
index 22102c6..ed60f5b 100644
--- a/edify/compile/leaves.py
+++ b/edify/compile/leaves.py
@@ -8,10 +8,13 @@ never depends on children or quantifier state, so the renderer is a single
from __future__ import annotations
from edify.elements.types.leaves import (
+ AlphanumericElement,
AnyCharElement,
CarriageReturnElement,
DigitElement,
EndOfInputElement,
+ LetterElement,
+ LowercaseElement,
NewLineElement,
NonDigitElement,
NonWhitespaceCharElement,
@@ -21,6 +24,7 @@ from edify.elements.types.leaves import (
NullByteElement,
StartOfInputElement,
TabElement,
+ UppercaseElement,
WhitespaceCharElement,
WordBoundaryElement,
WordElement,
@@ -32,7 +36,7 @@ def render_leaf(element: LeafElement) -> str:
"""Return the regex string produced by the given leaf element.
Args:
- element: One of the 16 leaf element classes.
+ element: One of the 20 leaf element classes.
Returns:
The exact regex fragment (e.g. ``"^"``, ``"\\d"``, ``""`` for noop).
@@ -68,5 +72,13 @@ def render_leaf(element: LeafElement) -> str:
return "\\t"
case NullByteElement():
return "\\0"
+ case LetterElement():
+ return "[a-zA-Z]"
+ case UppercaseElement():
+ return "[A-Z]"
+ case LowercaseElement():
+ return "[a-z]"
+ case AlphanumericElement():
+ return "[a-zA-Z0-9]"
case NoopElement():
return ""
diff --git a/edify/elements/types/leaves.py b/edify/elements/types/leaves.py
index 97ad285..5a1a678 100644
--- a/edify/elements/types/leaves.py
+++ b/edify/elements/types/leaves.py
@@ -94,5 +94,25 @@ class NullByteElement(BaseElement):
@dataclass(frozen=True)
+class LetterElement(BaseElement):
+ """The ``[a-zA-Z]`` ASCII letter character class."""
+
+
+@dataclass(frozen=True)
+class UppercaseElement(BaseElement):
+ """The ``[A-Z]`` ASCII uppercase-letter character class."""
+
+
+@dataclass(frozen=True)
+class LowercaseElement(BaseElement):
+ """The ``[a-z]`` ASCII lowercase-letter character class."""
+
+
+@dataclass(frozen=True)
+class AlphanumericElement(BaseElement):
+ """The ``[a-zA-Z0-9]`` ASCII alphanumeric character class."""
+
+
+@dataclass(frozen=True)
class NoopElement(BaseElement):
"""A no-op marker that emits an empty string in the compile path."""
diff --git a/edify/elements/types/union.py b/edify/elements/types/union.py
index e5a0785..27aed96 100644
--- a/edify/elements/types/union.py
+++ b/edify/elements/types/union.py
@@ -34,10 +34,13 @@ from edify.elements.types.groups import (
SubexpressionElement,
)
from edify.elements.types.leaves import (
+ AlphanumericElement,
AnyCharElement,
CarriageReturnElement,
DigitElement,
EndOfInputElement,
+ LetterElement,
+ LowercaseElement,
NewLineElement,
NonDigitElement,
NonWhitespaceCharElement,
@@ -47,6 +50,7 @@ from edify.elements.types.leaves import (
NullByteElement,
StartOfInputElement,
TabElement,
+ UppercaseElement,
WhitespaceCharElement,
WordBoundaryElement,
WordElement,
@@ -81,6 +85,10 @@ LeafElement = (
| CarriageReturnElement
| TabElement
| NullByteElement
+ | LetterElement
+ | UppercaseElement
+ | LowercaseElement
+ | AlphanumericElement
| NoopElement
)
@@ -137,6 +145,10 @@ Element = (
| CarriageReturnElement
| TabElement
| NullByteElement
+ | LetterElement
+ | UppercaseElement
+ | LowercaseElement
+ | AlphanumericElement
| NoopElement
| CharElement
| StringElement
diff --git a/edify/pattern/__init__.py b/edify/pattern/__init__.py
index 76090d5..76f530f 100644
--- a/edify/pattern/__init__.py
+++ b/edify/pattern/__init__.py
@@ -1,15 +1,19 @@
from edify.pattern.anchors import END, START
from edify.pattern.boundaries import NON_WORD_BOUNDARY, WORD_BOUNDARY
from edify.pattern.classes import (
+ ALPHANUMERIC,
ANY_CHAR,
CARRIAGE_RETURN,
DIGIT,
+ LETTER,
+ LOWERCASE,
NEW_LINE,
NON_DIGIT,
NON_WHITESPACE,
NON_WORD,
NULL_BYTE,
TAB,
+ UPPERCASE,
WHITESPACE,
WORD,
)
@@ -45,10 +49,13 @@ from edify.pattern.factories import (
)
__all__ = [
+ "ALPHANUMERIC",
"ANY_CHAR",
"CARRIAGE_RETURN",
"DIGIT",
"END",
+ "LETTER",
+ "LOWERCASE",
"NEW_LINE",
"NON_DIGIT",
"NON_WHITESPACE",
@@ -57,6 +64,7 @@ __all__ = [
"NULL_BYTE",
"START",
"TAB",
+ "UPPERCASE",
"WHITESPACE",
"WORD",
"WORD_BOUNDARY",
diff --git a/edify/pattern/classes.py b/edify/pattern/classes.py
index 563b956..67e519c 100644
--- a/edify/pattern/classes.py
+++ b/edify/pattern/classes.py
@@ -25,3 +25,7 @@ NEW_LINE: Pattern = Pattern().new_line()
CARRIAGE_RETURN: Pattern = Pattern().carriage_return()
TAB: Pattern = Pattern().tab()
NULL_BYTE: Pattern = Pattern().null_byte()
+LETTER: Pattern = Pattern().letter()
+UPPERCASE: Pattern = Pattern().uppercase()
+LOWERCASE: Pattern = Pattern().lowercase()
+ALPHANUMERIC: Pattern = Pattern().alphanumeric()
diff --git a/tests/builder/builder.test.py b/tests/builder/builder.test.py
index 33aa17d..ddd1997 100644
--- a/tests/builder/builder.test.py
+++ b/tests/builder/builder.test.py
@@ -170,6 +170,30 @@ def test_null_byte():
regex_compilation("\\0", expr)
+def test_letter():
+ expr = RegexBuilder().letter()
+ regex_equality("[a-zA-Z]", expr)
+ regex_compilation("[a-zA-Z]", expr)
+
+
+def test_uppercase():
+ expr = RegexBuilder().uppercase()
+ regex_equality("[A-Z]", expr)
+ regex_compilation("[A-Z]", expr)
+
+
+def test_lowercase():
+ expr = RegexBuilder().lowercase()
+ regex_equality("[a-z]", expr)
+ regex_compilation("[a-z]", expr)
+
+
+def test_alphanumeric():
+ expr = RegexBuilder().alphanumeric()
+ regex_equality("[a-zA-Z0-9]", expr)
+ regex_compilation("[a-zA-Z0-9]", expr)
+
+
def test_any_of_basic():
expr = RegexBuilder().any_of().string("hello").digit().word().char(".").char("#").end()
regex_equality("(?:hello|\\d|\\w|[\\.\\#])", expr)
diff --git a/tests/pattern/classes.test.py b/tests/pattern/classes.test.py
index de6bfc0..8a82353 100644
--- a/tests/pattern/classes.test.py
+++ b/tests/pattern/classes.test.py
@@ -3,15 +3,19 @@
import pytest
from edify import (
+ ALPHANUMERIC,
ANY_CHAR,
CARRIAGE_RETURN,
DIGIT,
+ LETTER,
+ LOWERCASE,
NEW_LINE,
NON_DIGIT,
NON_WHITESPACE,
NON_WORD,
NULL_BYTE,
TAB,
+ UPPERCASE,
WHITESPACE,
WORD,
Pattern,
@@ -32,6 +36,10 @@ from edify import (
(CARRIAGE_RETURN, "\\r"),
(TAB, "\\t"),
(NULL_BYTE, "\\0"),
+ (LETTER, "[a-zA-Z]"),
+ (UPPERCASE, "[A-Z]"),
+ (LOWERCASE, "[a-z]"),
+ (ALPHANUMERIC, "[a-zA-Z0-9]"),
],
)
def test_character_class_constant_compiles_to_expected_regex(constant, expected):
@@ -52,7 +60,29 @@ def test_character_class_constant_compiles_to_expected_regex(constant, expected)
CARRIAGE_RETURN,
TAB,
NULL_BYTE,
+ LETTER,
+ UPPERCASE,
+ LOWERCASE,
+ ALPHANUMERIC,
],
)
def test_character_class_constant_is_a_pattern(constant):
assert isinstance(constant, Pattern)
+
+
+ ("constant", "hit_input", "miss_input"),
+ [
+ (LETTER, "A", "4"),
+ (LETTER, "z", " "),
+ (UPPERCASE, "Q", "q"),
+ (LOWERCASE, "q", "Q"),
+ (ALPHANUMERIC, "4", " "),
+ (ALPHANUMERIC, "A", "!"),
+ ],
+)
+def test_convenience_char_class_constant_matches_expected_characters(
+ constant, hit_input, miss_input
+):
+ assert constant.test(hit_input) is True
+ assert constant.test(miss_input) is False