Add support for re.Pattern to validators.matches_re() (#877)
Fixes #875. This adds support for pre-compiled regular expressions to attr.validators.matches_re(). This cannot be combined with flags since the pre-compiled pattern already has those. Detailed changes: - attr.validators.matches_re() now accepts re.Pattern in addition to strings; update type annotations accordingly. - Convert percent-formatting into str.format() for an error message - Simplify (private) _MatchesReValidator helper class a bit: use the actual compiled pattern, and drop the unused .flags attribute. - Simplify control flow a bit; add pointer about fullmatch emulation. - Add tests - Tweak existing test to ensure that .fullmatch() actually works correctly by matching a pattern that also matches (but not ‘full-matches’) a shorter substring. Co-authored-by: Hynek Schlawack <hs@ox.cx>
This commit is contained in:
parent
fe19f4b341
commit
1759260aa0
|
@ -0,0 +1 @@
|
|||
``attr.validators.matches_re()`` now accepts pre-compiled regular expressions in addition to pattern strings.
|
|
@ -14,6 +14,12 @@ from ._make import _AndValidator, and_, attrib, attrs
|
|||
from .exceptions import NotCallableError
|
||||
|
||||
|
||||
try:
|
||||
Pattern = re.Pattern
|
||||
except AttributeError: # Python <3.7 lacks a Pattern type.
|
||||
Pattern = type(re.compile(""))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"and_",
|
||||
"deep_iterable",
|
||||
|
@ -129,8 +135,7 @@ def instance_of(type):
|
|||
|
||||
@attrs(repr=False, frozen=True, slots=True)
|
||||
class _MatchesReValidator(object):
|
||||
regex = attrib()
|
||||
flags = attrib()
|
||||
pattern = attrib()
|
||||
match_func = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
|
@ -139,18 +144,18 @@ class _MatchesReValidator(object):
|
|||
"""
|
||||
if not self.match_func(value):
|
||||
raise ValueError(
|
||||
"'{name}' must match regex {regex!r}"
|
||||
"'{name}' must match regex {pattern!r}"
|
||||
" ({value!r} doesn't)".format(
|
||||
name=attr.name, regex=self.regex.pattern, value=value
|
||||
name=attr.name, pattern=self.pattern.pattern, value=value
|
||||
),
|
||||
attr,
|
||||
self.regex,
|
||||
self.pattern,
|
||||
value,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<matches_re validator for pattern {regex!r}>".format(
|
||||
regex=self.regex
|
||||
return "<matches_re validator for pattern {pattern!r}>".format(
|
||||
pattern=self.pattern
|
||||
)
|
||||
|
||||
|
||||
|
@ -159,7 +164,7 @@ def matches_re(regex, flags=0, func=None):
|
|||
A validator that raises `ValueError` if the initializer is called
|
||||
with a string that doesn't match *regex*.
|
||||
|
||||
:param str regex: a regex string to match against
|
||||
:param regex: a regex string or precompiled pattern to match against
|
||||
:param int flags: flags that will be passed to the underlying re function
|
||||
(default 0)
|
||||
:param callable func: which underlying `re` function to call (options
|
||||
|
@ -169,34 +174,44 @@ def matches_re(regex, flags=0, func=None):
|
|||
but on a pre-`re.compile`\ ed pattern.
|
||||
|
||||
.. versionadded:: 19.2.0
|
||||
.. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern.
|
||||
"""
|
||||
fullmatch = getattr(re, "fullmatch", None)
|
||||
valid_funcs = (fullmatch, None, re.search, re.match)
|
||||
if func not in valid_funcs:
|
||||
raise ValueError(
|
||||
"'func' must be one of %s."
|
||||
% (
|
||||
"'func' must be one of {}.".format(
|
||||
", ".join(
|
||||
sorted(
|
||||
e and e.__name__ or "None" for e in set(valid_funcs)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
pattern = re.compile(regex, flags)
|
||||
if isinstance(regex, Pattern):
|
||||
if flags:
|
||||
raise TypeError(
|
||||
"'flags' can only be used with a string pattern; "
|
||||
"pass flags to re.compile() instead"
|
||||
)
|
||||
pattern = regex
|
||||
else:
|
||||
pattern = re.compile(regex, flags)
|
||||
|
||||
if func is re.match:
|
||||
match_func = pattern.match
|
||||
elif func is re.search:
|
||||
match_func = pattern.search
|
||||
else:
|
||||
if fullmatch:
|
||||
match_func = pattern.fullmatch
|
||||
else:
|
||||
pattern = re.compile(r"(?:{})\Z".format(regex), flags)
|
||||
match_func = pattern.match
|
||||
elif fullmatch:
|
||||
match_func = pattern.fullmatch
|
||||
else: # Python 2 fullmatch emulation (https://bugs.python.org/issue16203)
|
||||
pattern = re.compile(
|
||||
r"(?:{})\Z".format(pattern.pattern), pattern.flags
|
||||
)
|
||||
match_func = pattern.match
|
||||
|
||||
return _MatchesReValidator(pattern, flags, match_func)
|
||||
return _MatchesReValidator(pattern, match_func)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, hash=True)
|
||||
|
|
|
@ -9,6 +9,7 @@ from typing import (
|
|||
Mapping,
|
||||
Match,
|
||||
Optional,
|
||||
Pattern,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
|
@ -54,7 +55,7 @@ def optional(
|
|||
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
|
||||
def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
|
||||
def matches_re(
|
||||
regex: AnyStr,
|
||||
regex: Union[Pattern[AnyStr], AnyStr],
|
||||
flags: int = ...,
|
||||
func: Optional[
|
||||
Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]]
|
||||
|
|
|
@ -176,9 +176,9 @@ class TestMatchesRe(object):
|
|||
|
||||
@attr.s
|
||||
class ReTester(object):
|
||||
str_match = attr.ib(validator=matches_re("a"))
|
||||
str_match = attr.ib(validator=matches_re("a|ab"))
|
||||
|
||||
ReTester("a") # shouldn't raise exceptions
|
||||
ReTester("ab") # shouldn't raise exceptions
|
||||
with pytest.raises(TypeError):
|
||||
ReTester(1)
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -197,6 +197,29 @@ class TestMatchesRe(object):
|
|||
|
||||
MatchTester("A1") # test flags and using re.match
|
||||
|
||||
def test_precompiled_pattern(self):
|
||||
"""
|
||||
Pre-compiled patterns are accepted.
|
||||
"""
|
||||
pattern = re.compile("a")
|
||||
|
||||
@attr.s
|
||||
class RePatternTester(object):
|
||||
val = attr.ib(validator=matches_re(pattern))
|
||||
|
||||
RePatternTester("a")
|
||||
|
||||
def test_precompiled_pattern_no_flags(self):
|
||||
"""
|
||||
A pre-compiled pattern cannot be combined with a 'flags' argument.
|
||||
"""
|
||||
pattern = re.compile("")
|
||||
|
||||
with pytest.raises(
|
||||
TypeError, match="can only be used with a string pattern"
|
||||
):
|
||||
matches_re(pattern, flags=re.IGNORECASE)
|
||||
|
||||
def test_different_func(self):
|
||||
"""
|
||||
Changing the match functions works.
|
||||
|
|
|
@ -167,7 +167,7 @@ class Validated:
|
|||
attr.validators.instance_of(C), attr.validators.instance_of(D)
|
||||
),
|
||||
)
|
||||
e: str = attr.ib(validator=attr.validators.matches_re(r"foo"))
|
||||
e: str = attr.ib(validator=attr.validators.matches_re(re.compile(r"foo")))
|
||||
f: str = attr.ib(
|
||||
validator=attr.validators.matches_re(r"foo", flags=42, func=re.search)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue