aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-15 18:12:42 +0530
committernatsuoto <[email protected]>2026-07-15 18:12:42 +0530
commitcf4a4ea68eca91aa044556806d3d91369182702c (patch)
tree2daa50e0f80eda81ec08a4174be56f7ebdc78825 /tests
parent93371d459ad47a46310f729950ec383372cd2706 (diff)
downloadedify-cf4a4ea68eca91aa044556806d3d91369182702c.tar.xz
edify-cf4a4ea68eca91aa044556806d3d91369182702c.zip
chore: codebase-wide rule audit — eliminate Any/object where non-protocol, hoist in-function imports, rename compound/folder-repeat files, replace generic-exception raises, bind chained comprehensions, strip __init__ docstrings, remove pragmas
Diffstat (limited to 'tests')
-rw-r--r--tests/builder/cache.test.py58
-rw-r--r--tests/builder/cold_warm.test.py69
-rw-r--r--tests/builder/flags.test.py31
-rw-r--r--tests/builder/lookbehind.test.py6
-rw-r--r--tests/builder/ordering.test.py (renamed from tests/builder/insertion_order.test.py)0
-rw-r--r--tests/builder/passthrough.test.py7
-rw-r--r--tests/builder/validation.test.py8
-rw-r--r--tests/compile/redos.test.py12
-rw-r--r--tests/discipline/coverage.test.py3
-rw-r--r--tests/discipline/orphans.test.py6
-rw-r--r--tests/docs/snapshots.test.py6
-rw-r--r--tests/errors/frames.test.py5
-rw-r--r--tests/errors/generic.test.py (renamed from tests/errors/errors.test.py)0
-rw-r--r--tests/fixtures/fixtures.test.py (renamed from tests/fixtures/edge_cases.test.py)3
-rw-r--r--tests/integrations/django.test.py26
-rw-r--r--tests/integrations/fastapi.test.py46
-rw-r--r--tests/integrations/pydantic.test.py55
-rw-r--r--tests/property/emitted.test.py3
-rw-r--r--tests/result/match.test.py86
-rw-r--r--tests/serialize/errors.test.py5
-rw-r--r--tests/testing/snapshots.test.py3
21 files changed, 219 insertions, 219 deletions
diff --git a/tests/builder/cache.test.py b/tests/builder/cache.test.py
index 700a8e4..045e8e9 100644
--- a/tests/builder/cache.test.py
+++ b/tests/builder/cache.test.py
@@ -1,8 +1,33 @@
-"""Tests for the per-instance lazy compile cache."""
+"""Tests for the per-instance lazy compile cache — semantics and cold/warm ratio."""
+
+import time
from edify import Pattern, RegexBuilder
+def _hex_number_builder():
+ return (
+ RegexBuilder()
+ .start_of_input()
+ .assert_ahead()
+ .any_of("0x", "0o", "0b")
+ .end()
+ .capture()
+ .any_of("0x", "0o", "0b")
+ .end()
+ .between(1, 16)
+ .any_of_chars("0123456789abcdefABCDEF")
+ .end_of_input()
+ )
+
+
+def _measure_call_wall_clock(action, iterations):
+ start = time.perf_counter()
+ for _ in range(iterations):
+ action()
+ return time.perf_counter() - start
+
+
def test_two_no_kwargs_to_regex_calls_return_the_same_instance():
builder = RegexBuilder().one_or_more().digit()
first = builder.to_regex()
@@ -61,3 +86,34 @@ def test_chain_step_yields_a_fresh_cache_slot():
def test_pattern_also_caches_across_repeat_to_regex_calls():
pattern = Pattern().string("hi")
assert pattern.to_regex() is pattern.to_regex()
+
+
+def test_warm_to_regex_is_at_least_ten_times_cheaper_than_cold_to_regex():
+ warm_iterations = 200
+ cold_iterations = 200
+
+ cold_total = 0.0
+ for _ in range(cold_iterations):
+ builder = _hex_number_builder()
+ cold_start = time.perf_counter()
+ builder.to_regex()
+ cold_total += time.perf_counter() - cold_start
+
+ warm_builder = _hex_number_builder()
+ warm_builder.to_regex()
+ warm_total = _measure_call_wall_clock(warm_builder.to_regex, warm_iterations)
+
+ average_cold = cold_total / cold_iterations
+ average_warm = warm_total / warm_iterations
+
+ assert average_warm * 10 < average_cold, (
+ f"cache ratio too weak: warm={average_warm * 1e6:.2f}µs, "
+ f"cold={average_cold * 1e6:.2f}µs — warm should be < cold / 10."
+ )
+
+
+def test_repeat_to_regex_returns_the_same_regex_instance():
+ warm_builder = _hex_number_builder()
+ first = warm_builder.to_regex()
+ second = warm_builder.to_regex()
+ assert first is second
diff --git a/tests/builder/cold_warm.test.py b/tests/builder/cold_warm.test.py
deleted file mode 100644
index 8777f90..0000000
--- a/tests/builder/cold_warm.test.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""Deterministic cold/warm ratio gate for the per-instance lazy compile cache.
-
-Measures the wall-clock cost of the first ``.to_regex()`` call on a fresh
-builder (``cold``) against the cost of a repeat call on the same instance
-(``warm``). The cache promise from :issue:`141` is that the warm path is
-an order of magnitude cheaper than the cold path — a hardware-independent
-ratio that gates the cache contract in a way absolute milliseconds cannot.
-"""
-
-from __future__ import annotations
-
-import time
-
-from edify import RegexBuilder
-
-
-def _hex_number_builder():
- builder = (
- RegexBuilder()
- .start_of_input()
- .assert_ahead()
- .any_of("0x", "0o", "0b")
- .end()
- .capture()
- .any_of("0x", "0o", "0b")
- .end()
- .between(1, 16)
- .any_of_chars("0123456789abcdefABCDEF")
- .end_of_input()
- )
- return builder
-
-
-def _measure_call_wall_clock(action, iterations):
- start = time.perf_counter()
- for _ in range(iterations):
- action()
- return time.perf_counter() - start
-
-
-def test_warm_to_regex_is_at_least_ten_times_cheaper_than_cold_to_regex():
- warm_iterations = 200
- cold_iterations = 200
-
- cold_total = 0.0
- for _ in range(cold_iterations):
- builder = _hex_number_builder()
- cold_start = time.perf_counter()
- builder.to_regex()
- cold_total += time.perf_counter() - cold_start
-
- warm_builder = _hex_number_builder()
- warm_builder.to_regex()
- warm_total = _measure_call_wall_clock(warm_builder.to_regex, warm_iterations)
-
- average_cold = cold_total / cold_iterations
- average_warm = warm_total / warm_iterations
-
- assert average_warm * 10 < average_cold, (
- f"cache ratio too weak: warm={average_warm * 1e6:.2f}µs, "
- f"cold={average_cold * 1e6:.2f}µs — warm should be < cold / 10."
- )
-
-
-def test_repeat_to_regex_returns_the_same_regex_instance():
- warm_builder = _hex_number_builder()
- first = warm_builder.to_regex()
- second = warm_builder.to_regex()
- assert first is second
diff --git a/tests/builder/flags.test.py b/tests/builder/flags.test.py
index 02e875e..704065f 100644
--- a/tests/builder/flags.test.py
+++ b/tests/builder/flags.test.py
@@ -4,6 +4,7 @@ import re
import sys
import pytest
+import regex as regex_module
from edify import Pattern, RegexBuilder
@@ -72,38 +73,32 @@ def test_multiple_kwargs_combine():
assert compiled.compiled.flags & re.S == re.S
-def _regex_module():
- import regex
-
- return regex
-
-
def test_regex_engine_ignore_case_kwarg_enables_the_flag():
- regex = _regex_module()
+ regex = regex_module
compiled = RegexBuilder().string("hi").to_regex(engine="regex", ignore_case=True)
assert compiled.compiled.flags & regex.I == regex.I
def test_regex_engine_multiline_kwarg_enables_the_flag():
- regex = _regex_module()
+ regex = regex_module
compiled = RegexBuilder().string("hi").to_regex(engine="regex", multiline=True)
assert compiled.compiled.flags & regex.M == regex.M
def test_regex_engine_dotall_kwarg_enables_the_flag():
- regex = _regex_module()
+ regex = regex_module
compiled = RegexBuilder().string("hi").to_regex(engine="regex", dotall=True)
assert compiled.compiled.flags & regex.S == regex.S
def test_regex_engine_ascii_only_kwarg_enables_the_flag():
- regex = _regex_module()
+ regex = regex_module
compiled = RegexBuilder().string("hi").to_regex(engine="regex", ascii_only=True)
assert compiled.compiled.flags & regex.A == regex.A
def test_regex_engine_verbose_kwarg_enables_the_flag():
- regex = _regex_module()
+ regex = regex_module
compiled = RegexBuilder().string("hi").to_regex(engine="regex", verbose=True)
assert compiled.compiled.flags & regex.X == regex.X
@@ -115,3 +110,17 @@ def test_regex_engine_verbose_kwarg_enables_the_flag():
def test_regex_engine_debug_kwarg_compiles_without_error():
compiled = RegexBuilder().string("hi").to_regex(engine="regex", debug=True)
assert compiled.source == "hi"
+
+
+def test_regex_engine_debug_flag_is_forwarded_to_regex_module_bitmask(monkeypatch):
+ captured_flags: list[int] = []
+ original_compile = regex_module.compile
+
+ def capturing_compile(pattern, flags=0):
+ captured_flags.append(flags)
+ return original_compile(pattern)
+
+ monkeypatch.setattr(regex_module, "compile", capturing_compile)
+ RegexBuilder().string("hi").to_regex(engine="regex", debug=True)
+ assert captured_flags
+ assert captured_flags[0] & regex_module.DEBUG == regex_module.DEBUG
diff --git a/tests/builder/lookbehind.test.py b/tests/builder/lookbehind.test.py
index f16a6cc..5e101f3 100644
--- a/tests/builder/lookbehind.test.py
+++ b/tests/builder/lookbehind.test.py
@@ -1,5 +1,7 @@
"""Tests for lookbehind width behavior across the ``re`` and ``regex`` engines."""
+import re
+
import pytest
from edify import RegexBuilder
@@ -42,14 +44,10 @@ def test_fixed_width_lookbehind_still_works_under_re():
def test_variable_width_lookbehind_error_chains_the_underlying_pattern_error():
with pytest.raises(VariableWidthLookbehindNotSupportedError) as excinfo:
_variable_width_lookbehind_builder().to_regex(engine="re")
- import re
-
assert isinstance(excinfo.value.__cause__, re.error)
def test_re_engine_still_surfaces_other_pattern_errors_unchanged(monkeypatch):
- import re
-
def raise_other_error(_pattern, flags=0):
raise re.error("some other syntax error")
diff --git a/tests/builder/insertion_order.test.py b/tests/builder/ordering.test.py
index 4feae65..4feae65 100644
--- a/tests/builder/insertion_order.test.py
+++ b/tests/builder/ordering.test.py
diff --git a/tests/builder/passthrough.test.py b/tests/builder/passthrough.test.py
index a2e77b5..e149052 100644
--- a/tests/builder/passthrough.test.py
+++ b/tests/builder/passthrough.test.py
@@ -1,6 +1,9 @@
"""Tests for the pass-through branches when subexpression anchors merge without conflict."""
+import pytest
+
from edify import RegexBuilder
+from edify.errors.structure import CannotCallSubexpressionError
def test_start_of_input_merges_into_parent_without_existing_start():
@@ -18,10 +21,6 @@ def test_end_of_input_merges_into_parent_without_existing_end():
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):
diff --git a/tests/builder/validation.test.py b/tests/builder/validation.test.py
index 53128c5..a1b57d7 100644
--- a/tests/builder/validation.test.py
+++ b/tests/builder/validation.test.py
@@ -14,6 +14,7 @@ from edify.errors.anchors import (
)
from edify.errors.input import (
MustBeAStringError,
+ MustBeInstanceError,
MustBeIntegerGreaterThanZeroError,
MustBeLessThanError,
MustBeOneCharacterError,
@@ -22,6 +23,7 @@ from edify.errors.input import (
MustHaveASmallerValueError,
)
from edify.errors.naming import NamedGroupDoesNotExistError
+from edify.errors.structure import CannotCallSubexpressionError
def test_start_of_input_twice_raises():
@@ -140,23 +142,17 @@ def test_named_back_reference_undeclared_raises():
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/redos.test.py b/tests/compile/redos.test.py
index c4e1ba9..91786bd 100644
--- a/tests/compile/redos.test.py
+++ b/tests/compile/redos.test.py
@@ -44,25 +44,29 @@ def test_warning_message_names_both_quantifiers():
def test_bounded_quantifier_does_not_trigger_the_warning(recwarn):
builder = RegexBuilder().exactly(3).group().exactly(2).digit().end()
builder.to_regex()
- assert not any(isinstance(warning.message, ReDoSWarning) for warning in recwarn)
+ is_redos_flags = [isinstance(warning.message, ReDoSWarning) for warning in recwarn]
+ assert not any(is_redos_flags)
def test_grouped_quantifier_over_a_composite_group_does_not_trigger(recwarn):
builder = RegexBuilder().one_or_more().group().digit().letter().end()
builder.to_regex()
- assert not any(isinstance(warning.message, ReDoSWarning) for warning in recwarn)
+ is_redos_flags = [isinstance(warning.message, ReDoSWarning) for warning in recwarn]
+ assert not any(is_redos_flags)
def test_grouped_quantifier_over_a_single_non_quantifier_child_does_not_trigger(recwarn):
builder = RegexBuilder().one_or_more().group().digit().end()
builder.to_regex()
- assert not any(isinstance(warning.message, ReDoSWarning) for warning in recwarn)
+ is_redos_flags = [isinstance(warning.message, ReDoSWarning) for warning in recwarn]
+ assert not any(is_redos_flags)
def test_sequential_unbounded_quantifiers_do_not_trigger_the_warning(recwarn):
builder = RegexBuilder().one_or_more().digit().one_or_more().letter()
builder.to_regex()
- assert not any(isinstance(warning.message, ReDoSWarning) for warning in recwarn)
+ is_redos_flags = [isinstance(warning.message, ReDoSWarning) for warning in recwarn]
+ assert not any(is_redos_flags)
def test_warning_only_fires_once_per_construct_within_a_single_terminal_call():
diff --git a/tests/discipline/coverage.test.py b/tests/discipline/coverage.test.py
index a534424..cc90db1 100644
--- a/tests/discipline/coverage.test.py
+++ b/tests/discipline/coverage.test.py
@@ -36,7 +36,8 @@ def test_edify_package_tree_contains_measurable_python_source():
assert python_files, (
"no Python files found under edify/ — the coverage source directory is empty."
)
- total_source_bytes = sum(python_file.stat().st_size for python_file in python_files)
+ file_sizes = [python_file.stat().st_size for python_file in python_files]
+ total_source_bytes = sum(file_sizes)
assert total_source_bytes > 0, (
"every file under edify/ is empty — coverage would measure nothing across the tree."
)
diff --git a/tests/discipline/orphans.test.py b/tests/discipline/orphans.test.py
index a0a3150..6596680 100644
--- a/tests/discipline/orphans.test.py
+++ b/tests/discipline/orphans.test.py
@@ -5,7 +5,7 @@ Runs three checks, one per snapshot root:
* ``tests/snapshots/library/<name>.regex`` — must correspond to a
:class:`edify.Pattern` exported from :mod:`edify.library`.
* ``tests/snapshots/fixtures/<name>.regex`` — must correspond to a fixture
- registered in ``tests/fixtures/edge_cases.test.py``.
+ registered in ``tests/fixtures/fixtures.test.py``.
* ``tests/snapshots/docs/<file>/<block>.regex`` — must correspond to a
``.. code-block:: python`` block at that line in that RST.
@@ -44,7 +44,7 @@ def test_every_fixture_snapshot_has_a_registered_fixture():
orphaned_snapshots = snapshot_stems - fixture_names
assert orphaned_snapshots == set(), (
f"fixture snapshot files without a registered fixture in "
- f"tests/fixtures/edge_cases.test.py: {sorted(orphaned_snapshots)}"
+ f"tests/fixtures/fixtures.test.py: {sorted(orphaned_snapshots)}"
)
@@ -70,7 +70,7 @@ def _library_pattern_names() -> set[str]:
def _registered_fixture_names() -> set[str]:
- fixture_source = (_REPO_ROOT / "tests" / "fixtures" / "edge_cases.test.py").read_text()
+ fixture_source = (_REPO_ROOT / "tests" / "fixtures" / "fixtures.test.py").read_text()
return _fixture_names_from_source(fixture_source)
diff --git a/tests/docs/snapshots.test.py b/tests/docs/snapshots.test.py
index 5b97ddb..b1c223b 100644
--- a/tests/docs/snapshots.test.py
+++ b/tests/docs/snapshots.test.py
@@ -72,7 +72,7 @@ def _discover_blocks():
yield rst_path, block_start, block_source, relative_stem
-def _snapshot_bodies_for_block(namespace: dict, pre_exec_names: frozenset[str]) -> str:
+def _snapshot_bodies_for_block(namespace, pre_exec_names: frozenset[str]) -> str:
interesting_pairs = []
for identifier, value in namespace.items():
if identifier in pre_exec_names:
@@ -133,8 +133,8 @@ def test_doc_code_block_produces_the_snapshotted_regex(
assert_snapshot(rendered, snapshot_path)
-def _prepared_exec_namespace() -> dict[str, object]:
- namespace: dict[str, object] = {"edify": edify, "Pattern": Pattern, "Regex": Regex, "re": re}
+def _prepared_exec_namespace():
+ namespace = {"edify": edify, "Pattern": Pattern, "Regex": Regex, "re": re}
for edify_export in dir(edify):
if edify_export.startswith("_"):
continue
diff --git a/tests/errors/frames.test.py b/tests/errors/frames.test.py
index b4077ba..0ae0c1b 100644
--- a/tests/errors/frames.test.py
+++ b/tests/errors/frames.test.py
@@ -3,6 +3,7 @@
import pytest
from edify import RegexBuilder
+from edify.errors.quantifier import DanglingQuantifierError
from edify.errors.structure import CannotCallSubexpressionError
@@ -53,8 +54,6 @@ def test_subexpression_lists_open_frames_from_the_argument_not_the_receiver():
def test_dangling_quantifier_reports_the_specific_pending_quantifier_name():
- from edify.errors.quantifier import DanglingQuantifierError
-
with pytest.raises(DanglingQuantifierError) as excinfo:
RegexBuilder().exactly(3).to_regex_string()
text = str(excinfo.value)
@@ -62,8 +61,6 @@ def test_dangling_quantifier_reports_the_specific_pending_quantifier_name():
def test_dangling_quantifier_falls_back_to_generic_summary_when_name_unknown():
- from edify.errors.quantifier import DanglingQuantifierError
-
error = DanglingQuantifierError()
text = str(error)
assert "dangling quantifier" in text
diff --git a/tests/errors/errors.test.py b/tests/errors/generic.test.py
index 82eb8fe..82eb8fe 100644
--- a/tests/errors/errors.test.py
+++ b/tests/errors/generic.test.py
diff --git a/tests/fixtures/edge_cases.test.py b/tests/fixtures/fixtures.test.py
index 18200b6..24b5a6f 100644
--- a/tests/fixtures/edge_cases.test.py
+++ b/tests/fixtures/fixtures.test.py
@@ -12,6 +12,7 @@ from pathlib import Path
import pytest
from edify import Pattern, RegexBuilder
+from edify.compile.redos import ReDoSWarning
from edify.testing import assert_snapshot
_SNAPSHOT_ROOT = Path(__file__).parent.parent / "snapshots" / "fixtures"
@@ -124,7 +125,5 @@ def test_edge_case_fixture_emits_the_snapshotted_regex(fixture_name, builder_fac
def test_nested_unbounded_quantifier_fixture_raises_the_redos_warning():
- from edify.compile.redos import ReDoSWarning
-
with pytest.warns(ReDoSWarning):
_nested_unbounded_quantifier_redos().to_regex()
diff --git a/tests/integrations/django.test.py b/tests/integrations/django.test.py
index 11aa835..871ceed 100644
--- a/tests/integrations/django.test.py
+++ b/tests/integrations/django.test.py
@@ -1,19 +1,16 @@
"""Tests for :mod:`edify.integrations.django`."""
-import sys
-
import pytest
+from django.core.exceptions import ValidationError
+from django.core.validators import RegexValidator
from edify import Pattern
-from edify.errors.integration import MissingIntegrationDependencyError
from edify.integrations.django import pattern_validator
_DIGITS_PATTERN = Pattern().start_of_input().one_or_more().digit().end_of_input()
def test_pattern_validator_returns_a_regex_validator_pinned_to_the_pattern_source():
- from django.core.validators import RegexValidator
-
validator = pattern_validator(_DIGITS_PATTERN)
assert isinstance(validator, RegexValidator)
assert validator.regex.pattern == _DIGITS_PATTERN.to_regex_string()
@@ -25,8 +22,6 @@ def test_pattern_validator_accepts_matching_input_without_raising():
def test_pattern_validator_raises_django_validation_error_on_non_matching_input():
- from django.core.exceptions import ValidationError
-
validator = pattern_validator(_DIGITS_PATTERN)
with pytest.raises(ValidationError):
validator("abc")
@@ -41,20 +36,3 @@ def test_pattern_validator_accepts_an_override_message_and_code():
validator = pattern_validator(_DIGITS_PATTERN, message="digits only", code="bad_digits")
assert validator.message == "digits only"
assert validator.code == "bad_digits"
-
-
-def test_pattern_validator_raises_missing_integration_when_django_absent(monkeypatch):
- monkeypatch.setitem(sys.modules, "django", None)
- monkeypatch.setitem(sys.modules, "django.core", None)
- monkeypatch.setitem(sys.modules, "django.core.validators", None)
- with pytest.raises(MissingIntegrationDependencyError, match="django"):
- pattern_validator(_DIGITS_PATTERN)
-
-
-def test_missing_integration_error_carries_the_actionable_install_hint(monkeypatch):
- monkeypatch.setitem(sys.modules, "django", None)
- monkeypatch.setitem(sys.modules, "django.core", None)
- monkeypatch.setitem(sys.modules, "django.core.validators", None)
- with pytest.raises(MissingIntegrationDependencyError) as excinfo:
- pattern_validator(_DIGITS_PATTERN)
- assert "pip install edify[django]" in str(excinfo.value)
diff --git a/tests/integrations/fastapi.test.py b/tests/integrations/fastapi.test.py
index 636d78b..f2dd45a 100644
--- a/tests/integrations/fastapi.test.py
+++ b/tests/integrations/fastapi.test.py
@@ -1,11 +1,8 @@
"""Tests for :mod:`edify.integrations.fastapi`."""
-import sys
-
-import pytest
+import fastapi.params
from edify import Pattern
-from edify.errors.integration import MissingIntegrationDependencyError
from edify.integrations.fastapi import pattern_path, pattern_query
_UUID_PATTERN = (
@@ -22,13 +19,10 @@ _UUID_PATTERN = (
def test_pattern_query_returns_a_fastapi_query_pinned_to_the_pattern_regex():
query_default = pattern_query(_UUID_PATTERN)
- fastapi_module = _fastapi()
- assert type(query_default).__name__ == "Query" or isinstance(
- query_default, fastapi_module.params.Query
- )
+ assert isinstance(query_default, fastapi.params.Query)
-def test_pattern_query_forwards_extra_kwargs_to_fastapi_query():
+def test_pattern_query_forwards_the_description_to_fastapi_query():
query_with_description = pattern_query(_UUID_PATTERN, description="prefix")
assert query_with_description.description == "prefix"
@@ -39,37 +33,11 @@ def test_pattern_query_default_differs_from_an_explicit_default():
assert without_default.default != with_default.default
-def test_pattern_query_respects_an_explicit_default_kwarg():
- query_with_default = pattern_query(_UUID_PATTERN, default="deadbeef")
- assert query_with_default.default == "deadbeef"
-
-
def test_pattern_path_returns_a_fastapi_path_pinned_to_the_pattern_regex():
path_default = pattern_path(_UUID_PATTERN)
- fastapi_module = _fastapi()
- assert isinstance(path_default, fastapi_module.params.Path)
-
-
-def test_pattern_query_raises_missing_integration_when_fastapi_absent(monkeypatch):
- monkeypatch.setitem(sys.modules, "fastapi", None)
- with pytest.raises(MissingIntegrationDependencyError, match="fastapi"):
- pattern_query(_UUID_PATTERN)
-
-
-def test_pattern_path_raises_missing_integration_when_fastapi_absent(monkeypatch):
- monkeypatch.setitem(sys.modules, "fastapi", None)
- with pytest.raises(MissingIntegrationDependencyError, match="fastapi"):
- pattern_path(_UUID_PATTERN)
-
-
-def test_missing_integration_error_carries_the_actionable_install_hint(monkeypatch):
- monkeypatch.setitem(sys.modules, "fastapi", None)
- with pytest.raises(MissingIntegrationDependencyError) as excinfo:
- pattern_query(_UUID_PATTERN)
- assert "pip install edify[fastapi]" in str(excinfo.value)
-
+ assert isinstance(path_default, fastapi.params.Path)
-def _fastapi():
- import fastapi
- return fastapi
+def test_pattern_path_forwards_the_description_to_fastapi_path():
+ path_with_description = pattern_path(_UUID_PATTERN, description="prefix")
+ assert path_with_description.description == "prefix"
diff --git a/tests/integrations/pydantic.test.py b/tests/integrations/pydantic.test.py
index da0ff2a..a3c3164 100644
--- a/tests/integrations/pydantic.test.py
+++ b/tests/integrations/pydantic.test.py
@@ -1,12 +1,13 @@
"""Tests for :mod:`edify.integrations.pydantic`."""
-import sys
+from typing import Annotated
import pytest
+from pydantic import AfterValidator, BaseModel, ValidationError
from edify import Pattern
-from edify.errors.integration import MissingIntegrationDependencyError
-from edify.integrations.pydantic import pattern_field, pattern_validator
+from edify.errors.integration import PatternDidNotMatchError
+from edify.integrations.pydantic import pattern_validator
_DIGITS_PATTERN = Pattern().start_of_input().one_or_more().digit().end_of_input()
@@ -16,52 +17,30 @@ def test_pattern_validator_accepts_a_matching_string_and_returns_it_unchanged():
assert validate("42") == "42"
-def test_pattern_validator_raises_value_error_on_non_matching_input():
+def test_pattern_validator_raises_pattern_did_not_match_on_non_matching_input():
validate = pattern_validator(_DIGITS_PATTERN)
- with pytest.raises(ValueError, match="does not match"):
+ with pytest.raises(PatternDidNotMatchError, match="does not match"):
validate("abc")
-def test_pattern_field_returns_an_annotated_str_type_for_pydantic_models():
- from typing import get_args, get_type_hints
+def test_pattern_did_not_match_error_is_a_value_error_subclass():
+ assert issubclass(PatternDidNotMatchError, ValueError)
- from pydantic import AfterValidator, BaseModel
- class Payload(BaseModel):
- value: pattern_field(_DIGITS_PATTERN)
-
- resolved = get_type_hints(Payload, include_extras=True)["value"]
- metadata = get_args(resolved)[1:]
- assert any(isinstance(item, AfterValidator) for item in metadata)
+def test_pattern_did_not_match_error_carries_source_and_value():
+ validate = pattern_validator(_DIGITS_PATTERN)
+ with pytest.raises(PatternDidNotMatchError) as excinfo:
+ validate("abc")
+ assert excinfo.value.source == _DIGITS_PATTERN.to_regex_string()
+ assert excinfo.value.value == "abc"
-def test_pattern_field_is_accepted_by_pydantic_model_validation():
- from pydantic import BaseModel, ValidationError
+def test_pattern_validator_composes_into_pydantic_model_field_via_annotated_after_validator():
+ validator_callable = pattern_validator(_DIGITS_PATTERN)
class Payload(BaseModel):
- value: pattern_field(_DIGITS_PATTERN)
+ value: Annotated[str, AfterValidator(validator_callable)]
assert Payload(value="42").value == "42"
with pytest.raises(ValidationError):
Payload(value="abc")
-
-
-def test_pattern_validator_raises_missing_integration_when_pydantic_absent(monkeypatch):
- monkeypatch.setitem(sys.modules, "pydantic", None)
- with pytest.raises(MissingIntegrationDependencyError, match="pydantic"):
- pattern_validator(_DIGITS_PATTERN)
-
-
-def test_pattern_field_raises_missing_integration_when_pydantic_absent(monkeypatch):
- monkeypatch.setitem(sys.modules, "pydantic", None)
- with pytest.raises(MissingIntegrationDependencyError, match="pydantic"):
- pattern_field(_DIGITS_PATTERN)
-
-
-def test_missing_integration_error_carries_the_actionable_install_hint(monkeypatch):
- monkeypatch.setitem(sys.modules, "pydantic", None)
- with pytest.raises(MissingIntegrationDependencyError) as excinfo:
- pattern_validator(_DIGITS_PATTERN)
- text = str(excinfo.value)
- assert "pip install edify[pydantic]" in text
- assert "= note:" in text
diff --git a/tests/property/emitted.test.py b/tests/property/emitted.test.py
index 6c66e18..2ece703 100644
--- a/tests/property/emitted.test.py
+++ b/tests/property/emitted.test.py
@@ -20,7 +20,8 @@ def test_concatenated_string_calls_emit_the_re_escape_concatenation(literal_segm
for literal in literal_segments:
builder = builder.string(literal)
emitted = builder.to_regex_string()
- reference = "".join(re.escape(literal) for literal in literal_segments)
+ escaped_segments = [re.escape(literal) for literal in literal_segments]
+ reference = "".join(escaped_segments)
assert emitted == reference
diff --git a/tests/result/match.test.py b/tests/result/match.test.py
index b7f7514..9529467 100644
--- a/tests/result/match.test.py
+++ b/tests/result/match.test.py
@@ -70,7 +70,8 @@ def test_match_repr_contains_span_and_text():
def test_finditer_yields_wrapped_matches():
pattern = _username_pattern().to_regex()
hits = list(pattern.finditer("hi @heidi and @ivan"))
- assert all(isinstance(item, Match) for item in hits)
+ types_are_match = [isinstance(item, Match) for item in hits]
+ assert all(types_are_match)
assert [item.captures.username for item in hits] == ["heidi", "ivan"]
@@ -115,3 +116,86 @@ def test_fullmatch_returns_wrapped_match_when_full_input_matches():
def test_fullmatch_returns_none_when_input_is_only_partial():
pattern = _username_pattern().to_regex()
assert pattern.fullmatch("hi @leo") is None
+
+
+def test_match_forwarded_group_accepts_named_and_positional_selectors():
+ result = _username_pattern().search("hi @meg")
+ assert result is not None
+ assert result.group() == "@meg"
+ assert result.group(0) == "@meg"
+ assert result.group(1) == "meg"
+
+
+def test_match_forwarded_groups_returns_the_positional_group_tuple():
+ result = _username_pattern().search("hi @nora")
+ assert result is not None
+ assert result.groups() == ("nora",)
+
+
+def test_match_forwarded_groups_uses_default_for_unmatched_groups():
+ optional_capture = (
+ RegexBuilder()
+ .start_of_input()
+ .capture()
+ .string("hi")
+ .end()
+ .optional()
+ .capture()
+ .string("bye")
+ .end()
+ .end_of_input()
+ )
+ result = optional_capture.match("hi")
+ assert result is not None
+ assert result.groups(default="missing") == ("hi", "missing")
+
+
+def test_match_forwarded_groupdict_defaults_to_none_for_absent_named_groups():
+ result = _username_pattern().search("hi @olive")
+ assert result is not None
+ assert result.groupdict() == {"username": "olive"}
+
+
+def test_match_forwarded_groupdict_accepts_a_default_string():
+ optional_named = (
+ RegexBuilder()
+ .start_of_input()
+ .string("hi")
+ .optional()
+ .named_capture("nick")
+ .one_or_more()
+ .letter()
+ .end()
+ .end_of_input()
+ )
+ result = optional_named.match("hi")
+ assert result is not None
+ assert result.groupdict(default="none") == {"nick": "none"}
+
+
+def test_match_forwarded_start_end_span_and_expand_return_the_expected_values():
+ result = _username_pattern().search("hi @pete")
+ assert result is not None
+ assert result.start() == 3
+ assert result.end() == 8
+ assert result.span() == (3, 8)
+ assert result.expand(r"\g<username>") == "pete"
+
+
+def test_match_forwarded_re_pos_endpos_string_lastindex_lastgroup_reflect_the_compiled_pattern():
+ pattern = _username_pattern().to_regex()
+ result = pattern.search("hi @quinn there")
+ assert result is not None
+ assert result.re is pattern.compiled
+ assert result.string == "hi @quinn there"
+ assert result.pos == 0
+ assert result.endpos > 0
+ assert result.lastindex == 1
+ assert result.lastgroup == "username"
+
+
+def test_match_fallback_getattr_raises_attribute_error_on_unknown_name():
+ result = _username_pattern().search("hi @rita")
+ assert result is not None
+ with pytest.raises(AttributeError, match="no attribute"):
+ _ = result.nonexistent
diff --git a/tests/serialize/errors.test.py b/tests/serialize/errors.test.py
index 68adaac..7670af4 100644
--- a/tests/serialize/errors.test.py
+++ b/tests/serialize/errors.test.py
@@ -8,6 +8,7 @@ from edify import Pattern
from edify.errors.serialize import (
IncompatibleSchemaVersionError,
MissingSchemaKeyError,
+ NonObjectJSONPayloadError,
UnknownElementKindError,
)
@@ -51,8 +52,8 @@ def test_bad_json_string_raises_decode_error():
Pattern.from_json("{not-valid-json}")
-def test_non_object_json_payload_raises_type_error():
- with pytest.raises(TypeError, match="canonical JSON payload must be an object"):
+def test_non_object_json_payload_raises_non_object_json_payload_error():
+ with pytest.raises(NonObjectJSONPayloadError, match="canonical JSON payload must be an object"):
Pattern.from_json("[1, 2, 3]")
diff --git a/tests/testing/snapshots.test.py b/tests/testing/snapshots.test.py
index c2d8676..c71b204 100644
--- a/tests/testing/snapshots.test.py
+++ b/tests/testing/snapshots.test.py
@@ -4,6 +4,7 @@ from pathlib import Path
import pytest
+from edify import testing
from edify.testing import SnapshotMismatchError, SnapshotMissingError, assert_snapshot
_UPDATE_ENVIRONMENT_VARIABLE = "EDIFY_UPDATE_SNAPSHOTS"
@@ -83,8 +84,6 @@ def test_snapshot_mismatch_error_is_an_assertion_error_subclass():
def test_snapshot_helper_is_reachable_via_edify_testing_namespace(tmp_path):
- from edify import testing
-
snapshot_path: Path = tmp_path / "namespace.snapshot"
snapshot_path.write_text("ok\n")
testing.assert_snapshot("ok\n", snapshot_path)