Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "search", "match" and "fullmatch" modes to Regexp validator #871

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ refer to a single input from the form.
Example usage::

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

def validate_image(form, field):
Expand Down
34 changes: 32 additions & 2 deletions src/wtforms/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"mac_address",
"UUID",
"ValidationError",
"ValidatorSetupError",
"StopValidation",
"readonly",
"ReadOnly",
Expand All @@ -40,6 +41,15 @@
)


class ValidatorSetupError(ValueError):
"""
Raised when a validator is configured improperly.
"""

def __init__(self, message="", *args, **kwargs):
ValueError.__init__(self, message, *args, **kwargs)


class ValidationError(ValueError):
"""
Raised when a validator fails to validate its input.
Expand Down Expand Up @@ -340,16 +350,36 @@ class Regexp:
`regex` is not a string.
:param message:
Error message to raise in case of a validation error.
:param mode:
The matching mode to use. Must be one of "search", "match", or
"fullmatch". Defaults to "match".
"""

def __init__(self, regex, flags=0, message=None):
_supported_modes = ("search", "match", "fullmatch")

def __init__(self, regex, flags=0, message=None, mode="match"):
self.mode = self._validate_mode(mode)
Comment on lines +360 to +361
Copy link
Author

@artempronevskiy artempronevskiy Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my POV it's better to validate mode value in __init__() in order to prevent the object instantiating with invalid data. in other words, validating mode value is a check of the invariant (mode field has supported value) and thus the object must not be created/instantiated if any of invariant checks failed.

if isinstance(regex, str):
regex = re.compile(regex, flags)
self.regex = regex
self.message = message

def _validate_mode(self, mode):
if mode not in self._supported_modes:
raise ValidatorSetupError(
"Invalid mode value '{}'. Supported values: {}".format(
mode, ", ".join(self._supported_modes)
)
)
return mode

def _get_validator(self):
return getattr(self.regex, self.mode)

def __call__(self, form, field, message=None):
match = self.regex.match(field.data or "")
validator = self._get_validator()

match = validator(field.data or "")
if match:
return match

Expand Down
220 changes: 203 additions & 17 deletions tests/validators/test_regexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from wtforms.validators import regexp
from wtforms.validators import ValidationError
from wtforms.validators import ValidatorSetupError


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


