Skip to content

Commit b036194

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

File tree

3 files changed

+227
-21
lines changed

3 files changed

+227
-21
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

+29-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,14 @@
4041
)
4142

4243

44+
class ValidatorSetupError(ValueError):
45+
"""
46+
Raised when a validator is configured improperly.
47+
"""
48+
def __init__(self, message="", *args, **kwargs):
49+
ValueError.__init__(self, message, *args, **kwargs)
50+
51+
4352
class ValidationError(ValueError):
4453
"""
4554
Raised when a validator fails to validate its input.
@@ -340,16 +349,34 @@ class Regexp:
340349
`regex` is not a string.
341350
:param message:
342351
Error message to raise in case of a validation error.
352+
:param mode:
353+
The matching mode to use. Must be one of "search", "match", or
354+
"fullmatch". Defaults to "match".
343355
"""
344356

345-
def __init__(self, regex, flags=0, message=None):
357+
_supported_modes = ("search", "match", "fullmatch")
358+
359+
def __init__(self, regex, flags=0, message=None, mode="match"):
360+
self.mode = self._validate_mode(mode)
346361
if isinstance(regex, str):
347362
regex = re.compile(regex, flags)
348363
self.regex = regex
349364
self.message = message
350365

366+
def _validate_mode(self, mode):
367+
if mode not in self._supported_modes:
368+
raise ValidatorSetupError(
369+
"Invalid regexp mode. Supported values: {}".format(", ".join(self._supported_modes))
370+
)
371+
return mode
372+
373+
def _get_validator(self):
374+
return getattr(self.regex, self.mode)
375+
351376
def __call__(self, form, field, message=None):
352-
match = self.regex.match(field.data or "")
377+
validator = self._get_validator()
378+
379+
match = validator(field.data or "")
353380
if match:
354381
return match
355382

tests/validators/test_regexp.py

+197-18
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from wtforms.validators import regexp
5+
from wtforms.validators import regexp, ValidatorSetupError
66
from wtforms.validators import ValidationError
77

88

@@ -14,43 +14,187 @@ def grab_error_message(callable, form, field):
1414

1515

1616
@pytest.mark.parametrize(
17-
"re_pattern, re_flags, test_v, expected_v",
17+
"re_pattern, re_flags, re_mode, test_v, expected_v",
1818
[
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"),
19+
# match mode
20+
("^a", None, "match", "abcd", "a"),
21+
("^a", re.I, "match", "ABcd", "A"),
22+
("^ab", None, "match", "abcd", "ab"),
23+
("^ab", re.I, "match", "ABcd", "AB"),
24+
("^abcd", None, "match", "abcd", "abcd"),
25+
("^abcd", re.I, "match", "ABcd", "ABcd"),
26+
(r"^\w+", None, "match", "abcd", "abcd"),
27+
(r"^\w+", re.I, "match", "ABcd", "ABcd"),
28+
(re.compile("^a"), None, "match", "abcd", "a"),
29+
(re.compile("^a", re.I), None, "match", "ABcd", "A"),
30+
(re.compile("^ab"), None, "match", "abcd", "ab"),
31+
(re.compile("^ab", re.I), None, "match", "ABcd", "AB"),
32+
(re.compile("^abcd"), None, "match", "abcd", "abcd"),
33+
(re.compile("^abcd", re.I), None, "match", "ABcd", "ABcd"),
34+
(re.compile(r"^\w+"), None, "match", "abcd", "abcd"),
35+
(re.compile(r"^\w+", re.I), None, "match", "ABcd", "ABcd"),
36+
# fullmatch mode
37+
("^abcd", None, "fullmatch", "abcd", "abcd"),
38+
("^abcd", re.I, "fullmatch", "ABcd", "ABcd"),
39+
("^abcd$", None, "fullmatch", "abcd", "abcd"),
40+
("^abcd$", re.I, "fullmatch", "ABcd", "ABcd"),
41+
(r"^\w+", None, "fullmatch", "abcd", "abcd"),
42+
(r"^\w+", re.I, "fullmatch", "ABcd", "ABcd"),
43+
(r"^\w+$", None, "fullmatch", "abcd", "abcd"),
44+
(r"^\w+$", re.I, "fullmatch", "ABcd", "ABcd"),
45+
(re.compile("^abcd"), None, "fullmatch", "abcd", "abcd"),
46+
(re.compile("^abcd", re.I), None, "fullmatch", "ABcd", "ABcd"),
47+
(re.compile("^abcd$"), None, "fullmatch", "abcd", "abcd"),
48+
(re.compile("^abcd$", re.I), None, "fullmatch", "ABcd", "ABcd"),
49+
(re.compile(r"^\w+"), None, "fullmatch", "abcd", "abcd"),
50+
(re.compile(r"^\w+", re.I), None, "fullmatch", "ABcd", "ABcd"),
51+
(re.compile(r"^\w+$"), None, "fullmatch", "abcd", "abcd"),
52+
(re.compile(r"^\w+$", re.I), None, "fullmatch", "ABcd", "ABcd"),
53+
# search mode
54+
("^a", None, "search", "abcd", "a"),
55+
("^a", re.I, "search", "ABcd", "A"),
56+
("bc", None, "search", "abcd", "bc"),
57+
("bc", re.I, "search", "ABcd", "Bc"),
58+
("cd$", None, "search", "abcd", "cd"),
59+
("cd$", re.I, "search", "ABcd", "cd"),
60+
(r"\w", None, "search", "abcd", "a"),
61+
(r"\w", re.I, "search", "ABcd", "A"),
62+
(r"\w$", None, "search", "abcd", "d"),
63+
(r"\w$", re.I, "search", "ABcd", "d"),
64+
(r"\w+", None, "search", "abcd", "abcd"),
65+
(r"\w+", re.I, "search", "ABcd", "ABcd"),
66+
(r"\w+$", None, "search", "abcd", "abcd"),
67+
(r"\w+$", re.I, "search", "ABcd", "ABcd"),
68+
(re.compile("^a"), None, "search", "abcd", "a"),
69+
(re.compile("^a", re.I), None, "search", "ABcd", "A"),
70+
(re.compile(r"d$"), None, "search", "abcd", "d"),
71+
(re.compile(r"d$", re.I), None, "search", "ABcd", "d"),
72+
(re.compile("bc"), None, "search", "abcd", "bc"),
73+
(re.compile("bc", re.I), None, "search", "ABcd", "Bc"),
74+
(re.compile(r"\w"), None, "search", "abcd", "a"),
75+
(re.compile(r"\w", re.I), None, "search", "ABcd", "A"),
76+
(re.compile(r"\w+"), None, "search", "abcd", "abcd"),
77+
(re.compile(r"\w+", re.I), None, "search", "ABcd", "ABcd"),
2378
],
2479
)
2580
def test_regex_passes(
26-
re_pattern, re_flags, test_v, expected_v, dummy_form, dummy_field
81+
re_pattern, re_flags, re_mode, test_v, expected_v, dummy_form, dummy_field
2782
):
2883
"""
2984
Regex should pass if there is a match.
30-
Should work for complie regex too
85+
Should work for compile regex too
3186
"""
32-
validator = regexp(re_pattern, re_flags) if re_flags else regexp(re_pattern)
87+
kwargs = {
88+
"regex": re_pattern,
89+
"flags": re_flags if re_flags else 0,
90+
"message": None,
91+
"mode": re_mode,
92+
}
93+
validator = regexp(**kwargs)
3394
dummy_field.data = test_v
3495
assert validator(dummy_form, dummy_field).group(0) == expected_v
3596

