1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
"""Tests for the character-class escape normalization.
The minimal-correct form escapes only ``\\``, ``]``, first-position ``^``,
and interior ``-`` — everything else passes through as a literal. Match
behavior against a representative corpus must stay identical to the
previous over-escaped form.
"""
import re
import pytest
from edify import RegexBuilder
_METACHARS_UNRELATED_TO_CLASS = "#?!@$%^&*"
_MIXED_CORPUS = [
"",
"a",
"z",
"#",
"?",
"!",
"@",
"$",
"%",
"^",
"&",
"*",
"-",
".",
"abc",
"a#b",
"1-2",
" ",
"foo.bar",
"hello world",
"a^b",
"]",
"[a]",
"\\",
]
def test_any_of_chars_emits_minimal_form_for_class_safe_metachars():
emitted = RegexBuilder().any_of_chars(_METACHARS_UNRELATED_TO_CLASS).to_regex_string()
assert emitted == "[#?!@$%^&*]"
def test_any_of_chars_escapes_backslash_and_closing_bracket_only():
emitted = RegexBuilder().any_of_chars("a\\b]c").to_regex_string()
assert emitted == "[a\\\\b\\]c]"
def test_any_of_chars_escapes_first_position_caret_only():
first_caret = RegexBuilder().any_of_chars("^abc").to_regex_string()
later_caret = RegexBuilder().any_of_chars("a^bc").to_regex_string()
assert first_caret == "[\\^abc]"
assert later_caret == "[a^bc]"
def test_any_of_chars_escapes_interior_dash_only():
edge_dashes = RegexBuilder().any_of_chars("-a-").to_regex_string()
interior_dash = RegexBuilder().any_of_chars("a-b").to_regex_string()
assert edge_dashes == "[-a-]"
assert interior_dash == "[a\\-b]"
def test_any_of_chars_leaves_dot_and_asterisk_unescaped_inside_a_class():
emitted = RegexBuilder().any_of_chars(".*").to_regex_string()
assert emitted == "[.*]"
def test_anything_but_chars_leading_position_of_body_is_not_caret():
emitted = RegexBuilder().anything_but_chars("^abc").to_regex_string()
assert emitted == "[^\\^abc]"
def test_anything_but_chars_normalizes_the_same_way_as_any_of_chars():
emitted = RegexBuilder().anything_but_chars("#?!@$%^&*").to_regex_string()
assert emitted == "[^#?!@$%^&*]"
def test_string_terminal_still_uses_full_escape_outside_the_class():
emitted = RegexBuilder().string("a.b").to_regex_string()
assert emitted == "a\\.b"
def test_char_terminal_still_uses_full_escape_outside_the_class():
emitted = RegexBuilder().char(".").to_regex_string()
assert emitted == "\\."
@pytest.mark.parametrize("candidate", _MIXED_CORPUS)
def test_normalized_and_over_escaped_char_classes_match_the_same_inputs(candidate: str):
normalized_pattern = (
RegexBuilder().any_of_chars(_METACHARS_UNRELATED_TO_CLASS).to_regex_string()
)
over_escaped_pattern = f"[{re.escape(_METACHARS_UNRELATED_TO_CLASS)}]"
normalized_hits = re.findall(normalized_pattern, candidate)
over_escaped_hits = re.findall(over_escaped_pattern, candidate)
assert normalized_hits == over_escaped_hits
@pytest.mark.parametrize("candidate", _MIXED_CORPUS)
def test_normalized_and_over_escaped_negated_classes_match_the_same_inputs(candidate: str):
normalized_pattern = RegexBuilder().anything_but_chars("aeiou.-").to_regex_string()
over_escaped_pattern = "[^aeiou\\.\\-]"
normalized_hits = re.findall(normalized_pattern, candidate)
over_escaped_hits = re.findall(over_escaped_pattern, candidate)
assert normalized_hits == over_escaped_hits
def test_any_of_chars_empty_class_still_renders_empty():
emitted = RegexBuilder().any_of_chars("").to_regex_string()
assert emitted == "[]"
|