diff options
| author | Bobby <[email protected]> | 2026-06-30 16:34:20 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-06-30 16:34:20 +0530 |
| commit | bd3b12ff2f30c251c5fc52b43f81ec2f282d53ab (patch) | |
| tree | ce6e3f57da4c7d9af29c39df4dea78e54523cf3d /tests | |
| parent | 21ec7908c94839fff63b5275871cf84e92787a08 (diff) | |
| parent | 6774eab5f8a1bbda930efd38d41d793557057aed (diff) | |
| download | edify-bd3b12ff2f30c251c5fc52b43f81ec2f282d53ab.tar.xz edify-bd3b12ff2f30c251c5fc52b43f81ec2f282d53ab.zip | |
chore!: package reorg + uv/ruff/pytest/hatchling toolchain + Python 3.11 floor (#255)
## Summary
Closes #68 — package reorganization to a single-purpose, dir-per-concern
layout (no `src/`).
Closes #69 — toolchain migration to `uv` + `ruff` + `pytest` +
`hatchling` + `hatch-vcs`, driven entirely from `pyproject.toml`.
Closes #253.
Closes #259 — drop `.editorconfig`; LF / trim / final-newline / Python
indent now enforced by `mixed-line-ending` (added in fixup) + existing
pre-commit hooks + `ruff format`.— Python floor bump to 3.11; drops
`py39` / `py310` / `pypy310` from the CI matrix.
The three issues are paired by construction (dropping `src/` requires
the new build backend's package discovery, and the 3.11 floor lets the
new layout use `typing.Self`, frozen dataclasses, `match`/`case`, and
Python-3.11 idioms throughout).
## Highlights
**Layout** — `edify/builder/` (RegexBuilder + 11 mixin files + `types/`
for state/flags/frame/protocol), `edify/elements/types/` (sealed
dataclass union: 16 leaves, 7 char-shaped, 4 captures, 7 groups, 9
quantifiers, RootElement), `edify/compile/` (per-family render functions
+ dispatch, plus `quantifier/` subpackage for suffix + apply),
`edify/validate/`, `edify/errors/` (typed exception hierarchy),
`edify/library/` (per-validator files in family folders: `email/`,
`ip/`, `date/`, plus flat single-validator files), `pattern/`,
`serialize/`, `result/` skeletons.
**Toolchain** — replaces `tox` / `virtualenv` / `flake8` / `isort` /
`black` / `setup.py` / `setup.cfg` / `pytest.ini` / `.coveragerc` /
`.bumpversion.cfg` / `MANIFEST.in` / `ci/` / `tests.local.sh` with
`pyproject.toml`-driven `uv` + `ruff` + `pytest` + `hatchling` +
`hatch-vcs`. CI workflow rewritten to `uv sync --frozen` + `uv run
pytest`; required-status-check names preserved (`check`, `docs`, `py311
(ubuntu)`, `py312 (ubuntu)`, `py313 (ubuntu)`, `py314 (ubuntu)`,
`pypy311 (ubuntu)`). Pre-commit replaced with `ruff` hooks, all hook
repos pinned by commit SHA.
**Tests** — reorganised under `tests/builder/` and
`tests/library/<family>/` using the `*.test.py` naming shape. 173 tests,
100% line coverage, ruff lint + format clean, `uv build` produces sdist
+ wheel cleanly.
## Behaviour changes versus 0.3
These will get their own upgrade-guide entries when the migration
tooling lands (#80):
- `to_regex_string()` now returns the bare pattern (no `/.../` wrap).
- `to_regex()` on a named back-reference compiles successfully via
`(?P=name)` instead of raising.
- Builder errors raise typed `EdifyError` subclasses
(`MustBeAStringError`, `StartInputAlreadyDefinedError`, etc.) 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.
Diffstat (limited to 'tests')
30 files changed, 1381 insertions, 801 deletions
diff --git a/tests/builder/alternation.test.py b/tests/builder/alternation.test.py new file mode 100644 index 0000000..8d7fe82 --- /dev/null +++ b/tests/builder/alternation.test.py @@ -0,0 +1,13 @@ +"""Tests for alternation rendering branches not covered by the main builder tests.""" + +from edify import RegexBuilder + + +def test_any_of_with_only_non_fusable_members(): + expr = RegexBuilder().any_of().string("hello").digit().end() + assert expr.to_regex_string() == "(?:hello|\\d)" + + +def test_any_of_with_any_of_chars_member(): + expr = RegexBuilder().any_of().any_of_chars("xyz").end() + assert expr.to_regex_string() == "[xyz]" 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 + ) diff --git a/tests/builder/conflict.test.py b/tests/builder/conflict.test.py new file mode 100644 index 0000000..b683c06 --- /dev/null +++ b/tests/builder/conflict.test.py @@ -0,0 +1,20 @@ +"""Tests for anchor conflicts when subexpressions merge with ``ignore_start_and_end=False``.""" + +import pytest + +from edify import RegexBuilder +from edify.errors.anchors import EndInputAlreadyDefinedError, StartInputAlreadyDefinedError + + +def test_parent_with_start_merging_sub_with_start_raises(): + sub = RegexBuilder().start_of_input().digit() + parent = RegexBuilder().start_of_input().digit() + with pytest.raises(StartInputAlreadyDefinedError): + parent.subexpression(sub, ignore_start_and_end=False) + + +def test_parent_with_end_merging_sub_with_end_raises(): + sub = RegexBuilder().digit().end_of_input() + parent = RegexBuilder().digit().end_of_input() + with pytest.raises(EndInputAlreadyDefinedError): + parent.subexpression(sub, ignore_start_and_end=False) diff --git a/tests/builder/frame.test.py b/tests/builder/frame.test.py new file mode 100644 index 0000000..ffd2242 --- /dev/null +++ b/tests/builder/frame.test.py @@ -0,0 +1,17 @@ +"""Test for the ``UnexpectedFrameTypeError`` raise when an AST is built incorrectly.""" + +import pytest + +from edify import RegexBuilder +from edify.builder.types.frame import StackFrame +from edify.elements.types.leaves import DigitElement +from edify.errors.internal import UnexpectedFrameTypeError + + +def test_end_on_unrecognised_frame_type_raises(): + builder = RegexBuilder().capture() + stray_frame = StackFrame(type_node=DigitElement()) + new_state = builder._state.with_frame_pushed(stray_frame) + builder_with_stray = builder._with_state(new_state) + with pytest.raises(UnexpectedFrameTypeError, match="DigitElement"): + builder_with_stray.end() diff --git a/tests/builder/merge.test.py b/tests/builder/merge.test.py new file mode 100644 index 0000000..cc4d513 --- /dev/null +++ b/tests/builder/merge.test.py @@ -0,0 +1,123 @@ +"""Tests that exercise every element kind through the subexpression merge path. + +Each subexpression is built with one particular element kind and merged +into a parent; the resulting regex string is asserted byte-for-byte so the +per-type branches in :mod:`edify.builder.merge` all get exercised. +""" + +from edify import RegexBuilder + + +def _merge(sub: RegexBuilder) -> str: + return RegexBuilder().subexpression(sub).to_regex_string() + + +def test_subexpression_with_capture(): + sub = RegexBuilder().capture().digit().end() + assert _merge(sub) == "(\\d)" + + +def test_subexpression_with_named_capture(): + sub = RegexBuilder().named_capture("token").digit().end() + assert _merge(sub) == "(?P<token>\\d)" + + +def test_subexpression_with_back_reference(): + sub = RegexBuilder().capture().digit().end().back_reference(1) + assert _merge(sub) == "(\\d)\\1" + + +def test_subexpression_with_named_back_reference(): + sub = RegexBuilder().named_capture("token").digit().end().named_back_reference("token") + assert _merge(sub) == "(?P<token>\\d)(?P=token)" + + +def test_subexpression_with_group(): + sub = RegexBuilder().group().digit().end() + assert _merge(sub) == "(?:\\d)" + + +def test_subexpression_with_any_of(): + sub = RegexBuilder().any_of().char("a").char("b").end() + assert _merge(sub) == "[ab]" + + +def test_subexpression_with_nested_subexpression(): + inner = RegexBuilder().digit() + middle = RegexBuilder().subexpression(inner) + assert _merge(middle) == "\\d" + + +def test_subexpression_with_assert_ahead(): + sub = RegexBuilder().assert_ahead().digit().end() + assert _merge(sub) == "(?=\\d)" + + +def test_subexpression_with_assert_not_ahead(): + sub = RegexBuilder().assert_not_ahead().digit().end() + assert _merge(sub) == "(?!\\d)" + + +def test_subexpression_with_assert_behind(): + sub = RegexBuilder().assert_behind().digit().end() + assert _merge(sub) == "(?<=\\d)" + + +def test_subexpression_with_assert_not_behind(): + sub = RegexBuilder().assert_not_behind().digit().end() + assert _merge(sub) == "(?<!\\d)" + + +def test_subexpression_with_optional(): + sub = RegexBuilder().optional().digit() + assert _merge(sub) == "\\d?" + + +def test_subexpression_with_zero_or_more(): + sub = RegexBuilder().zero_or_more().digit() + assert _merge(sub) == "\\d*" + + +def test_subexpression_with_zero_or_more_lazy(): + sub = RegexBuilder().zero_or_more_lazy().digit() + assert _merge(sub) == "\\d*?" + + +def test_subexpression_with_one_or_more(): + sub = RegexBuilder().one_or_more().digit() + assert _merge(sub) == "\\d+" + + +def test_subexpression_with_one_or_more_lazy(): + sub = RegexBuilder().one_or_more_lazy().digit() + assert _merge(sub) == "\\d+?" + + +def test_subexpression_with_exactly(): + sub = RegexBuilder().exactly(3).digit() + assert _merge(sub) == "\\d{3}" + + +def test_subexpression_with_at_least(): + sub = RegexBuilder().at_least(2).digit() + assert _merge(sub) == "\\d{2,}" + + +def test_subexpression_with_between(): + sub = RegexBuilder().between(1, 4).digit() + assert _merge(sub) == "\\d{1,4}" + + +def test_subexpression_with_between_lazy(): + sub = RegexBuilder().between_lazy(1, 4).digit() + assert _merge(sub) == "\\d{1,4}?" + + +def test_subexpression_with_start_of_input_collapses_to_noop(): + sub = RegexBuilder().start_of_input().digit() + assert _merge(sub) == "\\d" + + +def test_subexpression_with_end_of_input_collapses_to_noop(): + sub = RegexBuilder().digit().end_of_input() + assert _merge(sub) == "\\d" diff --git a/tests/builder/passthrough.test.py b/tests/builder/passthrough.test.py new file mode 100644 index 0000000..de45146 --- /dev/null +++ b/tests/builder/passthrough.test.py @@ -0,0 +1,40 @@ +"""Tests for the pass-through branches when subexpression anchors merge without conflict.""" + +from edify import RegexBuilder + + +def test_start_of_input_merges_into_parent_without_existing_start(): + sub = RegexBuilder().start_of_input().digit() + parent = RegexBuilder().digit() + pattern = parent.subexpression(sub, ignore_start_and_end=False).to_regex_string() + assert "^" in pattern + + +def test_end_of_input_merges_into_parent_without_existing_end(): + sub = RegexBuilder().digit().end_of_input() + parent = RegexBuilder().digit() + pattern = parent.subexpression(sub, ignore_start_and_end=False).to_regex_string() + assert "$" in pattern + + +def test_subexpression_called_with_unfinished_expression_raises(): + import pytest + + from edify.errors.structure import CannotCallSubexpressionError + + unfinished_sub = RegexBuilder().capture().digit() + parent = RegexBuilder() + with pytest.raises(CannotCallSubexpressionError): + parent.subexpression(unfinished_sub) + + +def test_to_regex_with_invalid_pattern_raises_failed_to_compile(): + import pytest + + from edify.errors.internal import FailedToCompileRegexError + + expr = RegexBuilder().capture().digit().end().back_reference(1) + state_with_extra = expr._state.with_capture_groups_added(98) + bogus = expr._with_state(state_with_extra).back_reference(99) + with pytest.raises(FailedToCompileRegexError): + bogus.to_regex() diff --git a/tests/builder/validation.test.py b/tests/builder/validation.test.py new file mode 100644 index 0000000..e4898ad --- /dev/null +++ b/tests/builder/validation.test.py @@ -0,0 +1,157 @@ +"""Tests for the validation raise paths across every builder mixin. + +Each test triggers exactly one of the per-method input-validation branches +that the happy-path builder tests never hit. +""" + +import pytest + +from edify import RegexBuilder +from edify.errors.anchors import ( + CannotDefineStartAfterEndError, + EndInputAlreadyDefinedError, + StartInputAlreadyDefinedError, +) +from edify.errors.input import ( + MustBeAStringError, + MustBeIntegerGreaterThanZeroError, + MustBeLessThanError, + MustBeOneCharacterError, + MustBePositiveIntegerError, + MustBeSingleCharacterError, + MustHaveASmallerValueError, +) +from edify.errors.naming import NamedGroupDoesNotExistError + + +def test_start_of_input_twice_raises(): + with pytest.raises(StartInputAlreadyDefinedError): + RegexBuilder().start_of_input().start_of_input() + + +def test_start_of_input_after_end_raises(): + with pytest.raises(CannotDefineStartAfterEndError): + RegexBuilder().end_of_input().start_of_input() + + +def test_end_of_input_twice_raises(): + with pytest.raises(EndInputAlreadyDefinedError): + RegexBuilder().end_of_input().end_of_input() + + +def test_named_capture_non_string_raises(): + with pytest.raises(MustBeAStringError): + RegexBuilder().named_capture(42) + + +def test_named_capture_empty_string_raises(): + with pytest.raises(MustBeOneCharacterError): + RegexBuilder().named_capture("") + + +def test_string_non_string_raises(): + with pytest.raises(MustBeAStringError): + RegexBuilder().string(42) + + +def test_string_empty_raises(): + with pytest.raises(MustBeOneCharacterError): + RegexBuilder().string("") + + +def test_char_non_string_raises(): + with pytest.raises(MustBeAStringError): + RegexBuilder().char(42) + + +def test_range_first_codepoint_not_less_than_second_raises(): + with pytest.raises(MustHaveASmallerValueError): + RegexBuilder().range("z", "a") + + +def test_anything_but_string_non_string_raises(): + with pytest.raises(MustBeAStringError): + RegexBuilder().anything_but_string(42) + + +def test_anything_but_string_empty_raises(): + with pytest.raises(MustBeOneCharacterError): + RegexBuilder().anything_but_string("") + + +def test_anything_but_chars_non_string_raises(): + with pytest.raises(MustBeAStringError): + RegexBuilder().anything_but_chars(42) + + +def test_anything_but_chars_empty_raises(): + with pytest.raises(MustBeOneCharacterError): + RegexBuilder().anything_but_chars("") + + +def test_anything_but_range_multi_char_raises(): + with pytest.raises(MustBeSingleCharacterError): + RegexBuilder().anything_but_range("abc", "z") + + +def test_anything_but_range_ascending_raises(): + with pytest.raises(MustHaveASmallerValueError): + RegexBuilder().anything_but_range("z", "a") + + +def test_exactly_non_positive_raises(): + with pytest.raises(MustBePositiveIntegerError): + RegexBuilder().exactly(0).digit() + + +def test_at_least_non_positive_raises(): + with pytest.raises(MustBePositiveIntegerError): + RegexBuilder().at_least(-1).digit() + + +def test_between_negative_lower_raises(): + with pytest.raises(MustBeIntegerGreaterThanZeroError): + RegexBuilder().between(-1, 5).digit() + + +def test_between_lower_not_less_than_upper_raises(): + with pytest.raises(MustBeLessThanError): + RegexBuilder().between(5, 5).digit() + + +def test_between_lazy_negative_lower_raises(): + with pytest.raises(MustBeIntegerGreaterThanZeroError): + RegexBuilder().between_lazy(-1, 5).digit() + + +def test_between_lazy_lower_not_less_than_upper_raises(): + with pytest.raises(MustBeLessThanError): + RegexBuilder().between_lazy(5, 5).digit() + + +def test_named_back_reference_undeclared_raises(): + with pytest.raises(NamedGroupDoesNotExistError): + RegexBuilder().named_back_reference("missing") + + +def test_to_regex_string_with_open_frame_raises(): + from edify.errors.structure import CannotCallSubexpressionError + + unfinished = RegexBuilder().capture().digit() + with pytest.raises(CannotCallSubexpressionError): + unfinished.to_regex_string() + + +def test_to_regex_with_open_frame_raises(): + from edify.errors.structure import CannotCallSubexpressionError + + unfinished = RegexBuilder().capture().digit() + with pytest.raises(CannotCallSubexpressionError): + unfinished.to_regex() + + +def test_subexpression_non_builder_raises(): + from edify.errors.input import MustBeInstanceError + + with pytest.raises(MustBeInstanceError): + RegexBuilder().subexpression("not a builder") diff --git a/tests/compile/invariants.test.py b/tests/compile/invariants.test.py new file mode 100644 index 0000000..2260427 --- /dev/null +++ b/tests/compile/invariants.test.py @@ -0,0 +1,30 @@ +"""Tests for the compile-path invariant raises that public-builder use never triggers. + +These exercise defensive branches that fire only when an AST is constructed +outside the builder (so the type system can't catch it). They guarantee +the typed exception is raised instead of a silent miscompilation. +""" + +import pytest + +from edify.compile.dispatch import render_element +from edify.compile.fuse import _fragment_for +from edify.elements.types.base import BaseElement +from edify.elements.types.leaves import DigitElement +from edify.errors.internal import NonFusableElementError, UnknownElementTypeError + + +class _StrayElement(BaseElement): + """A BaseElement subclass not in the ``Element`` union — used only here.""" + + +def test_render_element_unknown_type_raises(): + stray = _StrayElement() + with pytest.raises(UnknownElementTypeError, match="_StrayElement"): + render_element(stray) + + +def test_fragment_for_non_fusable_raises(): + non_fusable = DigitElement() + with pytest.raises(NonFusableElementError, match="DigitElement"): + _fragment_for(non_fusable) diff --git a/tests/errors/errors.test.py b/tests/errors/errors.test.py new file mode 100644 index 0000000..12cfcd5 --- /dev/null +++ b/tests/errors/errors.test.py @@ -0,0 +1,153 @@ +from edify.errors.anchors import ( + CannotDefineStartAfterEndError, + EndInputAlreadyDefinedError, + StartInputAlreadyDefinedError, +) +from edify.errors.captures import InvalidTotalCaptureGroupsIndexError +from edify.errors.input import ( + MustBeAStringError, + MustBeInstanceError, + MustBeIntegerGreaterThanZeroError, + MustBeLessThanError, + MustBeOneCharacterError, + MustBePositiveIntegerError, + MustBeSingleCharacterError, + MustHaveASmallerValueError, +) +from edify.errors.internal import ( + FailedToCompileRegexError, + NonFusableElementError, + UnexpectedFrameTypeError, + UnknownElementTypeError, +) +from edify.errors.naming import ( + CannotCreateDuplicateNamedGroupError, + NamedGroupDoesNotExistError, + NameNotValidError, +) +from edify.errors.structure import ( + CannotCallSubexpressionError, + CannotEndWhileBuildingRootExpressionError, +) + + +def test_start_input_already_defined_outside_subexpression(): + error = StartInputAlreadyDefinedError() + assert "already has a start" in str(error) + assert "ignore_start_and_end" not in str(error) + + +def test_start_input_already_defined_in_subexpression(): + error = StartInputAlreadyDefinedError(in_subexpression=True) + assert "ignore_start_and_end" in str(error) + + +def test_end_input_already_defined_outside_subexpression(): + error = EndInputAlreadyDefinedError() + assert "already has an end" in str(error) + assert "ignore_start_and_end" not in str(error) + + +def test_end_input_already_defined_in_subexpression(): + error = EndInputAlreadyDefinedError(in_subexpression=True) + assert "ignore_start_and_end" in str(error) + + +def test_cannot_define_start_after_end(): + error = CannotDefineStartAfterEndError() + assert "start of input after defining an end" in str(error) + + +def test_invalid_total_capture_groups_index(): + error = InvalidTotalCaptureGroupsIndexError(5, 3) + assert "Invalid index #5" in str(error) + assert "only 3 capture groups" in str(error) + + +def test_must_be_a_string(): + error = MustBeAStringError("Name", "int") + assert "Name must be a string" in str(error) + assert "int" in str(error) + + +def test_must_be_one_character(): + error = MustBeOneCharacterError("Value") + assert "Value must be one character long" in str(error) + + +def test_must_be_single_character(): + error = MustBeSingleCharacterError("Value", "str") + assert "Value must be a single character" in str(error) + assert "str" in str(error) + + +def test_must_be_positive_integer(): + error = MustBePositiveIntegerError("count") + assert "count must be a positive integer" in str(error) + + +def test_must_be_integer_greater_than_zero(): + error = MustBeIntegerGreaterThanZeroError("x") + assert "x must be an integer greater than zero" in str(error) + + +def test_must_be_instance(): + error = MustBeInstanceError("Expression", "str", "RegexBuilder") + assert "Expression must be an instance of RegexBuilder" in str(error) + assert "str" in str(error) + + +def test_must_have_a_smaller_value(): + error = MustHaveASmallerValueError("z", "a") + assert "z must have a smaller character value than a" in str(error) + + +def test_must_be_less_than(): + error = MustBeLessThanError("X", "Y") + assert "X must be less than Y" in str(error) + + +def test_name_not_valid(): + error = NameNotValidError("bad name") + assert "Name bad name is not valid" in str(error) + + +def test_cannot_create_duplicate_named_group(): + error = CannotCreateDuplicateNamedGroupError("dup") + assert 'Can not create duplicate named group "dup"' in str(error) + + +def test_named_group_does_not_exist(): + error = NamedGroupDoesNotExistError("missing") + assert 'Named group "missing" does not exist' in str(error) + + +def test_cannot_end_while_building_root_expression(): + error = CannotEndWhileBuildingRootExpressionError() + assert "Can not end while building the root expression" in str(error) + + +def test_cannot_call_subexpression(): + error = CannotCallSubexpressionError("capture") + assert "Can not call subexpression" in str(error) + assert "capture" in str(error) + + +def test_unknown_element_type(): + error = UnknownElementTypeError("WeirdElement") + assert "WeirdElement" in str(error) + + +def test_non_fusable_element(): + error = NonFusableElementError("DigitElement") + assert "Cannot fuse element of type DigitElement" in str(error) + + +def test_unexpected_frame_type(): + error = UnexpectedFrameTypeError("DigitElement") + assert "Stack frame anchored at unexpected element type DigitElement" in str(error) + + +def test_failed_to_compile_regex(): + error = FailedToCompileRegexError("missing )") + assert "Cannot compile regex: missing )" in str(error) diff --git a/tests/library/date/basic.test.py b/tests/library/date/basic.test.py new file mode 100644 index 0000000..9b3f45a --- /dev/null +++ b/tests/library/date/basic.test.py @@ -0,0 +1,29 @@ +from edify.library import date + +_TEST_CASES = { + "1/1/2020": True, + "01/01/2020": True, + "1/01/2020": True, + "01/1/2020": True, + "1/1/20": False, + "01/01/20": False, + "1/1/202": False, + "01/01/202": False, + "12/12/2022": True, + "12/12/2": False, + "2021-11-04T22:32:47.142354-10:00": False, + "2021-11-04T22:32:47.142354Z": False, + "2021-11-04T22:32:47.142354": False, + "2021-11-04T22:32:47": False, + "2021-11-04T22:32": False, + "2021-11-04T22": False, + "2021-11-04": False, + "2021-11": False, + "2021": False, + "1-1-2020": False, +} + + +def test_date(): + for candidate, expectation in _TEST_CASES.items(): + assert date(candidate) is expectation diff --git a/tests/library/date/iso.test.py b/tests/library/date/iso.test.py new file mode 100644 index 0000000..de29a4c --- /dev/null +++ b/tests/library/date/iso.test.py @@ -0,0 +1,28 @@ +from edify.library import iso_date + +_TEST_CASES = { + "1/1/2020": False, + "01/01/2020": False, + "1/01/2020": False, + "01/1/2020": False, + "1/1/20": False, + "01/01/20": False, + "1/1/202": False, + "01/01/202": False, + "12/12/2022": False, + "12/12/2": False, + "2021-11-04T22:32:47.142354-10:00": True, + "2021-11-04T22:32:47.142354Z": True, + "2021-11-04T22:32:47.142354": True, + "2021-11-04T22:32:47": True, + "2021-11-04T22:32": False, + "2021-11-04T22": False, + "2021-11-04": False, + "2021-11": False, + "2021": False, +} + + +def test_iso_date(): + for candidate, expectation in _TEST_CASES.items(): + assert iso_date(candidate) is expectation diff --git a/tests/library/email/basic.test.py b/tests/library/email/basic.test.py new file mode 100644 index 0000000..e77e3b8 --- /dev/null +++ b/tests/library/email/basic.test.py @@ -0,0 +1,32 @@ +from edify.library import email + +_TEST_CASES = [ + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected].", False), + ("plainaddress", False), + ("#@%^%#$@#$@#.com", False), + ("@example.com", False), + ("Joe Smith <[email protected]>", False), + ("email.example.com", False), + ("email@[email protected]", False), + ("[email protected]", False), + ("[email protected]", False), + ("[email protected]", False), + ("あいうえお@example.com", False), + ("[email protected]", False), + ("[email protected]", False), +] + + +def test_email(): + for candidate, expectation in _TEST_CASES: + assert email(candidate) is expectation diff --git a/tests/library/email/strict.test.py b/tests/library/email/strict.test.py new file mode 100644 index 0000000..7cefbce --- /dev/null +++ b/tests/library/email/strict.test.py @@ -0,0 +1,32 @@ +from edify.library import email_rfc_5322 + +_TEST_CASES = [ + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected]", True), + ("[email protected].", True), + ("plainaddress", False), + ("#@%^%#$@#$@#.com", False), + ("@example.com", False), + ("Joe Smith <[email protected]>", False), + ("email.example.com", False), + ("email@[email protected]", False), + ("[email protected]", False), + ("[email protected]", False), + ("[email protected]", False), + ("あいうえお@example.com", False), + ("[email protected]", False), + ("[email protected]", False), +] + + +def test_email_rfc_5322(): + for candidate, expectation in _TEST_CASES: + assert email_rfc_5322(candidate) is expectation diff --git a/tests/test_guid.py b/tests/library/guid.test.py index b4b1a64..67fcad1 100644 --- a/tests/test_guid.py +++ b/tests/library/guid.test.py @@ -4,8 +4,8 @@ from edify.library import guid def test_valid_guids(): guids = { "6ba7b810-9dad-11d1-80b4-00c04fd430c8": True, - '{51d52cf1-83c9-4f02-b117-703ecb728b74}': True, - '{51d52cf1-83c9-4f02-b117-703ecb728-b74}': False, + "{51d52cf1-83c9-4f02-b117-703ecb728b74}": True, + "{51d52cf1-83c9-4f02-b117-703ecb728-b74}": False, } for guid_string, expectation in guids.items(): assert guid(guid_string) == expectation diff --git a/tests/library/ip/v4.test.py b/tests/library/ip/v4.test.py new file mode 100644 index 0000000..32a1975 --- /dev/null +++ b/tests/library/ip/v4.test.py @@ -0,0 +1,16 @@ +from edify.library import ipv4 + +_TEST_CASES = { + "192.168.0.1": True, + "244.232.123.233": True, + "363.232.123.233": False, + "234.234234.234.234": False, + "12.12.12.12.12": False, + "0.0.0.0": True, + "987.987.987.987": False, +} + + +def test_ipv4(): + for candidate, expectation in _TEST_CASES.items(): + assert ipv4(candidate) is expectation diff --git a/tests/library/ip/v6.test.py b/tests/library/ip/v6.test.py new file mode 100644 index 0000000..a68f892 --- /dev/null +++ b/tests/library/ip/v6.test.py @@ -0,0 +1,17 @@ +from edify.library import ipv6 + +_TEST_CASES = { + "2001:0db8:85a3:0000:0000:8a2e:0370:7334": True, + "2001:db8:85a3:0:0:8a2e:370:7334": True, + "2001:db8:85a3::8a2e:370:7334": True, + "2001:db8:85a3:0:0:8A2E:370:7334": True, + "2001:db8:85a3:0:0:8a2e:370:7334:": False, + "2001:db8:85a3:0:0:8a2e:370:7334:7334": False, + "2001:db8:85a3:0:0:8a2e:370:7334:7334:7334": False, + "2001:db8:85a3:0:0:8a2e:370:7334:7334:7334:7334": False, +} + + +def test_ipv6(): + for candidate, expectation in _TEST_CASES.items(): + assert ipv6(candidate) is expectation diff --git a/tests/test_mac.py b/tests/library/mac.test.py index 2f24b44..2f24b44 100644 --- a/tests/test_mac.py +++ b/tests/library/mac.test.py diff --git a/tests/test_password.py b/tests/library/password.test.py index 538874b..ec3b627 100644 --- a/tests/test_password.py +++ b/tests/library/password.test.py @@ -7,4 +7,9 @@ def test_password(): assert password("Password123!", max_length=8) is False assert password("Password123!", min_upper=2) is False assert password("password", min_upper=0, min_digit=0, min_special=0) is True - assert password("pass@#1", min_special=1, special_chars="!", min_digit=0, min_upper=0, min_length=4) is False + assert ( + password( + "pass@#1", min_special=1, special_chars="!", min_digit=0, min_upper=0, min_length=4 + ) + is False + ) diff --git a/tests/test_phone.py b/tests/library/phone.test.py index cbff29c..6fe16b1 100644 --- a/tests/test_phone.py +++ b/tests/library/phone.test.py @@ -7,7 +7,6 @@ def test(): "123 456 7890": True, "123-456-7890": True, "123.456.7890": True, - "123 456 7890": True, "+1 (123) 456-7890": True, "+1 (123) 456 7890": True, "+1-(123)-456-7890": True, @@ -20,7 +19,7 @@ def test(): "+1 (1) 456-7890": True, "9012": True, "911": True, - "+1 (615) 243-": False + "+1 (615) 243-": False, } for phone, expectation in phones.items(): assert phone_number(phone) == expectation diff --git a/tests/ssn_test.py b/tests/library/ssn.test.py index ce2a57b..ce2a57b 100644 --- a/tests/ssn_test.py +++ b/tests/library/ssn.test.py diff --git a/tests/library/url.test.py b/tests/library/url.test.py new file mode 100644 index 0000000..293bd34 --- /dev/null +++ b/tests/library/url.test.py @@ -0,0 +1,55 @@ +import pytest + +from edify.library import url + +_URLS = [ + "example.com", + "www.example.com", + "www.example.com/path/to/file", + "http://www.example.com", + "http://example.com", + "http://www.example.com/path/to/page", + "https://example.com", + "https://www.example.com/", + "https://www.example.com/path/to/page", + "//example.com", +] + + +def test_all_protocols(): + match_list = ["proto", "no_proto"] + expected = [True] * 9 + [False] + for uri, expectation in zip(_URLS, expected, strict=True): + assert url(uri, match=match_list) is expectation + + +def test_proto_only(): + match_list = ["proto"] + expected = [False] * 3 + [True] * 6 + [False] + for uri, expectation in zip(_URLS, expected, strict=True): + assert url(uri, match=match_list) is expectation + + +def test_no_proto_only(): + match_list = ["no_proto"] + expected = [True] * 3 + [False] * 7 + for uri, expectation in zip(_URLS, expected, strict=True): + assert url(uri, match=match_list) is expectation + + +def test_invalid_protocol(): + for uri in _URLS: + with pytest.raises(ValueError, match="Invalid protocol"): + url(uri, match=["invalid"]) + + +def test_invalid_match_type(): + for uri in _URLS: + with pytest.raises(TypeError, match="must be a list"): + url(uri, match="invalid") + + +def test_empty_match_list(): + for uri in _URLS: + with pytest.raises(ValueError, match="must not be empty"): + url(uri, match=[]) diff --git a/tests/test_uuid.py b/tests/library/uuid.test.py index df4e840..df4e840 100644 --- a/tests/test_uuid.py +++ b/tests/library/uuid.test.py diff --git a/tests/library/zip.test.py b/tests/library/zip.test.py new file mode 100644 index 0000000..77a3e0e --- /dev/null +++ b/tests/library/zip.test.py @@ -0,0 +1,43 @@ +import pytest + +from edify.library import zip + +_DEFAULT_US_ZIPS = { + "12345": True, + "12345-1234": True, + "12345-123456": False, + "1234": False, +} + +_INDIA_ZIPS = { + "123456": True, + "000000": False, + "012345": False, + "12345": False, + "1234567": False, +} + + +def test_valid_zips(): + for candidate, expectation in _DEFAULT_US_ZIPS.items(): + assert zip(candidate) is expectation + + +def test_invalid_locale(): + with pytest.raises(ValueError, match="locale must be one of"): + zip("12345", locale="INVALID") + + +def test_invalid_locale_type(): + with pytest.raises(TypeError, match="locale must be a string"): + zip("12345", 5) + + +def test_empty_locale(): + with pytest.raises(ValueError, match="locale cannot be empty"): + zip("12345", "") + + +def test_locale_india(): + for candidate, expectation in _INDIA_ZIPS.items(): + assert zip(candidate, locale="IN") is expectation diff --git a/tests/package.test.py b/tests/package.test.py new file mode 100644 index 0000000..384f9ce --- /dev/null +++ b/tests/package.test.py @@ -0,0 +1,13 @@ +"""Tests for the top-level :mod:`edify` package surface.""" + +import importlib.metadata + +from edify import _resolve_installed_version + + +def test_version_falls_back_when_package_metadata_missing(monkeypatch): + def raise_not_found(distribution_name: str) -> str: + raise importlib.metadata.PackageNotFoundError(distribution_name) + + monkeypatch.setattr(importlib.metadata, "version", raise_not_found) + assert _resolve_installed_version() == "0.0.0" diff --git a/tests/test_builder.py b/tests/test_builder.py deleted file mode 100644 index 76a04b5..0000000 --- a/tests/test_builder.py +++ /dev/null @@ -1,518 +0,0 @@ -import re - -from edify import RegexBuilder - -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('/(?:)/A', expr) - regex_compilation('(?:)', expr, re.A) - - -def test_flag_d(): - expr = RegexBuilder().debug() - regex_equality('/(?:)/D', expr) - regex_compilation('(?:)', expr, re.DEBUG) - - -def test_flag_i(): - expr = RegexBuilder().ignore_case() - regex_equality('/(?:)/I', expr) - regex_compilation('(?:)', expr, re.I) - - -def test_flag_m(): - expr = RegexBuilder().multi_line() - regex_equality('/(?:)/M', expr) - regex_compilation('(?:)', expr, re.M) - - -def test_flag_s(): - expr = RegexBuilder().dot_all() - regex_equality('/(?:)/S', expr) - regex_compilation('(?:)', expr, re.S) - - -def test_flag_x(): - expr = RegexBuilder().verbose() - regex_equality('/(?:)/X', 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(): - try: - (RegexBuilder().named_capture('hello world').string('hello ').word().char('!').end()) - except Exception as e: - assert isinstance(e, Exception) - - -def test_same_name_error(): - try: - ( - RegexBuilder() - .namedCapture('hello') - .string('hello ') - .word() - .char('!') - .end() - .namedCapture('hello') - .string('hello ') - .word() - .char('!') - .end() - ) - except Exception as e: - assert isinstance(e, Exception) - - -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!)\\k<this_is_the_name>/', expr) - # Python does not support named back references, so we raise an error - try: - expr.to_regex() - except Exception as e: - assert isinstance(e, Exception) - - -def test_named_back_reference_no_cg_exists(): - try: - RegexBuilder().named_back_reference('not_here') - except Exception as e: - assert isinstance(e, Exception) - - -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(): - try: - RegexBuilder().back_reference(1) - except Exception as e: - assert isinstance(e, Exception) - - -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(): - try: - RegexBuilder().end() - except Exception as e: - assert isinstance(e, Exception) - - -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(): - try: - RegexBuilder().char('hello') - except Exception as e: - assert isinstance(e, Exception) - - -def test_range(): - expr = RegexBuilder().range('a', 'z') - regex_equality('/[a-z]/', expr) - regex_compilation('[a-z]', expr) - - -def test_must_be_instance_error(): - try: - RegexBuilder().subexpression('nope') - except Exception as e: - assert isinstance(e, Exception) - - -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]$/IMS', 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]$/S', 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_dont_ignore_start_and_end(): - try: - (RegexBuilder().at_least(3).digit().subexpression(start_end_se, {'ignore_start_and_end': False}).range('0', '9')) - except Exception as e: - assert isinstance(e, Exception) - - -def test_dont_ignore_start_and_end2(): - try: - se = RegexBuilder().start_of_input().string('hello').any_char().string('world') - (RegexBuilder().at_least(3).digit().subexpression(se, {'ignore_start_and_end': False}).range('0', '9')) - except Exception as e: - assert isinstance(e, Exception) - - -def test_dont_ignore_start_and_end3(): - try: - se = RegexBuilder().string('hello').any_char().string('world').end_of_input() - (RegexBuilder().at_least(3).digit().subexpression(se, {'ignore_start_and_end': False}).range('0', '9')) - except Exception as e: - assert isinstance(e, Exception) - - -def test_start_defined_in_me_and_se(): - try: - (RegexBuilder().start_of_input().at_least(3).digit().subexpression(start_end_se, {'ignore_start_and_end': False}).range('0', '9')) - except Exception as e: - assert isinstance(e, Exception) - - -def test_end_defined_in_me_and_se(): - try: - (RegexBuilder().at_least(3).digit().subexpression(start_end_se, {'ignore_start_and_end': False}).range('0', '9').end_of_input()) - except Exception as e: - assert isinstance(e, Exception) - - -def test_no_namespacing(): - expr = RegexBuilder().at_least(3).digit().subexpression(nc_se).range('0', '9') - regex_equality('/\\d{3,}(?P<module>.{2})\\k<module>[0-9]/', expr) - try: - expr.to_regex() - except Exception as e: - assert isinstance(e, Exception) - - -def test_namespacing(): - expr = RegexBuilder().at_least(3).digit().subexpression(nc_se, {'namespace': 'yolo'}).range('0', '9') - regex_equality('/\\d{3,}(?P<yolomodule>.{2})\\k<yolomodule>[0-9]/', expr) - try: - expr.to_regex() - except Exception as e: - assert isinstance(e, Exception) - - -def test_group_name_collision_error(): - try: - (RegexBuilder().namedCapture('module').at_least(3).digit().end().subexpression(nc_se).range('0', '9')) - except Exception as e: - assert isinstance(e, Exception) - - -def test_group_name_collision_error_after_namespacing(): - try: - (RegexBuilder().namedCapture('module').at_least(3).digit().end().subexpression(nc_se, {'namespace': 'yolo'}).range('0', '9')) - except Exception as e: - assert isinstance(e, Exception) - - -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) diff --git a/tests/test_date.py b/tests/test_date.py deleted file mode 100644 index 555dfb0..0000000 --- a/tests/test_date.py +++ /dev/null @@ -1,57 +0,0 @@ -from edify.library import date -from edify.library import iso_date - - -def test_date(): - dates = { - "1/1/2020": True, - "01/01/2020": True, - "1/01/2020": True, - "01/1/2020": True, - "1/1/20": False, - "01/01/20": False, - "1/1/202": False, - "01/01/202": False, - "12/12/2022": True, - "12/12/2": False, - "2021-11-04T22:32:47.142354-10:00": False, - "2021-11-04T22:32:47.142354Z": False, - "2021-11-04T22:32:47.142354": False, - "2021-11-04T22:32:47": False, - "2021-11-04T22:32": False, - "2021-11-04T22": False, - "2021-11-04": False, - "2021-11": False, - "2021": False, - "1-1-2020": False - } - - for date_string, expectation in dates.items(): - assert date(date_string) == expectation - - -def test_iso_date(): - dates = { - "1/1/2020": False, - "01/01/2020": False, - "1/01/2020": False, - "01/1/2020": False, - "1/1/20": False, - "01/01/20": False, - "1/1/202": False, - "01/01/202": False, - "12/12/2022": False, - "12/12/2": False, - "2021-11-04T22:32:47.142354-10:00": True, - "2021-11-04T22:32:47.142354Z": True, - "2021-11-04T22:32:47.142354": True, - "2021-11-04T22:32:47": True, - "2021-11-04T22:32": False, - "2021-11-04T22": False, - "2021-11-04": False, - "2021-11": False, - "2021": False, - } - - for date_string, expectation in dates.items(): - assert iso_date(date_string) == expectation diff --git a/tests/test_email.py b/tests/test_email.py deleted file mode 100644 index cc28472..0000000 --- a/tests/test_email.py +++ /dev/null @@ -1,89 +0,0 @@ -from edify.library import email -from edify.library import email_rfc_5322 - -emails = [ - "[email protected]", - "[email protected]", - "[email protected]", - "[email protected]", - "[email protected]", - "[email protected]", - "[email protected]", - "[email protected]", - "[email protected]", - "[email protected]", - "[email protected].", - "plainaddress", - "#@%^%#$@#$@#.com", - "@example.com", - "Joe Smith <[email protected]>", - "email.example.com", - "email@[email protected]", - "[email protected]", - "[email protected]", - "[email protected]", - "あいうえお@example.com", - "[email protected]", -] - - -def test_email(): - - expectations = [ - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False - ] - for i in range(len(emails)): - assert email(emails[i]) == expectations[i] - - -def test_email_rfc_5322(): - expectations = [ - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False - ] - for i in range(len(emails)): - assert email_rfc_5322(emails[i]) == expectations[i] diff --git a/tests/test_ip.py b/tests/test_ip.py deleted file mode 100644 index dc081a5..0000000 --- a/tests/test_ip.py +++ /dev/null @@ -1,35 +0,0 @@ -from edify.library import ipv4 -from edify.library import ipv6 - -# Generate ipv4 dictionary -ipv4_dict = { - "192.168.0.1": True, - "244.232.123.233": True, - "363.232.123.233": False, - "234.234234.234.234": False, - "12.12.12.12.12": False, - "0.0.0.0": True, - "987.987.987.987": False, -} - -# Generate ipv6 dictionary -ipv6_dict = { - "2001:0db8:85a3:0000:0000:8a2e:0370:7334": True, - "2001:db8:85a3:0:0:8a2e:370:7334": True, - "2001:db8:85a3::8a2e:370:7334": True, - "2001:db8:85a3:0:0:8A2E:370:7334": True, - "2001:db8:85a3:0:0:8a2e:370:7334:": False, - "2001:db8:85a3:0:0:8a2e:370:7334:7334": False, - "2001:db8:85a3:0:0:8a2e:370:7334:7334:7334": False, - "2001:db8:85a3:0:0:8a2e:370:7334:7334:7334:7334": False, -} - - -def test_ipv4(): - for ip, expectation in ipv4_dict.items(): - assert ipv4(ip) == expectation - - -def test_ipv6(): - for ip, expectation in ipv6_dict.items(): - assert ipv6(ip) == expectation diff --git a/tests/test_url.py b/tests/test_url.py deleted file mode 100644 index 74f8ed1..0000000 --- a/tests/test_url.py +++ /dev/null @@ -1,63 +0,0 @@ -from edify.library import url - -urls = [ - "example.com", - "www.example.com", - "www.example.com/path/to/file", - "http://www.example.com", - "http://example.com", - "http://www.example.com/path/to/page", - "https://example.com", - "https://www.example.com/", - "https://www.example.com/path/to/page", - "//example.com", -] - - -def test_all_protocols(): - match_list = ["proto", "no_proto"] - expected = [True] * 9 + [False] - for uri, expectation in zip(urls, expected): - assert url(uri, match=match_list) == expectation - - -def test_proto_only(): - match_list = ["proto"] - expected = [False] * 3 + [True] * 6 + [False] - for uri, expectation in zip(urls, expected): - print(uri, expectation) - assert url(uri, match=match_list) == expectation - - -def test_no_proto_only(): - match_list = ["no_proto"] - expected = [True] * 3 + [False] * 7 - for uri, expectation in zip(urls, expected): - assert url(uri, match=match_list) == expectation - - -def test_invalid_protocol(): - match_list = ["invalid"] - for uri in urls: - try: - url(uri, match=match_list) - except ValueError: - assert True - - -def test_invalid_match_type(): - match_list = "invalid" - for uri in urls: - try: - url(uri, match=match_list) - except TypeError: - assert True - - -def test_empty_match_list(): - match_list = [] - for uri in urls: - try: - url(uri, match=match_list) - except ValueError: - assert True diff --git a/tests/test_zip.py b/tests/test_zip.py deleted file mode 100644 index 999d55d..0000000 --- a/tests/test_zip.py +++ /dev/null @@ -1,34 +0,0 @@ -from edify.library import zip - - -def test_valid_zips(): - zips = {"12345": True, "12345-1234": True, "12345-123456": False, "1234": False} - for zip_string, expectation in zips.items(): - assert zip(zip_string) == expectation - - -def test_invalid_locale(): - try: - zip("12345", locale="INVALID") - except ValueError: - assert True - - -def test_invalid_locale_type(): - try: - zip("12345", 5) - except TypeError: - assert True - - -def test_empty_locale(): - try: - zip("12345", "") - except ValueError: - assert True - - -def test_locale_IN(): - zips = {"123456": True, "000000": False, "012345": False, "12345": False, "1234567": False} - for zip_string, expectation in zips.items(): - assert zip(zip_string, locale="IN") == expectation |
