Skip to content

Commit

Permalink
fix: Add end start and contains support for matchers. Fix import orde…
Browse files Browse the repository at this point in the history
…r. Add not_empty_string and extra main matchers
  • Loading branch information
MartinGotelli committed Aug 25, 2024
1 parent 19d454f commit 83f47a9
Show file tree
Hide file tree
Showing 28 changed files with 473 additions and 167 deletions.
6 changes: 6 additions & 0 deletions src/pytest_matchers/pytest_matchers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@
is_datetime,
is_datetime_string,
is_dict,
is_float,
is_instance,
is_int,
is_iso_8601_date,
is_iso_8601_datetime,
is_iso_8601_time,
is_json,
is_list,
is_number,
is_strict_dict,
is_string,
is_uuid,
not_empty_string,
one_of,
same_value,
)
Expand Down
28 changes: 28 additions & 0 deletions src/pytest_matchers/pytest_matchers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,26 @@ def is_string(**kwargs) -> String:
return String(**kwargs)


def not_empty_string(**kwargs) -> String:
min_length = 1
if "min_length" in kwargs:
min_length = kwargs.pop("min_length")

return is_string(min_length=min_length, **kwargs)


def is_number(match_type: Type = None, **kwargs) -> Number:
return Number(match_type, **kwargs)


def is_int(**kwargs) -> Number:
return is_number(int, **kwargs)


def is_float(**kwargs) -> Number:
return is_number(float, **kwargs)


def one_of(*values: Matcher | Any) -> Or:
return Or(*values)