@pytest.mark.parametrize(
"re_pattern, re_flags, test_v, expected_v",
"re_pattern, re_flags, re_mode, test_v, expected_v",
[
("^a", None, "abcd", "a"),
("^a", re.I, "ABcd", "A"),
(re.compile("^a"), None, "abcd", "a"),
(re.compile("^a", re.I), None, "ABcd", "A"),
# match mode
("^a", None, "match", "abcd", "a"),
("^a", re.I, "match", "ABcd", "A"),
("^ab", None, "match", "abcd", "ab"),
("^ab", re.I, "match", "ABcd", "AB"),
("^abcd", None, "match", "abcd", "abcd"),
("^abcd", re.I, "match", "ABcd", "ABcd"),
(r"^\w+", None, "match", "abcd", "abcd"),
(r"^\w+", re.I, "match", "ABcd", "ABcd"),
(re.compile("^a"), None, "match", "abcd", "a"),
(re.compile("^a", re.I), None, "match", "ABcd", "A"),
(re.compile("^ab"), None, "match", "abcd", "ab"),
(re.compile("^ab", re.I), None, "match", "ABcd", "AB"),
(re.compile("^abcd"), None, "match", "abcd", "abcd"),
(re.compile("^abcd", re.I), None, "match", "ABcd", "ABcd"),
(re.compile(r"^\w+"), None, "match", "abcd", "abcd"),
(re.compile(r"^\w+", re.I), None, "match", "ABcd", "ABcd"),
# fullmatch mode
("^abcd", None, "fullmatch", "abcd", "abcd"),
("^abcd", re.I, "fullmatch", "ABcd", "ABcd"),
("^abcd$", None, "fullmatch", "abcd", "abcd"),
("^abcd$", re.I, "fullmatch", "ABcd", "ABcd"),
(r"^\w+", None, "fullmatch", "abcd", "abcd"),
(r"^\w+", re.I, "fullmatch", "ABcd", "ABcd"),
(r"^\w+$", None, "fullmatch", "abcd", "abcd"),
(r"^\w+$", re.I, "fullmatch", "ABcd", "ABcd"),
(re.compile("^abcd"), None, "fullmatch", "abcd", "abcd"),
(re.compile("^abcd", re.I), None, "fullmatch", "ABcd", "ABcd"),
(re.compile("^abcd$"), None, "fullmatch", "abcd", "abcd"),
(re.compile("^abcd$", re.I), None, "fullmatch", "ABcd", "ABcd"),
(re.compile(r"^\w+"), None, "fullmatch", "abcd", "abcd"),
(re.compile(r"^\w+", re.I), None, "fullmatch", "ABcd", "ABcd"),
(re.compile(r"^\w+$"), None, "fullmatch", "abcd", "abcd"),
(re.compile(r"^\w+$", re.I), None, "fullmatch", "ABcd", "ABcd"),
# search mode
("^a", None, "search", "abcd", "a"),
("^a", re.I, "search", "ABcd", "A"),
("bc", None, "search", "abcd", "bc"),
("bc", re.I, "search", "ABcd", "Bc"),
("cd$", None, "search", "abcd", "cd"),
("cd$", re.I, "search", "ABcd", "cd"),
(r"\w", None, "search", "abcd", "a"),
(r"\w", re.I, "search", "ABcd", "A"),
(r"\w$", None, "search", "abcd", "d"),
(r"\w$", re.I, "search", "ABcd", "d"),
(r"\w+", None, "search", "abcd", "abcd"),
(r"\w+", re.I, "search", "ABcd", "ABcd"),
(r"\w+$", None, "search", "abcd", "abcd"),
(r"\w+$", re.I, "search", "ABcd", "ABcd"),
(re.compile("^a"), None, "search", "abcd", "a"),
(re.compile("^a", re.I), None, "search", "ABcd", "A"),
(re.compile(r"d$"), None, "search", "abcd", "d"),
(re.compile(r"d$", re.I), None, "search", "ABcd", "d"),
(re.compile("bc"), None, "search", "abcd", "bc"),
(re.compile("bc", re.I), None, "search", "ABcd", "Bc"),
(re.compile(r"\w"), None, "search", "abcd", "a"),
(re.compile(r"\w", re.I), None, "search", "ABcd", "A"),
(re.compile(r"\w+"), None, "search", "abcd", "abcd"),
(re.compile(r"\w+", re.I), None, "search", "ABcd", "ABcd"),
],
)
def test_regex_passes(
re_pattern, re_flags, test_v, expected_v, dummy_form, dummy_field
re_pattern, re_flags, re_mode, test_v, expected_v, dummy_form, dummy_field
):
"""
Regex should pass if there is a match.
Should work for complie regex too
Should work for compile regex too
"""
validator = regexp(re_pattern, re_flags) if re_flags else regexp(re_pattern)
kwargs = {
"regex": re_pattern,
"flags": re_flags if re_flags else 0,
"message": None,
"mode": re_mode,
}
validator = regexp(**kwargs)
dummy_field.data = test_v
assert validator(dummy_form, dummy_field).group(0) == expected_v


