diff options
| -rw-r--r-- | edify/__init__.py | 8 | ||||
| -rw-r--r-- | edify/builder/mixins/classes.py | 24 | ||||
| -rw-r--r-- | edify/compile/leaves.py | 14 | ||||
| -rw-r--r-- | edify/elements/types/leaves.py | 20 | ||||
| -rw-r--r-- | edify/elements/types/union.py | 12 | ||||
| -rw-r--r-- | edify/pattern/__init__.py | 8 | ||||
| -rw-r--r-- | edify/pattern/classes.py | 4 | ||||
| -rw-r--r-- | tests/builder/builder.test.py | 24 | ||||
| -rw-r--r-- | tests/pattern/classes.test.py | 30 |
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 |