Expand Down Expand Up @@ -92,6 +108,18 @@ def is_datetime_string(
return DatetimeString(expected_format, min_value=min_value, max_value=max_value)


def is_iso_8601_date(**kwargs) -> DatetimeString:
return is_datetime_string("%Y-%m-%d", **kwargs)


def is_iso_8601_datetime(**kwargs) -> DatetimeString:
return is_datetime_string("%Y-%m-%dT%H:%M:%S", **kwargs)


def is_iso_8601_time(**kwargs) -> DatetimeString:
return is_datetime_string("%H:%M:%S", **kwargs)


def same_value() -> SameValue:
return SameValue()

Expand Down
2 changes: 1 addition & 1 deletion src/pytest_matchers/pytest_matchers/matchers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from .base import Matcher
from .matcher_factory import MatcherFactory
from .eq import Eq
from .length import Length
from .contains import Contains
from .starts_with import StartsWith
from .ends_with import EndsWith
from .eq import Eq
from .between import Between

from .and_matcher import And
Expand Down
18 changes: 18 additions & 0 deletions src/pytest_matchers/pytest_matchers/matchers/between.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,21 @@ def _max_repr(self):
return (
f"lower or equal than {self._max}" if self._max_inclusive else f"lower than {self._max}"
)


def between_matcher(
min_value: float | None,
max_value: float | None,
inclusive: bool | None,
min_inclusive: bool | None,
max_inclusive: bool | None,
) -> Between | None:
if min_value is None and max_value is None:
return None
return Between(
min_value,
max_value,
inclusive=inclusive,
min_inclusive=min_inclusive,
max_inclusive=max_inclusive,
)
42 changes: 37 additions & 5 deletions src/pytest_matchers/pytest_matchers/matchers/contains.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,54 @@

from pytest_matchers.matchers import Matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_detector import MatcherDetector
from pytest_matchers.utils.repr_utils import as_matcher_repr


def _in_string_matcher(contained_value: Matcher, value: str):
if contained_value == value:
return True
for i in range(len(value)):
for j in range(i + 1, len(value) + 1):
substring = value[i:j]
if contained_value == substring:
return True
return False


@matcher
class Contains(Matcher):
def __init__(self, contained_value: Any):
def __init__(self, *contained_values: Any):
super().__init__()
self._contained_value = contained_value
if len(contained_values) == 0:
raise ValueError("At least one value must be provided")
self._contained_values = contained_values

def matches(self, value: Any) -> bool:
try:
return self._contained_value in value
return all(
self._in(contained_value, value) for contained_value in self._contained_values
)
except TypeError:
return False

@staticmethod
def _in(contained_value: Any, value: Any) -> bool:
if MatcherDetector(contained_value).uses_matchers() and isinstance(value, str):
return _in_string_matcher(contained_value, value)
return contained_value in value

def __repr__(self) -> str:
return f"To contain {repr(self._contained_value)}"
return f"To contain {', '.join(map(repr, self._contained_values))}"

def concatenated_repr(self) -> str:
return f"containing {repr(self._contained_value)}"
return (
"containing something expected "
f"{', and something '.join(map(as_matcher_repr, self._contained_values))}"
)


def contains_matcher(contains: str | None) -> Contains | None:
if contains is None:
return None
return Contains(contains)
4 changes: 2 additions & 2 deletions src/pytest_matchers/pytest_matchers/matchers/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from pytest_matchers.matchers.has_attribute import has_attribute_matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_utils import (
between_matcher,
is_instance_matcher,
matches_or_none,
)
from pytest_matchers.matchers.is_instance import is_instance_matcher
from pytest_matchers.matchers.between import between_matcher
from pytest_matchers.utils.repr_utils import concat_reprs


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from pytest_matchers.matchers import String, Matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_utils import between_matcher, matches_or_none
from pytest_matchers.utils.matcher_utils import matches_or_none
from pytest_matchers.matchers.between import between_matcher
from pytest_matchers.utils.repr_utils import concat_reprs


Expand Down
27 changes: 25 additions & 2 deletions src/pytest_matchers/pytest_matchers/matchers/ends_with.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,32 @@

from pytest_matchers.matchers import Matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_detector import MatcherDetector
from pytest_matchers.utils.repr_utils import as_matcher_repr


def _ends_with_matcher(prefix: Matcher, value: Any):
if prefix == value: # With some luck the matcher matches with the whole value
return True
try:
for index in range(1, len(value)):
if prefix == value[-index:]:
return True
except TypeError:
pass
return False


@matcher
class EndsWith(Matcher):
def __init__(self, suffix: Sized | str):
def __init__(self, suffix: Sized | str | Matcher):
super().__init__()
self._suffix = suffix

def matches(self, value: Any) -> bool:
if MatcherDetector(self._suffix).uses_matchers():
return _ends_with_matcher(self._suffix, value)

if isinstance(value, str):
try:
return value.endswith(self._suffix)
Expand All @@ -26,4 +43,10 @@ def __repr__(self) -> str:
return f"To end with {repr(self._suffix)}"

def concatenated_repr(self) -> str:
return f"ending with {repr(self._suffix)}"
return f"with ending expected {as_matcher_repr(self._suffix)}"


def ends_with_matcher(ends_with: str | None) -> EndsWith | None:
if ends_with is None:
return None
return EndsWith(ends_with)
6 changes: 6 additions & 0 deletions src/pytest_matchers/pytest_matchers/matchers/is_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ def __repr__(self) -> str:

def concatenated_repr(self) -> str:
return f"of '{self._match_type.__name__}' instance"


def is_instance_matcher(match_type: type | None) -> IsInstance | None:
if match_type is None:
return None
return IsInstance(match_type)
10 changes: 10 additions & 0 deletions src/pytest_matchers/pytest_matchers/matchers/length.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,13 @@ def __repr__(self) -> str:
if length_repr:
return f"To have length {length_repr}"
return length_repr


def length_matcher(
length: int | None,
min_length: int | None,
max_length: int | None,
) -> Length | None:
if length is None and max_length is None and min_length is None:
return None
return Length(length, min_length, max_length)
4 changes: 2 additions & 2 deletions src/pytest_matchers/pytest_matchers/matchers/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from pytest_matchers.matchers import IsInstance, Matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_utils import (
is_instance_matcher,
length_matcher,
matches_or_none,
partial_matches_or_none,
)
from pytest_matchers.matchers.is_instance import is_instance_matcher
from pytest_matchers.matchers.length import length_matcher
from pytest_matchers.utils.repr_utils import concat_reprs


Expand Down
4 changes: 2 additions & 2 deletions src/pytest_matchers/pytest_matchers/matchers/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from pytest_matchers.matchers import Matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_utils import (
between_matcher,
is_instance_matcher,
matches_or_none,
)
from pytest_matchers.matchers.is_instance import is_instance_matcher
from pytest_matchers.matchers.between import between_matcher
from pytest_matchers.utils.repr_utils import concat_reprs


Expand Down
27 changes: 25 additions & 2 deletions src/pytest_matchers/pytest_matchers/matchers/starts_with.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,32 @@

from pytest_matchers.matchers import Matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_detector import MatcherDetector
from pytest_matchers.utils.repr_utils import as_matcher_repr


def _starts_with_matcher(prefix: Matcher, value: Any):
if prefix == value: # With some luck the matcher matches with the whole value
return True
try:
for index in range(1, len(value)):
if prefix == value[0:index]:
return True
except TypeError:
pass
return False


@matcher
class StartsWith(Matcher):
def __init__(self, prefix: Sized | str):
def __init__(self, prefix: Sized | str | Matcher):
super().__init__()
self._prefix = prefix

def matches(self, value: Any) -> bool:
if MatcherDetector(self._prefix).uses_matchers():
return _starts_with_matcher(self._prefix, value)

if isinstance(value, str):
try:
return value.startswith(self._prefix)
Expand All @@ -25,4 +42,10 @@ def __repr__(self) -> str:
return f"To start with {repr(self._prefix)}"

def concatenated_repr(self) -> str:
return f"starting with {repr(self._prefix)}"
return f"with start expected {as_matcher_repr(self._prefix)}"


def starts_with_matcher(starts_with: str | None) -> StartsWith | None:
if starts_with is None:
return None
return StartsWith(starts_with)
8 changes: 4 additions & 4 deletions src/pytest_matchers/pytest_matchers/matchers/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from pytest_matchers.matchers import IsInstance, Matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_utils import (
contains_matcher,
ends_with_matcher,
length_matcher,
matches_or_none,
starts_with_matcher,
)
from pytest_matchers.matchers.contains import contains_matcher
from pytest_matchers.matchers.length import length_matcher
from pytest_matchers.matchers.starts_with import starts_with_matcher
from pytest_matchers.matchers.ends_with import ends_with_matcher
from pytest_matchers.utils.repr_utils import concat_reprs


Expand Down
2 changes: 1 addition & 1 deletion src/pytest_matchers/pytest_matchers/matchers/uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.utils.matcher_utils import (
as_matcher_or_none,
is_instance_matcher,
matches_or_none,
)
from pytest_matchers.matchers.is_instance import is_instance_matcher
from pytest_matchers.utils.repr_utils import concat_matcher_repr, concat_reprs


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from pydantic.v1.fields import ModelField

from pytest_matchers.matchers import HasAttribute, Matcher
from pytest_matchers.utils.matcher_utils import is_instance_matcher
from pytest_matchers.matchers.matcher_factory import matcher
from pytest_matchers.matchers.is_instance import is_instance_matcher
from pytest_matchers.utils.repr_utils import concat_reprs


Expand Down Expand Up @@ -39,6 +40,7 @@ def _version(model_class: Type) -> str | None:
return None


@matcher
class PydanticModel(Matcher):
def __init__(
self,
Expand Down Expand Up @@ -95,7 +97,7 @@ def _attribute_matchers(self, value: Any = None) -> list[Matcher]:

def matches(self, value: Any) -> bool:
return self._matches_instance(value) and all(
matcher == value for matcher in self._attribute_matchers(value)
attr_matcher == value for attr_matcher in self._attribute_matchers(value)
)

def _matches_instance(self, value: Any) -> bool:
Expand Down
Loading

0 comments on commit 83f47a9

Please sign in to comment.