3697

3798
@pytest.mark.parametrize(
38-
"re_pattern, re_flags, test_v",
99+
"re_pattern, re_flags, re_mode, test_v",
39100
[
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),
101+
# math mode
102+
("^a", None, "match", "ABC"),
103+
("^a", re.I, "match", "foo"),
104+
("^a", None, "match", None),
105+
("^ab", None, "match", "ABC"),
106+
("^ab", re.I, "match", "foo"),
107+
("^ab", None, "match", None),
108+
("^ab$", None, "match", "ABC"),
109+
("^ab$", re.I, "match", "foo"),
110+
("^ab$", None, "match", None),
111+
(re.compile("^a"), None, "match", "ABC"),
112+
(re.compile("^a", re.I), None, "match", "foo"),
113+
(re.compile("^a"), None, "match", None),
114+
(re.compile("^ab"), None, "match", "ABC"),
115+
(re.compile("^ab", re.I), None, "match", "foo"),
116+
(re.compile("^ab"), None, "match", None),
117+
(re.compile("^ab$"), None, "match", "ABC"),
118+
(re.compile("^ab$", re.I), None, "match", "foo"),
119+
(re.compile("^ab$"), None, "match", None),
120+
# fullmatch mode
121+
("^abcd", None, "fullmatch", "abc"),
122+
("^abcd", re.I, "fullmatch", "abc"),
123+
("^abcd", None, "fullmatch", "foo"),
124+
("^abcd", re.I, "fullmatch", "foo"),
125+
("^abcd", None, "fullmatch", None),
126+
("^abcd", re.I, "fullmatch", None),
127+
("abcd$", None, "fullmatch", "abc"),
128+
("abcd$", re.I, "fullmatch", "abc"),
129+
("abcd$", None, "fullmatch", "foo"),
130+
("abcd$", re.I, "fullmatch", "foo"),
131+
("abcd$", None, "fullmatch", None),
132+
("abcd$", re.I, "fullmatch", None),
133+
("^abcd$", None, "fullmatch", "abc"),
134+
("^abcd$", re.I, "fullmatch", "abc"),
135+
("^abcd$", None, "fullmatch", "foo"),
136+
("^abcd$", re.I, "fullmatch", "foo"),
137+
("^abcd$", None, "fullmatch", None),
138+
("^abcd$", re.I, "fullmatch", None),
139+
(re.compile("^abcd"), None, "fullmatch", "abc"),
140+
(re.compile("^abcd", re.I), None, "fullmatch", "abc"),
141+
(re.compile("^abcd"), None, "fullmatch", "foo"),
142+
(re.compile("^abcd", re.I), None, "fullmatch", "foo"),
143+
(re.compile("^abcd"), None, "fullmatch", None),
144+
(re.compile("^abcd", re.I), None, "fullmatch", None),
145+
(re.compile("abcd$"), None, "fullmatch", "abc"),
146+
(re.compile("abcd$", re.I), None, "fullmatch", "abc"),
147+
(re.compile("abcd$"), None, "fullmatch", "foo"),
148+
(re.compile("abcd$", re.I), None, "fullmatch", "foo"),
149+
(re.compile("abcd$"), None, "fullmatch", None),
150+
(re.compile("abcd$", re.I), None, "fullmatch", None),
151+
(re.compile("^abcd$"), None, "fullmatch", "abc"),
152+
(re.compile("^abcd$", re.I), None, "fullmatch", "abc"),
153+
(re.compile("^abcd$"), None, "fullmatch", "foo"),
154+
(re.compile("^abcd$", re.I), None, "fullmatch", "foo"),
155+
(re.compile("^abcd$"), None, "fullmatch", None),
156+
(re.compile("^abcd$", re.I), None, "fullmatch", None),
157+
# search mode
158+
("^a", None, "search", "foo"),
159+
("^a", re.I, "search", "foo"),
160+
("^a", None, "search", None),
161+
("^a", re.I, "search", None),
162+
("bc", None, "search", "foo"),
163+
("bc", re.I, "search", "foo"),
164+
("bc", None, "search", None),
165+
("bc", re.I, "search", None),
166+
("cd$", None, "search", "foo"),
167+
("cd$", re.I, "search", "foo"),
168+
("cd$", None, "search", None),
169+
("cd$", re.I, "search", None),
170+
(re.compile("^a"), None, "search", "foo"),
171+
(re.compile("^a", re.I), None, "search", "foo"),
172+
(re.compile("^a"), None, "search", None),
173+
(re.compile("^a", re.I), None, "search", None),
174+
(re.compile("bc"), None, "search", "foo"),
175+
(re.compile("bc", re.I), None, "search", "foo"),
176+
(re.compile("bc"), None, "search", None),
177+
(re.compile("bc", re.I), None, "search", None),
178+
(re.compile(r"cd$"), None, "search", "foo"),
179+
(re.compile(r"cd$", re.I), None, "search", "foo"),
180+
(re.compile(r"cd$"), None, "search", None),
181+
(re.compile(r"cd$", re.I), None, "search", None),
45182
],
46183
)
47-
def test_regex_raises(re_pattern, re_flags, test_v, dummy_form, dummy_field):
184+
def test_regex_raises(re_pattern, re_flags, re_mode, test_v, dummy_form, dummy_field):
48185
"""
49186
Regex should raise ValidationError if there is no match
50-
Should work for complie regex too
187+
Should work for compile regex too
51188
"""
52-
validator = regexp(re_pattern, re_flags) if re_flags else regexp(re_pattern)
189+
kwargs = {
190+
"regex": re_pattern,
191+
"flags": re_flags if re_flags else 0,
192+
"message": None,
193+
"mode": re_mode,
194+
}
195+
validator = regexp(**kwargs)
53196
dummy_field.data = test_v
197+
54198
with pytest.raises(ValidationError):
55199
validator(dummy_form, dummy_field)
56200

@@ -62,3 +206,38 @@ def test_regexp_message(dummy_form, dummy_field):
62206
validator = regexp("^a", message="foo")
63207
dummy_field.data = "f"
64208
assert grab_error_message(validator, dummy_form, dummy_field) == "foo"
209+
210+
211+
@pytest.mark.parametrize(
212+
"re_mode",
213+
[
214+
"MATCH",
215+
"SEARCH",
216+
"FULLMATCH",
217+
"Match",
218+
"Search",
219+
"Fullmatch",
220+
"",
221+
"match ",
222+
" match",
223+
"search ",
224+
" search",
225+
"fullmatch ",
226+
" fullmatch",
227+
None,
228+
1,
229+
1.0,
230+
True,
231+
False,
232+
[],
233+
{},
234+
(),
235+
]
236+
)
237+
def test_regex_invalid_mode(dummy_form, dummy_field, re_mode):
238+
"""
239+
Regexp validator should raise ValidatorSetupError during an object instantiation,
240+
if mode is invalid (unsupported).
241+
"""
242+
with pytest.raises(ValidatorSetupError):
243+
regexp("^a", mode=re_mode)

0 commit comments

Comments
 (0)