@pytest.mark.parametrize(
"re_pattern, re_flags, test_v",
"re_pattern, re_flags, re_mode, test_v",
[
("^a", None, "ABC"),
("^a", re.I, "foo"),
("^a", None, None),
(re.compile("^a"), None, "foo"),
(re.compile("^a", re.I), None, None),
# math mode
("^a", None, "match", "ABC"),
("^a", re.I, "match", "foo"),
("^a", None, "match", None),
("^ab", None, "match", "ABC"),
("^ab", re.I, "match", "foo"),
("^ab", None, "match", None),
("^ab$", None, "match", "ABC"),
("^ab$", re.I, "match", "foo"),
("^ab$", None, "match", None),
(re.compile("^a"), None, "match", "ABC"),
(re.compile("^a", re.I), None, "match", "foo"),
(re.compile("^a"), None, "match", None),
(re.compile("^ab"), None, "match", "ABC"),
(re.compile("^ab", re.I), None, "match", "foo"),
(re.compile("^ab"), None, "match", None),
(re.compile("^ab$"), None, "match", "ABC"),
(re.compile("^ab$", re.I), None, "match", "foo"),
(re.compile("^ab$"), None, "match", None),
# fullmatch mode
("^abcd", None, "fullmatch", "abc"),
("^abcd", re.I, "fullmatch", "abc"),
("^abcd", None, "fullmatch", "foo"),
("^abcd", re.I, "fullmatch", "foo"),
("^abcd", None, "fullmatch", None),
("^abcd", re.I, "fullmatch", None),
("abcd$", None, "fullmatch", "abc"),
("abcd$", re.I, "fullmatch", "abc"),
("abcd$", None, "fullmatch", "foo"),
("abcd$", re.I, "fullmatch", "foo"),
("abcd$", None, "fullmatch", None),
("abcd$", re.I, "fullmatch", None),
("^abcd$", None, "fullmatch", "abc"),
("^abcd$", re.I, "fullmatch", "abc"),
("^abcd$", None, "fullmatch", "foo"),
("^abcd$", re.I, "fullmatch", "foo"),
("^abcd$", None, "fullmatch", None),
("^abcd$", re.I, "fullmatch", None),
(re.compile("^abcd"), None, "fullmatch", "abc"),
(re.compile("^abcd", re.I), None, "fullmatch", "abc"),
(re.compile("^abcd"), None, "fullmatch", "foo"),
(re.compile("^abcd", re.I), None, "fullmatch", "foo"),
(re.compile("^abcd"), None, "fullmatch", None),
(re.compile("^abcd", re.I), None, "fullmatch", None),
(re.compile("abcd$"), None, "fullmatch", "abc"),
(re.compile("abcd$", re.I), None, "fullmatch", "abc"),
(re.compile("abcd$"), None, "fullmatch", "foo"),
(re.compile("abcd$", re.I), None, "fullmatch", "foo"),
(re.compile("abcd$"), None, "fullmatch", None),
(re.compile("abcd$", re.I), None, "fullmatch", None),
(re.compile("^abcd$"), None, "fullmatch", "abc"),
(re.compile("^abcd$", re.I), None, "fullmatch", "abc"),
(re.compile("^abcd$"), None, "fullmatch", "foo"),
(re.compile("^abcd$", re.I), None, "fullmatch", "foo"),
(re.compile("^abcd$"), None, "fullmatch", None),
(re.compile("^abcd$", re.I), None, "fullmatch", None),
# search mode
("^a", None, "search", "foo"),
("^a", re.I, "search", "foo"),
("^a", None, "search", None),
("^a", re.I, "search", None),
("bc", None, "search", "foo"),
("bc", re.I, "search", "foo"),
("bc", None, "search", None),
("bc", re.I, "search", None),
("cd$", None, "search", "foo"),
("cd$", re.I, "search", "foo"),
("cd$", None, "search", None),
("cd$", re.I, "search", None),
(re.compile("^a"), None, "search", "foo"),
(re.compile("^a", re.I), None, "search", "foo"),
(re.compile("^a"), None, "search", None),
(re.compile("^a", re.I), None, "search", None),
(re.compile("bc"), None, "search", "foo"),
(re.compile("bc", re.I), None, "search", "foo"),
(re.compile("bc"), None, "search", None),
(re.compile("bc", re.I), None, "search", None),
(re.compile(r"cd$"), None, "search", "foo"),
(re.compile(r"cd$", re.I), None, "search", "foo"),
(re.compile(r"cd$"), None, "search", None),
(re.compile(r"cd$", re.I), None, "search", None),
],
)
def test_regex_raises(re_pattern, re_flags, test_v, dummy_form, dummy_field):
def test_regex_raises(re_pattern, re_flags, re_mode, test_v, dummy_form, dummy_field):
"""
Regex should raise ValidationError if there is no match
Should work for complie regex too
Should work for compile regex too
"""
validator = regexp(re_pattern, re_flags) if re_flags else regexp(re_pattern)
kwargs = {
"regex": re_pattern,
"flags": re_flags if re_flags else 0,
"message": None,
"mode": re_mode,
}
validator = regexp(**kwargs)
dummy_field.data = test_v

with pytest.raises(ValidationError):
validator(dummy_form, dummy_field)

Expand All @@ -62,3 +207,44 @@ def test_regexp_message(dummy_form, dummy_field):
validator = regexp("^a", message="foo")
dummy_field.data = "f"
assert grab_error_message(validator, dummy_form, dummy_field) == "foo"


@pytest.mark.parametrize(
"re_mode",
[
"MATCH",
"SEARCH",
"FULLMATCH",
"Match",
"Search",
"Fullmatch",
"",
"match ",
" match",
"search ",
" search",
"fullmatch ",
" fullmatch",
None,
1,
1.0,
True,
False,
[],
{},
(),
],
)
def test_regex_invalid_mode(dummy_form, dummy_field, re_mode):
"""
Regexp validator should raise ValidatorSetupError during an object instantiation,
if mode is invalid (unsupported).
"""
with pytest.raises(ValidatorSetupError) as e:
regexp("^a", mode=re_mode)

expected_msg_tmpl = (
"Invalid mode value '{}'. Supported values: search, match, fullmatch"
)

assert e.value.args[0] == expected_msg_tmpl.format(re_mode)