diff options
| author | natsuoto <[email protected]> | 2026-06-26 19:23:09 +0530 |
|---|---|---|
| committer | natsuoto <[email protected]> | 2026-06-26 19:23:09 +0530 |
| commit | 39f16caea1a92c9fb3e7d9c55cdcc20a058174ee (patch) | |
| tree | f51cbe04af8a387d7d6e2e96eb2a746cca318669 /tests/builder/builder.test.py | |
| parent | 21ec7908c94839fff63b5275871cf84e92787a08 (diff) | |
| download | edify-39f16caea1a92c9fb3e7d9c55cdcc20a058174ee.tar.xz edify-39f16caea1a92c9fb3e7d9c55cdcc20a058174ee.zip | |
chore!: package reorg, toolchain migration to uv+ruff+pytest+hatchling, Python 3.11 floor
Drops the `src/` layout. The package lives at `edify/` at repo root, split
into single-purpose subpackages: `builder/` (mixins composing RegexBuilder),
`elements/` (sealed dataclass AST), `compile/` (per-family render functions
plus dispatch), `validate/`, `errors/` (typed exception hierarchy), and
`library/` (per-validator files in family folders). Pattern, serialize,
and result subpackages ship as skeletons.
Replaces tox / virtualenv / flake8+isort+black / setup.py+setup.cfg /
pytest.ini / .coveragerc / .bumpversion.cfg / MANIFEST.in / ci/ /
tests.local.sh with a unified uv + ruff + pytest + hatchling + hatch-vcs
stack driven entirely from pyproject.toml. CI workflow rewritten to
`uv sync --frozen` + `uv run pytest` per matrix entry; required-status-
check names preserved for `check`, `docs`, `py3X (ubuntu)`, and
`pypy311 (ubuntu)`. `py39` / `py310` / `pypy310` dropped (3.11 floor).
Pre-commit replaced with ruff hooks (lint + format), all hook repos
SHA-pinned to commit SHAs.
Tests reorganised under `tests/builder/` and `tests/library/<family>/`
with the `*.test.py` naming shape. 173 tests pass, 100% line coverage.
Behaviour changes versus 0.3 (each will get a documented upgrade-guide
entry once #80 lands):
- `to_regex_string()` returns the bare pattern (no `/.../` wrap)
- `to_regex()` on `named_back_reference` compiles successfully via
`(?P=name)` instead of raising
- builder errors raise typed `EdifyError` subclasses instead of bare
`Exception` (catch via `except EdifyError`)
- internal helpers (`apply_quantifier`, `frame_creating_element`,
`evaluate`, `state`, etc.) removed outright per the 0.3 → 1.0
stub-free policy
Closes #68
Closes #69
Closes #253
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat (limited to 'tests/builder/builder.test.py')
| -rw-r--r-- | tests/builder/builder.test.py | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/tests/builder/builder.test.py b/tests/builder/builder.test.py new file mode 100644 index 0000000..062e1ae --- /dev/null +++ b/tests/builder/builder.test.py @@ -0,0 +1,554 @@ +import re + +import pytest + +from edify import RegexBuilder +from edify.errors.anchors import StartInputAlreadyDefinedError +from edify.errors.captures import InvalidTotalCaptureGroupsIndexError +from edify.errors.input import MustBeInstanceError, MustBeSingleCharacterError +from edify.errors.naming import ( + CannotCreateDuplicateNamedGroupError, + NamedGroupDoesNotExistError, + NameNotValidError, +) +from edify.errors.structure import CannotEndWhileBuildingRootExpressionError + +simple_se = RegexBuilder().string("hello").any_char().string("world") +flags_se = RegexBuilder().multi_line().ignore_case().string("hello").any_char().string("world") +start_end_se = ( + RegexBuilder().start_of_input().string("hello").any_char().string("world").end_of_input() +) +nc_se = ( + RegexBuilder() + .named_capture("module") + .exactly(2) + .any_char() + .end() + .named_back_reference("module") +) +indexed_back_reference_se = RegexBuilder().capture().exactly(2).any_char().end().back_reference(1) +nested_se = RegexBuilder().exactly(2).any_char() +first_layer_se = ( + RegexBuilder() + .string("outer begin") + .named_capture("inner_subexpression") + .optional() + .subexpression(nested_se) + .end() + .string("outer end") +) + + +def regex_equality(regex, rb_expression): + regex_str = str(regex) + rb_expression_str = rb_expression.to_regex_string() + assert regex_str == str(rb_expression_str) + + +def regex_compilation(regex, rb_expression, f=0): + rb_expression_c = rb_expression.to_regex() + assert re.compile(regex, flags=f) == rb_expression_c + + +def test_empty_regex(): + expr = RegexBuilder() + regex_equality("(?:)", expr) + regex_compilation("(?:)", expr) + + +def test_flag_a(): + expr = RegexBuilder().ascii_only() + regex_equality("(?:)", expr) + regex_compilation("(?:)", expr, re.A) + + +def test_flag_d(): + expr = RegexBuilder().debug() + regex_equality("(?:)", expr) + regex_compilation("(?:)", expr, re.DEBUG) + + +def test_flag_i(): + expr = RegexBuilder().ignore_case() + regex_equality("(?:)", expr) + regex_compilation("(?:)", expr, re.I) + + +def test_flag_m(): + expr = RegexBuilder().multi_line() + regex_equality("(?:)", expr) + regex_compilation("(?:)", expr, re.M) + + +def test_flag_s(): + expr = RegexBuilder().dot_all() + regex_equality("(?:)", expr) + regex_compilation("(?:)", expr, re.S) + + +def test_flag_x(): + expr = RegexBuilder().verbose() + regex_equality("(?:)", expr) + regex_compilation("(?:)", expr, re.X) + + +def test_any_char(): + expr = RegexBuilder().any_char() + regex_equality(".", expr) + regex_compilation(".", expr) + + +def test_whitespace_char(): + expr = RegexBuilder().whitespace_char() + regex_equality("\\s", expr) + regex_compilation("\\s", expr) + + +def test_non_whitespace_char(): + expr = RegexBuilder().non_whitespace_char() + regex_equality("\\S", expr) + regex_compilation("\\S", expr) + + +def test_digit(): + expr = RegexBuilder().digit() + regex_equality("\\d", expr) + regex_compilation("\\d", expr) + + +def test_non_digit(): + expr = RegexBuilder().non_digit() + regex_equality("\\D", expr) + regex_compilation("\\D", expr) + + +def test_word(): + expr = RegexBuilder().word() + regex_equality("\\w", expr) + regex_compilation("\\w", expr) + + +def test_non_word(): + expr = RegexBuilder().non_word() + regex_equality("\\W", expr) + regex_compilation("\\W", expr) + + +def test_word_boundary(): + expr = RegexBuilder().word_boundary() + regex_equality("\\b", expr) + regex_compilation("\\b", expr) + + +def test_non_word_boundary(): + expr = RegexBuilder().non_word_boundary() + regex_equality("\\B", expr) + regex_compilation("\\B", expr) + + +def test_new_line(): + expr = RegexBuilder().new_line() + regex_equality("\\n", expr) + regex_compilation("\\n", expr) + + +def test_carriage_return(): + expr = RegexBuilder().carriage_return() + regex_equality("\\r", expr) + regex_compilation("\\r", expr) + + +def test_tab(): + expr = RegexBuilder().tab() + regex_equality("\\t", expr) + regex_compilation("\\t", expr) + + +def test_null_byte(): + expr = RegexBuilder().null_byte() + regex_equality("\\0", expr) + regex_compilation("\\0", expr) + + +def test_any_of_basic(): + expr = RegexBuilder().any_of().string("hello").digit().word().char(".").char("#").end() + regex_equality("(?:hello|\\d|\\w|[\\.\\#])", expr) + regex_compilation("(?:hello|\\d|\\w|[\\.\\#])", expr) + + +def test_any_of_range_fusion(): + expr = ( + RegexBuilder() + .any_of() + .range("a", "z") + .range("A", "Z") + .range("0", "9") + .char(".") + .char("#") + .end() + ) + regex_equality("[a-zA-Z0-9\\.\\#]", expr) + regex_compilation("[a-zA-Z0-9\\.\\#]", expr) + + +def test_any_of_range_fusion_with_other_choices(): + expr = ( + RegexBuilder() + .any_of() + .range("a", "z") + .range("A", "Z") + .range("0", "9") + .char(".") + .char("#") + .string("hello") + .end() + ) + regex_equality("(?:hello|[a-zA-Z0-9\\.\\#])", expr) + regex_compilation("(?:hello|[a-zA-Z0-9\\.\\#])", expr) + + +def test_capture(): + expr = RegexBuilder().capture().string("hello ").word().char("!").end() + regex_equality("(hello \\w!)", expr) + regex_compilation("(hello \\w!)", expr) + + +def test_named_capture(): + expr = RegexBuilder().named_capture("this_is_the_name").string("hello ").word().char("!").end() + regex_equality("(?P<this_is_the_name>hello \\w!)", expr) + regex_compilation("(?P<this_is_the_name>hello \\w!)", expr) + + +def test_bad_name_error(): + with pytest.raises(NameNotValidError): + (RegexBuilder().named_capture("hello world").string("hello ").word().char("!").end()) + + +def test_same_name_error(): + with pytest.raises(CannotCreateDuplicateNamedGroupError): + ( + RegexBuilder() + .named_capture("hello") + .string("hello ") + .word() + .char("!") + .end() + .named_capture("hello") + .string("hello ") + .word() + .char("!") + .end() + ) + + +def test_named_back_reference(): + expr = ( + RegexBuilder() + .named_capture("this_is_the_name") + .string("hello ") + .word() + .char("!") + .end() + .named_back_reference("this_is_the_name") + ) + regex_equality("(?P<this_is_the_name>hello \\w!)(?P=this_is_the_name)", expr) + regex_compilation("(?P<this_is_the_name>hello \\w!)(?P=this_is_the_name)", expr) + + +def test_named_back_reference_no_cg_exists(): + with pytest.raises(NamedGroupDoesNotExistError): + RegexBuilder().named_back_reference("not_here") + + +def test_back_reference(): + expr = RegexBuilder().capture().string("hello ").word().char("!").end().back_reference(1) + regex_equality("(hello \\w!)\\1", expr) + regex_compilation("(hello \\w!)\\1", expr) + + +def test_back_reference_no_cg_exists(): + with pytest.raises(InvalidTotalCaptureGroupsIndexError): + RegexBuilder().back_reference(1) + + +def test_group(): + expr = RegexBuilder().group().string("hello ").word().char("!").end() + regex_equality("(?:hello \\w!)", expr) + regex_compilation("(?:hello \\w!)", expr) + + +def test_error_when_called_with_no_stack(): + with pytest.raises(CannotEndWhileBuildingRootExpressionError): + RegexBuilder().end() + + +def test_assert_ahead(): + expr = RegexBuilder().assert_ahead().range("a", "f").end().range("a", "z") + regex_equality("(?=[a-f])[a-z]", expr) + regex_compilation("(?=[a-f])[a-z]", expr) + + +def test_assert_behind(): + expr = RegexBuilder().assert_behind().string("hello ").end().range("a", "z") + regex_equality("(?<=hello )[a-z]", expr) + regex_compilation("(?<=hello )[a-z]", expr) + + +def test_assert_not_ahead(): + expr = RegexBuilder().assert_not_ahead().range("a", "f").end().range("0", "9") + regex_equality("(?![a-f])[0-9]", expr) + regex_compilation("(?![a-f])[0-9]", expr) + + +def test_assert_not_behind(): + expr = RegexBuilder().assert_not_behind().string("hello ").end().range("a", "z") + regex_equality("(?<!hello )[a-z]", expr) + regex_compilation("(?<!hello )[a-z]", expr) + + +def test_optional(): + expr = RegexBuilder().optional().word() + regex_equality("\\w?", expr) + regex_compilation("\\w?", expr) + + +def test_zero_or_more(): + expr = RegexBuilder().zero_or_more().word() + regex_equality("\\w*", expr) + regex_compilation("\\w*", expr) + + +def test_zero_or_more_lazy(): + expr = RegexBuilder().zero_or_more_lazy().word() + regex_equality("\\w*?", expr) + regex_compilation("\\w*?", expr) + + +def test_one_or_more(): + expr = RegexBuilder().one_or_more().word() + regex_equality("\\w+", expr) + regex_compilation("\\w+", expr) + + +def test_one_or_more_lazy(): + expr = RegexBuilder().one_or_more_lazy().word() + regex_equality("\\w+?", expr) + regex_compilation("\\w+?", expr) + + +def test_exactly(): + expr = RegexBuilder().exactly(3).word() + regex_equality("\\w{3}", expr) + regex_compilation("\\w{3}", expr) + + +def test_at_least(): + expr = RegexBuilder().at_least(3).word() + regex_equality("\\w{3,}", expr) + regex_compilation("\\w{3,}", expr) + + +def test_between(): + expr = RegexBuilder().between(3, 5).word() + regex_equality("\\w{3,5}", expr) + regex_compilation("\\w{3,5}", expr) + + +def test_between_lazy(): + expr = RegexBuilder().between_lazy(3, 5).word() + regex_equality("\\w{3,5}?", expr) + regex_compilation("\\w{3,5}?", expr) + + +def test_start_of_input(): + expr = RegexBuilder().start_of_input() + regex_equality("^", expr) + regex_compilation("^", expr) + + +def test_end_of_input(): + expr = RegexBuilder().end_of_input() + regex_equality("$", expr) + regex_compilation("$", expr) + + +def test_any_of_chars(): + expr = RegexBuilder().any_of_chars("aeiou.-") + regex_equality("[aeiou\\.\\-]", expr) + regex_compilation("[aeiou\\.\\-]", expr) + + +def test_anything_but_chars(): + expr = RegexBuilder().anything_but_chars("aeiou.-") + regex_equality("[^aeiou\\.\\-]", expr) + regex_compilation("[^aeiou\\.\\-]", expr) + + +def test_anything_but_string(): + expr = RegexBuilder().anything_but_string("aeiou.") + regex_equality("(?:[^a][^e][^i][^o][^u][^\\][^.])", expr) + regex_compilation("(?:[^a][^e][^i][^o][^u][^\\][^.])", expr) + + +def test_anything_but_range(): + expr = RegexBuilder().anything_but_range("a", "z") + regex_equality("[^a-z]", expr) + regex_compilation("[^a-z]", expr) + expr = RegexBuilder().anything_but_range("0", "9") + regex_equality("[^0-9]", expr) + regex_compilation("[^0-9]", expr) + + +def test_string(): + expr = RegexBuilder().string("hello") + regex_equality("hello", expr) + regex_compilation("hello", expr) + + +def test_string_escapes_special_chars_with_strings_of_len_1(): + expr = RegexBuilder().string("^").string("hello") + regex_equality("\\^hello", expr) + regex_compilation("\\^hello", expr) + + +def test_char(): + expr = RegexBuilder().char("a") + regex_equality("a", expr) + regex_compilation("a", expr) + + +def test_char_more_than_one_error(): + with pytest.raises(MustBeSingleCharacterError): + RegexBuilder().char("hello") + + +def test_range(): + expr = RegexBuilder().range("a", "z") + regex_equality("[a-z]", expr) + regex_compilation("[a-z]", expr) + + +def test_must_be_instance_error(): + with pytest.raises(MustBeInstanceError): + RegexBuilder().subexpression("nope") + + +def test_simple_se(): + expr = ( + RegexBuilder() + .start_of_input() + .at_least(3) + .digit() + .subexpression(simple_se) + .range("0", "9") + .end_of_input() + ) + regex_equality("^\\d{3,}hello.world[0-9]$", expr) + regex_compilation("^\\d{3,}hello.world[0-9]$", expr) + + +def test_simple_quantified_se(): + expr = ( + RegexBuilder() + .start_of_input() + .at_least(3) + .digit() + .one_or_more() + .subexpression(simple_se) + .range("0", "9") + .end_of_input() + ) + regex_equality("^\\d{3,}(?:hello.world)+[0-9]$", expr) + regex_compilation("^\\d{3,}(?:hello.world)+[0-9]$", expr) + + +def test_flags_se(): + expr = ( + RegexBuilder() + .dot_all() + .start_of_input() + .at_least(3) + .digit() + .subexpression(flags_se, ignore_flags=False) + .range("0", "9") + .end_of_input() + ) + regex_equality("^\\d{3,}hello.world[0-9]$", expr) + regex_compilation("^\\d{3,}hello.world[0-9]$", expr, f=re.M | re.I | re.S) + + +def test_flags_se_ignore_flags(): + expr = ( + RegexBuilder() + .dot_all() + .start_of_input() + .at_least(3) + .digit() + .subexpression(flags_se) + .range("0", "9") + .end_of_input() + ) + regex_equality("^\\d{3,}hello.world[0-9]$", expr) + regex_compilation("^\\d{3,}hello.world[0-9]$", expr, f=re.S) + + +def test_ignore_start_and_end(): + expr = RegexBuilder().at_least(3).digit().subexpression(start_end_se).range("0", "9") + regex_equality("\\d{3,}hello.world[0-9]", expr) + regex_compilation("\\d{3,}hello.world[0-9]", expr) + + +def test_start_defined_in_me_and_se(): + with pytest.raises(StartInputAlreadyDefinedError): + ( + RegexBuilder() + .start_of_input() + .at_least(3) + .digit() + .subexpression(start_end_se, ignore_start_and_end=False) + .range("0", "9") + ) + + +def test_no_namespacing(): + expr = RegexBuilder().at_least(3).digit().subexpression(nc_se).range("0", "9") + regex_equality("\\d{3,}(?P<module>.{2})(?P=module)[0-9]", expr) + regex_compilation("\\d{3,}(?P<module>.{2})(?P=module)[0-9]", expr) + + +def test_namespacing(): + expr = RegexBuilder().at_least(3).digit().subexpression(nc_se, namespace="yolo").range("0", "9") + regex_equality("\\d{3,}(?P<yolomodule>.{2})(?P=yolomodule)[0-9]", expr) + regex_compilation("\\d{3,}(?P<yolomodule>.{2})(?P=yolomodule)[0-9]", expr) + + +def test_indexed_back_referencing(): + expr = ( + RegexBuilder() + .capture() + .at_least(3) + .digit() + .end() + .subexpression(indexed_back_reference_se) + .back_reference(1) + .range("0", "9") + ) + regex_equality("(\\d{3,})(.{2})\\2\\1[0-9]", expr) + regex_compilation("(\\d{3,})(.{2})\\2\\1[0-9]", expr) + + +def test_deeply_nested_se(): + expr = ( + RegexBuilder() + .capture() + .at_least(3) + .digit() + .end() + .subexpression(first_layer_se) + .back_reference(1) + .range("0", "9") + ) + regex_equality("(\\d{3,})outer begin(?P<inner_subexpression>(?:.{2})?)outer end\\1[0-9]", expr) + regex_compilation( + "(\\d{3,})outer begin(?P<inner_subexpression>(?:.{2})?)outer end\\1[0-9]", expr + ) |
