Skip to content

Commit fdee836

Browse files
Add "search", "match" and "fullmatch" modes to Regexp validator
1 parent b47e73d commit fdee836

File tree

3 files changed

+238
-20
lines changed

3 files changed

+238
-20
lines changed

docs/fields.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ refer to a single input from the form.
233233
Example usage::
234234

235235
class UploadForm(Form):
236-
image = FileField('Image File', [validators.regexp('^[^/\\]\.jpg$')])
236+
image = FileField('Image File', [validators.regexp('^[^/\\]\.jpg$', mode='fullmatch')])
237237
description = TextAreaField('Image Description')
238238

239239
def validate_image(form, field):

src/wtforms/validators.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"mac_address",
3333
"UUID",
3434
"ValidationError",
35+
"ValidatorSetupError",
3536
"StopValidation",
3637
"readonly",
3738
"ReadOnly",
@@ -40,6 +41,15 @@
4041
)
4142

4243

44+
class ValidatorSetupError(ValueError):
45+
"""
46+
Raised when a validator is configured improperly.
47+
"""
48+
49+
def __init__(self, message="", *args, **kwargs):
50+
ValueError.__init__(self, message, *args, **kwargs)
51+
52+
4353
class ValidationError(ValueError):
4454
"""
4555
Raised when a validator fails to validate its input.
@@ -340,16 +350,37 @@ class Regexp:
340350
`regex` is not a string.
341351
:param message:
342352
Error message to raise in case of a validation error.
353+
:param mode:
354+
The matching mode to use. Must be one of "search", "match", or
355+
"fullmatch". Defaults to "match".
343356
"""
344357

345-
def __init__(self, regex, flags=0, message=None):
358+
_supported_modes = ("search", "match", "fullmatch")
359+
360+
def __init__(self, regex, flags=0, message=None, mode="match"):
361+
self.mode = self._validate_mode(mode)
346362
if isinstance(regex, str):
347363
regex = re.compile(regex, flags)
348364
self.regex = regex
349365
self.message = message
350366

367+
def _validate_mode(self, mode):
368+
if mode not in self._supported_modes:
369+
raise ValidatorSetupError(
370+
"Invalid mode value '{}'. Supported values: {}".format(
371+
mode,
372+
", ".join(self._supported_modes)
373+
)
374+
)
375+
return mode
376+
377+
def _get_validator(self):
378+
return getattr(self.regex, self.mode)
379+
351380
def __call__(self, form, field, message=None):
352-
match = self.regex.match(field.data or "")
381+
validator = self._get_validator()
382+
383+
match = validator(field.data or "")
353384
if match:
354385
return match
355386

tests/validators/test_regexp.py

+204-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from wtforms.validators import regexp
66
from wtforms.validators import ValidationError
7+
from wtforms.validators import ValidatorSetupError
78

89

910
def grab_error_message(callable, form, field):
@@ -14,43 +15,187 @@ def grab_error_message(callable, form, field):
1415

1516

