from typing import Any, Callable, Iterable, TypeVar, assert_never, cast
from pydantic import validate_call
from monkey_wrench.generic._common import apply_to_single_or_collection
from monkey_wrench.generic._types import ListSetTuple, Model
from monkey_wrench.generic.models._function import TransformFunction
OriginalType = TypeVar("OriginalType")
TransformedType = TypeVar("TransformedType")
class Pattern(Model):
"""Pydantic model for finding sub-strings in other strings."""
negate: bool = False
"""A boolean indicating whether the result of pattern matching should be negated, i.e. if one needs a non-match.
In other words, the result of match will be always XORed (^) with this boolean. Defaults to ``False``, which means
the result will not be negated.
"""
sub_strings: str | list[str] | None = None
"""The sub-strings to look for. It can be either a single string, a list of strings, or ``None.``.
Defaults to ``None``, which means :func:`check` returns ``True`` if ``negate`` is also ``False``.
"""
case_sensitive: bool = True
"""A boolean indicating whether to perform a case-sensitive match. Defaults to ``True``."""
match_all: bool = True
"""A boolean indicating whether to match all or any of the sub-strings. Defaults to ``True``.
When it is set to ``False``, only one match suffices. In the case of a single sub-string this parameter does not
have any effect.
"""
@property
def pattern(self) -> "Pattern":
return Pattern(
sub_strings=self.sub_strings,
case_sensitive=self.case_sensitive,
match_all=self.match_all,
negate=self.negate
)
@property
def sub_strings_list(self) -> list[str]:
"""Enclose ``sub_strings`` in a list, if there is only a single sub-string."""
match self.sub_strings:
case None:
return []
case list():
return self.sub_strings
case str():
return [self.sub_strings]
case _:
assert_never(self.sub_strings)
@property
def match_function(self) -> Callable[[Iterable[object]], bool]:
"""Return either ``all()`` or ``any()`` built-in function depending on :attr:`Pattern.match_all`."""
return all if self.match_all else any
@validate_call
def check(self, item: Any) -> bool:
"""Check if the pattern exists in the given item.
Args:
item:
The string in which the sub-strings will be looked for. If the item is not a string, it will be first
converted to a string.
Returns:
A boolean indicating whether all or any (depending on :attr:`Pattern.match_all`) of the sub-strings exist(s)
in the given item.
Examples:
>>> Pattern().check("abcde")
True
>>> Pattern(negate=True).check("abcde")
False
>>> Pattern(sub_strings="ab").check("abcde")
True
>>> Pattern(sub_strings="ab", negate=True).check("abcde")
False
>>> Pattern(sub_strings="A", case_sensitive=False).check("abcde")
True
>>> Pattern(sub_strings=["A", "b"], match_all=False).check("abcde")
True
>>> Pattern(sub_strings=["A", "b"], match_all=True).check("abcde")
False
>>> Pattern(sub_strings=["A", "b"], match_all=True, case_sensitive=False).check("abcde")
True
>>> Pattern(sub_strings=["A", "b"], match_all=True, case_sensitive=False, negate=True).check("abcde")
False
"""
if self.sub_strings is None:
return True ^ self.negate
item = str(item)
_sub_strings = self.sub_strings_list[:]
if not self.case_sensitive:
item = item.lower()
_sub_strings = [s.lower() for s in _sub_strings]
return self.match_function(s in item for s in _sub_strings) ^ self.negate
@validate_call
def __ror__(self, other: str) -> bool:
"""Syntactic sugar for :func:`check`.
Examples:
>>> "abcde" | Pattern()
True
>>> "abcde" | Pattern(negate=True)
False
>>> "abcde" | Pattern(sub_strings=["A", "b"], match_all=True)
False
"""
return self.check(other)