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

fix: Add end start and contains support for matchers. Fix import order. Add not_empty_string and extra main matchers #12

Merged
merged 1 commit into from
Aug 25, 2024
Merged
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
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
Loading