1617
@pytest.mark.parametrize(
17-
"re_pattern, re_flags, test_v, expected_v",
18+
"re_pattern, re_flags, re_mode, test_v, expected_v",
1819
[
19-
("^a", None, "abcd", "a"),
20-
("^a", re.I, "ABcd", "A"),
21-
(re.compile("^a"), None, "abcd", "a"),
22-
(re.compile("^a", re.I), None, "ABcd", "A"),
20+
# match mode
21+
("^a", None, "match", "abcd", "a"),
22+
("^a", re.I, "match", "ABcd", "A"),
23+
("^ab", None, "match", "abcd", "ab"),
24+
("^ab", re.I, "match", "ABcd", "AB"),
25+
("^abcd", None, "match", "abcd", "abcd"),
26+
("^abcd", re.I, "match", "ABcd", "ABcd"),
27+
(r"^\w+", None, "match", "abcd", "abcd"),
28+
(r"^\w+", re.I, "match", "ABcd", "ABcd"),
29+
(re.compile("^a"), None, "match", "abcd", "a"),
30+
(re.compile("^a", re.I), None, "match", "ABcd", "A"),
31+
(re.compile("^ab"), None, "match", "abcd", "ab"),
32+
(re.compile("^ab", re.I), None, "match", "ABcd", "AB"),
33+
(re.compile("^abcd"), None, "match", "abcd", "abcd"),
34+
(re.compile("^abcd", re.I), None, "match", "ABcd", "ABcd"),
35+
(re.compile(r"^\w+"), None, "match", "abcd", "abcd"),
36+
(re.compile(r"^\w+", re.I), None, "match", "ABcd", "ABcd"),
37+
# fullmatch mode
38+
("^abcd", None, "fullmatch", "abcd", "abcd"),
39+
("^abcd", re.I, "fullmatch", "ABcd", "ABcd"),
40+
("^abcd$", None, "fullmatch", "abcd", "abcd"),
41+
("^abcd$", re.I, "fullmatch", "ABcd", "ABcd"),
42+
(r"^\w+", None, "fullmatch", "abcd", "abcd"),
43+
(r"^\w+", re.I, "fullmatch", "ABcd", "ABcd"),
44+
(r"^\w+$", None, "fullmatch", "abcd", "abcd"),
45+
(r"^\w+$", re.I, "fullmatch", "ABcd", "ABcd"),
46+
(re.compile("^abcd"), None, "fullmatch", "abcd", "abcd"),
47+
(re.compile("^abcd", re.I), None, "fullmatch", "ABcd", "ABcd"),
48+
(re.compile("^abcd$"), None, "fullmatch", "abcd", "abcd"),
49+
(re.compile("^abcd$", re.I), None, "fullmatch", "ABcd", "ABcd"),
50+
(re.compile(r"^\w+"), None, "fullmatch", "abcd", "abcd"),
51+
(re.compile(r"^\w+", re.I), None, "fullmatch", "ABcd", "ABcd"),
52+
(re.compile(r"^\w+$"), None, "fullmatch", "abcd", "abcd"),
53+
(re.compile(r"^\w+$", re.I), None, "fullmatch", "ABcd", "ABcd"),
54+
# search mode
55+
("^a", None, "search", "abcd", "a"),
56+
("^a", re.I, "search", "ABcd", "A"),
57+
("bc", None, "search", "abcd", "bc"),
58+
("bc", re.I, "search", "ABcd", "Bc"),
59+
("cd$", None, "search", "abcd", "cd"),
60+
("cd$", re.I, "search", "ABcd", "cd"),
61+
(r"\w", None, "search", "abcd", "a"),
62+
(r"\w", re.I, "search", "ABcd", "A"),
63+
(r"\w$", None, "search", "abcd", "d"),
64+
(r"\w$", re.I, "search", "ABcd", "d"),
65+
(r"\w+", None, "search", "abcd", "abcd"),
66+
(r"\w+", re.I, "search", "ABcd", "ABcd"),
67+
(r"\w+$", None, "search", "abcd", "abcd"),
68+
(r"\w+$", re.I, "search", "ABcd", "ABcd"),
69+
(re.compile("^a"), None, "search", "abcd", "a"),
70+
(re.compile("^a", re.I), None, "search", "ABcd", "A"),
71+
(re.compile(r"d$"), None, "search", "abcd", "d"),
72+
(re.compile(r"d$", re.I), None, "search", "ABcd", "d"),
73+
(re.compile("bc"), None, "search", "abcd", "bc"),
74+
(re.compile("bc", re.I), None, "search", "ABcd", "Bc"),
75+
(re.compile(r"\w"), None, "search", "abcd", "a"),
76+
(re.compile(r"\w", re.I), None, "search", "ABcd", "A"),
77+
(re.compile(r"\w+"), None, "search", "abcd", "abcd"),
78+
(re.compile(r"\w+", re.I), None, "search", "ABcd", "ABcd"),
2379
],
2480
)
2581
def test_regex_passes(
26-
re_pattern, re_flags, test_v, expected_v, dummy_form, dummy_field
82+
re_pattern, re_flags, re_mode, test_v, expected_v, dummy_form, dummy_field
2783
):
2884
"""
2985
Regex should pass if there is a match.
30-
Should work for complie regex too
86+
Should work for compile regex too
3187
"""
32-
validator = regexp(re_pattern, re_flags) if re_flags else regexp(re_pattern)
88+
kwargs = {
89+
"regex": re_pattern,
90+
"flags": re_flags if re_flags else 0,
91+
"message": None,
92+
"mode": re_mode,
93+
}
94+
validator = regexp(**kwargs)
3395
dummy_field.data = test_v
3496
assert validator(dummy_form, dummy_field).group(0) == expected_v
3597

