From 9592bbc54534793618ab158775af0cc68b362460 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Fri, 18 Aug 2023 02:12:46 +0300 Subject: [PATCH] Regexp functions support (#13) * feat!: other regexp functions support * feat: add non-breakable regex * chore: add tests for regex * Expand iterable input types in magic_filter methods Modified the 'in_' and 'not_in' methods in 'magic_filter/magic.py' to accept both 'Container' and 'MagicT' types as iterable. This change provides more flexibility in specifying iterables in these methods and broadens their potential usage. * fix: regexp typo --------- Co-authored-by: JRoot Junior --- magic_filter/__init__.py | 3 +- magic_filter/exceptions.py | 4 ++ magic_filter/magic.py | 40 ++++++++++++++++-- tests/test_magic.py | 84 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 123 insertions(+), 8 deletions(-) diff --git a/magic_filter/__init__.py b/magic_filter/__init__.py index ee10f4a..d9afe6f 100644 --- a/magic_filter/__init__.py +++ b/magic_filter/__init__.py @@ -1,12 +1,13 @@ from . import operations from .attrdict import AttrDict -from .magic import MagicFilter, MagicT +from .magic import MagicFilter, MagicT, RegexpMode __all__ = ( "__version__", "operations", "MagicFilter", "MagicT", + "RegexpMode", "F", "AttrDict", ) diff --git a/magic_filter/exceptions.py b/magic_filter/exceptions.py index 435ccc3..59e81ab 100644 --- a/magic_filter/exceptions.py +++ b/magic_filter/exceptions.py @@ -17,3 +17,7 @@ class SwitchModeToAny(SwitchMode): class RejectOperations(MagicFilterException): pass + + +class ParamsConflict(MagicFilterException): + pass diff --git a/magic_filter/magic.py b/magic_filter/magic.py index 8ba211e..77266e7 100644 --- a/magic_filter/magic.py +++ b/magic_filter/magic.py @@ -2,8 +2,14 @@ import re from functools import wraps from typing import Any, Callable, Container, Optional, Pattern, Tuple, Type, TypeVar, Union +from warnings import warn -from magic_filter.exceptions import RejectOperations, SwitchModeToAll, SwitchModeToAny +from magic_filter.exceptions import ( + ParamsConflict, + RejectOperations, + SwitchModeToAll, + SwitchModeToAny, +) from magic_filter.operations import ( BaseOperation, CallOperation, @@ -24,6 +30,14 @@ MagicT = TypeVar("MagicT", bound="MagicFilter") +class RegexpMode: + SEARCH = "search" + MATCH = "match" + FINDALL = "findall" + FINDITER = "finditer" + FULLMATCH = "fullmatch" + + class MagicFilter: __slots__ = ("_operations",) @@ -240,13 +254,31 @@ def regexp( self: MagicT, pattern: Union[str, Pattern[str]], *, - search: bool = False, + mode: Optional[str] = None, + search: Optional[bool] = None, flags: Union[int, re.RegexFlag] = 0, ) -> MagicT: + + if search is not None: + warn( + "Param 'search' is deprecated, use 'mode' instead.", + DeprecationWarning, + ) + + if mode is not None: + msg = "Can't pass both 'search' and 'mode' params." + raise ParamsConflict(msg) + + mode = RegexpMode.SEARCH if search else RegexpMode.MATCH + + if mode is None: + mode = RegexpMode.MATCH + if isinstance(pattern, str): pattern = re.compile(pattern, flags=flags) - regexp_mode = pattern.search if search else pattern.match - return self._extend(FunctionOperation(regexp_mode)) + + regexp_func = getattr(pattern, mode) + return self._extend(FunctionOperation(regexp_func)) def func(self: MagicT, func: Callable[[Any], Any], *args: Any, **kwargs: Any) -> MagicT: return self._extend(FunctionOperation(func, *args, **kwargs)) diff --git a/tests/test_magic.py b/tests/test_magic.py index 5844175..e435d0d 100644 --- a/tests/test_magic.py +++ b/tests/test_magic.py @@ -1,9 +1,11 @@ +import warnings from collections import namedtuple from typing import Any, NamedTuple, Optional import pytest -from magic_filter import F, MagicFilter +from magic_filter import F, MagicFilter, RegexpMode +from magic_filter.exceptions import ParamsConflict Job = namedtuple("Job", ["place", "salary", "position"]) @@ -84,8 +86,6 @@ class TestMagicFilter: F.age.in_(range(15, 40)), F.job.place.in_({"New York", "WDC"}), F.age.not_in(range(40, 100)), - F.about.regexp(r"Gonna .+"), - F.about.regexp(r".+"), F.about.contains("Factory"), F.job.place.lower().contains("n"), F.job.place.upper().contains("N"), @@ -159,3 +159,81 @@ def test_extract_operation(self): def test_bool(self): case = F.foo.bar.baz assert bool(case) is True + + +class TestMagicRegexpFilter: + @pytest.mark.parametrize( + "case,result", + [ + (F.about.regexp(r"Gonna .+"), True), + (F.about.regexp(r".+"), True), + (F.about.regexp(r"Gonna .+", mode=RegexpMode.MATCH), True), + (F.about.regexp(r"fly"), False), + (F.about.regexp(r"fly", search=False), False), + ], + ) + def test_match(self, case: MagicFilter, user: User, result: bool): + assert bool(case.resolve(user)) is result + + @pytest.mark.parametrize( + "case,result", + [ + (F.about.regexp(r"fly", search=True), True), + (F.about.regexp(r"fly", mode=RegexpMode.SEARCH), True), + (F.about.regexp(r"run", mode=RegexpMode.SEARCH), False), + ], + ) + def test_search(self, case: MagicFilter, user: User, result: bool): + assert bool(case.resolve(user)) is result + + @pytest.mark.parametrize( + "case,result", + [ + (F.job.place.regexp(r"[A-Z]", mode=RegexpMode.FINDALL), ['N', 'Y']), + ], + ) + def test_findall(self, case: MagicFilter, user: User, result: bool): + assert case.resolve(user) == result + + @pytest.mark.parametrize( + "case,result", + [ + (F.about.regexp(r"(\w{5,})", mode=RegexpMode.FINDITER), + ['Gonna', 'Factory']), + ], + ) + def test_finditer(self, case: MagicFilter, user: User, result: bool): + assert [m.group() for m in case.resolve(user)] == result + + @pytest.mark.parametrize( + "case,result", + [ + (F.job.place.regexp(r"New York", mode=RegexpMode.FULLMATCH), True), + (F.job.place.regexp(r"Old York", mode=RegexpMode.FULLMATCH), False), + ], + ) + def test_full_match(self, case: MagicFilter, user: User, result: bool): + assert bool(case.resolve(user)) is result + + @pytest.mark.parametrize("search", [True, False]) + def test_search_deprecation(self, search): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + F.about.regexp(r"test deprecation", search=search) + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + + @pytest.mark.parametrize( + "mode", + [ + RegexpMode.SEARCH, + RegexpMode.MATCH, + RegexpMode.FULLMATCH, + RegexpMode.FINDALL, + RegexpMode.FINDITER, + ] + ) + @pytest.mark.parametrize("search", [True, False]) + def test_params_conflict(self, search, mode): + with pytest.raises(ParamsConflict): + F.about.regexp(r"", search=search, mode=mode)