3698

3799
@pytest.mark.parametrize(
38-
"re_pattern, re_flags, test_v",
100+
"re_pattern, re_flags, re_mode, test_v",
39101
[
40-
("^a", None, "ABC"),
41-
("^a", re.I, "foo"),
42-
("^a", None, None),
43-
(re.compile("^a"), None, "foo"),
44-
(re.compile("^a", re.I), None, None),
102+
# math mode
103+
("^a", None, "match", "ABC"),
104+
("^a", re.I, "match", "foo"),
105+
("^a", None, "match", None),
106+
("^ab", None, "match", "ABC"),
107+
("^ab", re.I, "match", "foo"),
108+
("^ab", None, "match", None),
109+
("^ab$", None, "match", "ABC"),
110+
("^ab$", re.I, "match", "foo"),
111+
("^ab$", None, "match", None),
112+
(re.compile("^a"), None, "match", "ABC"),
113+
(re.compile("^a", re.I), None, "match", "foo"),
114+
(re.compile("^a"), None, "match", None),
115+
(re.compile("^ab"), None, "match", "ABC"),
116+
(re.compile("^ab", re.I), None, "match", "foo"),
117+
(re.compile("^ab"), None, "match", None),
118+
(re.compile("^ab$"), None, "match", "ABC"),
119+
(re.compile("^ab$", re.I), None, "match", "foo"),
120+
(re.compile("^ab$"), None, "match", None),
121+
# fullmatch mode
122+
("^abcd", None, "fullmatch", "abc"),
123+
("^abcd", re.I, "fullmatch", "abc"),
124+
("^abcd", None, "fullmatch", "foo"),
125+
("^abcd", re.I, "fullmatch", "foo"),
126+
("^abcd", None, "fullmatch", None),
127+
("^abcd", re.I, "fullmatch", None),
128+
("abcd$", None, "fullmatch", "abc"),
129+
("abcd$", re.I, "fullmatch", "abc"),
130+
("abcd$", None, "fullmatch", "foo"),
131+
("abcd$", re.I, "fullmatch", "foo"),
132+
("abcd$", None, "fullmatch", None),
133+
("abcd$", re.I, "fullmatch", None),
134+
("^abcd$", None, "fullmatch", "abc"),
135+
("^abcd$", re.I, "fullmatch", "abc"),
136+
("^abcd$", None, "fullmatch", "foo"),
137+
("^abcd$", re.I, "fullmatch", "foo"),
138+
("^abcd$", None, "fullmatch", None),
139+
("^abcd$", re.I, "fullmatch", None),
140+
(re.compile("^abcd"), None, "fullmatch", "abc"),
141+
(re.compile("^abcd", re.I), None, "fullmatch", "abc"),
142+
(re.compile("^abcd"), None, "fullmatch", "foo"),
143+
(re.compile("^abcd", re.I), None, "fullmatch", "foo"),
144+
(re.compile("^abcd"), None, "fullmatch", None),
145+
(re.compile("^abcd", re.I), None, "fullmatch", None),
146+
(re.compile("abcd$"), None, "fullmatch", "abc"),
147+
(re.compile("abcd$", re.I), None, "fullmatch", "abc"),
148+
(re.compile("abcd$"), None, "fullmatch", "foo"),
149+
(re.compile("abcd$", re.I), None, "fullmatch", "foo"),
150+
(re.compile("abcd$"), None, "fullmatch", None),
151+
(re.compile("abcd$", re.I), None, "fullmatch", None),
152+
(re.compile("^abcd$"), None, "fullmatch", "abc"),
153+
(re.compile("^abcd$", re.I), None, "fullmatch", "abc"),
154+
(re.compile("^abcd$"), None, "fullmatch", "foo"),
155+
(re.compile("^abcd$", re.I), None, "fullmatch", "foo"),
156+
(re.compile("^abcd$"), None, "fullmatch", None),
157+
(re.compile("^abcd$", re.I), None, "fullmatch", None),
158+
# search mode
159+
("^a", None, "search", "foo"),
160+
("^a", re.I, "search", "foo"),
161+
("^a", None, "search", None),
162+
("^a", re.I, "search", None),
163+
("bc", None, "search", "foo"),
164+
("bc", re.I, "search", "foo"),
165+
("bc", None, "search", None),
166+
("bc", re.I, "search", None),
167+
("cd$", None, "search", "foo"),
168+
("cd$", re.I, "search", "foo"),
169+
("cd$", None, "search", None),
170+
("cd$", re.I, "search", None),
171+
(re.compile("^a"), None, "search", "foo"),
172+
(re.compile("^a", re.I), None, "search", "foo"),
173+
(re.compile("^a"), None, "search", None),
174+
(re.compile("^a", re.I), None, "search", None),
175+
(re.compile("bc"), None, "search", "foo"),
176+
(re.compile("bc", re.I), None, "search", "foo"),
177+
(re.compile("bc"), None, "search", None),
178+
(re.compile("bc", re.I), None, "search", None),
179+
(re.compile(r"cd$"), None, "search", "foo"),
180+
(re.compile(r"cd$", re.I), None, "search", "foo"),
181+
(re.compile(r"cd$"), None, "search", None),
182+
(re.compile(r"cd$", re.I), None, "search", None),
45183
],
46184
)
47-
def test_regex_raises(re_pattern, re_flags, test_v, dummy_form, dummy_field):
185+
def test_regex_raises(re_pattern, re_flags, re_mode, test_v, dummy_form, dummy_field):
48186
"""
49187
Regex should raise ValidationError if there is no match
50-
Should work for complie regex too
188+
Should work for compile regex too
51189
"""
52-
validator = regexp(re_pattern, re_flags) if re_flags else regexp(re_pattern)
190+
kwargs = {
191+
"regex": re_pattern,
192+
"flags": re_flags if re_flags else 0,
193+
"message": None,
194+
"mode": re_mode,
195+
}
196+
validator = regexp(**kwargs)
53197
dummy_field.data = test_v
198+
54199
with pytest.raises(ValidationError):
55200
validator(dummy_form, dummy_field)
56201

@@ -62,3 +207,45 @@ def test_regexp_message(dummy_form, dummy_field):
62207
validator = regexp("^a", message="foo")
63208
dummy_field.data = "f"
64209
assert grab_error_message(validator, dummy_form, dummy_field) == "foo"
210+
211+
212+
@pytest.mark.parametrize(
213+
"re_mode",
214+
[
215+
"MATCH",
216+
"SEARCH",
217+
"FULLMATCH",
218+
"Match",
219+
"Search",
220+
"Fullmatch",
221+
"",
222+
"match ",
223+
" match",
224+
"search ",
225+
" search",
226+
"fullmatch ",
227+
" fullmatch",
228+
None,
229+
1,
230+
1.0,
231+
True,
232+
False,
233+
[],
234+
{},
235+
(),
236+
],
237+
)
238+
def test_regex_invalid_mode(dummy_form, dummy_field, re_mode):
239+
"""
240+
Regexp validator should raise ValidatorSetupError during an object instantiation,
241+
if mode is invalid (unsupported).
242+
"""
243+
with pytest.raises(ValidatorSetupError) as e:
244+
regexp("^a", mode=re_mode)
245+
246+
expected = (
247+
"Invalid mode value '{}'. "
248+
"Supported values: search, match, fullmatch"
249+
).format(re_mode)
250+
251+
assert e.value.args[0] == expected

0 commit comments

Comments
 (0)