diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 801980d..0662f7f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -28,6 +28,7 @@ jobs: run: | python -m pip install --upgrade pip if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi + pip install -e . - name: Lint with pylint run: | make lint diff --git a/.pylintrc b/.pylintrc index 9791762..6a11c07 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,4 @@ [MESSAGES CONTROL] - disable= missing-function-docstring, missing-module-docstring, diff --git a/Makefile b/Makefile index 2190fb7..4af5673 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ spell: codespell tests: - pytest src/pytest_matchers/tests + pytest src/tests requirements: validate_env @make requirements_dev diff --git a/pyproject.toml b/pyproject.toml index c4f2e3d..5f86d2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,9 +24,13 @@ vulture = "^2.11" requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" +[project.entry-points.pytest11] +myproject = "pytest_matchers.plugin" + + [tool.coverage.run] branch = true -command_line = "-m pytest src/pytest_matchers/tests" +command_line = "-m pytest src/tests" [tool.coverage.report] show_missing = true diff --git a/scripts/python_folders.sh b/scripts/python_folders.sh index 2440ad3..087c955 100755 --- a/scripts/python_folders.sh +++ b/scripts/python_folders.sh @@ -1 +1 @@ -echo "src/pytest_matchers/pytest_matchers src/pytest_matchers/tests" +echo "src/pytest_matchers/pytest_matchers src/tests" diff --git a/src/pytest_matchers/tests/__init__.py b/src/__init__.py similarity index 100% rename from src/pytest_matchers/tests/__init__.py rename to src/__init__.py diff --git a/src/pytest_matchers/pytest_matchers/__init__.py b/src/pytest_matchers/pytest_matchers/__init__.py index 39ec36c..5f9bc9d 100644 --- a/src/pytest_matchers/pytest_matchers/__init__.py +++ b/src/pytest_matchers/pytest_matchers/__init__.py @@ -1,3 +1,4 @@ +from .asserts.asserts import assert_match, assert_not_match from .main import ( anything, between, diff --git a/src/pytest_matchers/tests/examples/__init__.py b/src/pytest_matchers/pytest_matchers/asserts/__init__.py similarity index 100% rename from src/pytest_matchers/tests/examples/__init__.py rename to src/pytest_matchers/pytest_matchers/asserts/__init__.py diff --git a/src/pytest_matchers/pytest_matchers/asserts/asserts.py b/src/pytest_matchers/pytest_matchers/asserts/asserts.py new file mode 100644 index 0000000..e7d3f22 --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/asserts/asserts.py @@ -0,0 +1,25 @@ +from typing import Any + +from pytest_matchers.asserts.comparer import Comparer + + +def _match(actual: Any, expected: Any) -> bool: + return Comparer().compare(actual, expected) + + +def assert_match(actual: Any, expected: Any) -> None: + try: + assert _match(actual, expected) + except AssertionError: + assert actual == expected # Return the original assertion error + + +def _not_match(actual: Any, expected: Any) -> bool: + return not _match(actual, expected) + + +def assert_not_match(actual: Any, expected: Any) -> None: + try: + assert _not_match(actual, expected) + except AssertionError: + assert actual != expected # Return the original assertion error diff --git a/src/pytest_matchers/pytest_matchers/asserts/comparer.py b/src/pytest_matchers/pytest_matchers/asserts/comparer.py new file mode 100644 index 0000000..b093d30 --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/asserts/comparer.py @@ -0,0 +1,22 @@ +from typing import Any + +from pytest_matchers.asserts.comparers.dict import DictComparer +from pytest_matchers.asserts.comparers.list import ListComparer +from pytest_matchers.asserts.comparers.set import SetComparer +from pytest_matchers.matchers import Matcher + + +class Comparer: + def compare(self, actual: Any, expected: Any, *, fail_fast: bool = True) -> bool: + if isinstance(expected, Matcher) and not isinstance(actual, Matcher): + return self.compare(expected, actual, fail_fast=fail_fast) # Reverse the order + return self._compare_by_type(actual, expected, fail_fast=fail_fast) + + def _compare_by_type(self, actual: Any, expected: Any, fail_fast: bool = True) -> bool: + if isinstance(expected, (list, tuple)): + return ListComparer(actual, expected, fail_fast=fail_fast).compare() + if isinstance(expected, dict): + return DictComparer(actual, expected, fail_fast=fail_fast).compare() + if isinstance(expected, set): + return SetComparer(actual, expected, fail_fast=fail_fast).compare() + return actual == expected diff --git a/src/pytest_matchers/tests/matchers/__init__.py b/src/pytest_matchers/pytest_matchers/asserts/comparers/__init__.py similarity index 100% rename from src/pytest_matchers/tests/matchers/__init__.py rename to src/pytest_matchers/pytest_matchers/asserts/comparers/__init__.py diff --git a/src/pytest_matchers/pytest_matchers/asserts/comparers/base.py b/src/pytest_matchers/pytest_matchers/asserts/comparers/base.py new file mode 100644 index 0000000..934d220 --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/asserts/comparers/base.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from typing import Any + + +class BaseComparer(ABC): + def __init__(self, actual: Any, expected: Any, *, fail_fast: bool = True): + self._actual = actual + self._expected = expected + self._fail_fast = fail_fast + + @abstractmethod + def compare(self) -> bool: + pass + + def _base_compare(self, actual: Any, expected: Any) -> bool: + # pylint: disable=import-outside-toplevel + from pytest_matchers.asserts.comparer import Comparer + + return Comparer().compare(actual, expected, fail_fast=self._fail_fast) diff --git a/src/pytest_matchers/pytest_matchers/asserts/comparers/dict.py b/src/pytest_matchers/pytest_matchers/asserts/comparers/dict.py new file mode 100644 index 0000000..7c44129 --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/asserts/comparers/dict.py @@ -0,0 +1,25 @@ +from typing import Generator + +from pytest_matchers.asserts.comparers.base import BaseComparer + + +class DictComparer(BaseComparer): + def compare(self) -> bool: + same_length = len(self._actual) == len(self._expected) + if not same_length and self._fail_fast: + return False + + return self._items_compare() and same_length + + def _items_compare(self) -> bool: + compared_items = self._compared_items() + if not self._fail_fast: + compared_items = list(compared_items) + return all(compared_items) + + def _compared_items(self) -> Generator: + for key in self._expected: + if key in self._actual: + yield self._base_compare(self._actual[key], self._expected[key]) + else: + yield False diff --git a/src/pytest_matchers/pytest_matchers/asserts/comparers/list.py b/src/pytest_matchers/pytest_matchers/asserts/comparers/list.py new file mode 100644 index 0000000..4e73189 --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/asserts/comparers/list.py @@ -0,0 +1,19 @@ +from pytest_matchers.asserts.comparers.base import BaseComparer + + +class ListComparer(BaseComparer): + def compare(self) -> bool: + same_length = len(self._actual) == len(self._expected) + if not same_length and self._fail_fast: + return False + + return self._items_compare() and same_length + + def _items_compare(self) -> bool: + compared_items = ( + self._base_compare(actual_item, expected_item) + for actual_item, expected_item in zip(self._actual, self._expected) + ) + if not self._fail_fast: + compared_items = list(compared_items) # Force full generation + return all(compared_items) diff --git a/src/pytest_matchers/pytest_matchers/asserts/comparers/set.py b/src/pytest_matchers/pytest_matchers/asserts/comparers/set.py new file mode 100644 index 0000000..99f0b81 --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/asserts/comparers/set.py @@ -0,0 +1,26 @@ +from typing import Any, Set + +from pytest_matchers.asserts.comparers.base import BaseComparer + + +class SetComparer(BaseComparer): + def compare(self) -> bool: + same_length = len(self._actual) == len(self._expected) + if not same_length and self._fail_fast: + return False + + return self._items_compare() and same_length + + def _items_compare(self) -> bool: + expected = self._expected.copy() + compared_items = (self._in(actual_value, expected) for actual_value in self._actual) + if not self._fail_fast: + compared_items = list(compared_items) + return all(compared_items) and not expected + + def _in(self, actual_value: Any, expected: Set[Any]) -> bool: + for expected_value in expected: + if self._base_compare(actual_value, expected_value): + expected.discard(expected_value) + return True + return False diff --git a/src/pytest_matchers/pytest_matchers/main.py b/src/pytest_matchers/pytest_matchers/main.py index df77a04..cc50785 100644 --- a/src/pytest_matchers/pytest_matchers/main.py +++ b/src/pytest_matchers/pytest_matchers/main.py @@ -11,9 +11,9 @@ HasAttribute, If, IsInstance, - IsList, - IsNumber, - IsString, + List, + Number, + String, JSON, Matcher, Or, @@ -31,18 +31,18 @@ def is_instance(match_type: Type) -> IsInstance: return IsInstance(match_type) -def is_list(match_type: Type = None, **kwargs) -> IsList | IsInstance: +def is_list(match_type: Type = None, **kwargs) -> List | IsInstance: if match_type is None and not kwargs: return is_instance(list) - return IsList(match_type, **kwargs) + return List(match_type, **kwargs) -def is_string(**kwargs) -> IsString: - return IsString(**kwargs) +def is_string(**kwargs) -> String: + return String(**kwargs) -def is_number(match_type: Type = None, **kwargs) -> IsNumber: - return IsNumber(match_type, **kwargs) +def is_number(match_type: Type = None, **kwargs) -> Number: + return Number(match_type, **kwargs) def one_of(*values: Matcher | Any) -> Or: diff --git a/src/pytest_matchers/pytest_matchers/matchers/__init__.py b/src/pytest_matchers/pytest_matchers/matchers/__init__.py index 68597a3..7400db8 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/__init__.py +++ b/src/pytest_matchers/pytest_matchers/matchers/__init__.py @@ -1,4 +1,5 @@ from .base import Matcher +from .matcher_factory import MatcherFactory from .length import Length from .contains import Contains from .starts_with import StartsWith @@ -8,11 +9,11 @@ from .and_matcher import And from .is_instance import IsInstance -from .is_list import IsList -from .is_string import IsString +from .list import List +from .string import String from .or_matcher import Or from .not_matcher import Not -from .is_number import IsNumber +from .number import Number from .anything import Anything from .datetime import Datetime from .datetime_string import DatetimeString diff --git a/src/pytest_matchers/pytest_matchers/matchers/and_matcher.py b/src/pytest_matchers/pytest_matchers/matchers/and_matcher.py index 741585e..933de82 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/and_matcher.py +++ b/src/pytest_matchers/pytest_matchers/matchers/and_matcher.py @@ -1,11 +1,14 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.repr_utils import concat_reprs +@matcher class And(Matcher): def __init__(self, *matchers: Matcher): + super().__init__() self._matchers = matchers def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/anything.py b/src/pytest_matchers/pytest_matchers/matchers/anything.py index 343790c..7e3854a 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/anything.py +++ b/src/pytest_matchers/pytest_matchers/matchers/anything.py @@ -1,8 +1,10 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher +@matcher class Anything(Matcher): def matches(self, _value: Any) -> bool: return True diff --git a/src/pytest_matchers/pytest_matchers/matchers/base.py b/src/pytest_matchers/pytest_matchers/matchers/base.py index 06427c8..aa944c3 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/base.py +++ b/src/pytest_matchers/pytest_matchers/matchers/base.py @@ -4,8 +4,15 @@ class Matcher(ABC): + def __init__(self): + self._compared_values = [] + self._compared_values_index = 0 + def __eq__(self, other: Any) -> bool: - return self.matches(other) + result = self.matches(other) + if result: + self._compared_values.append(other) + return result def __and__(self, other: "Matcher") -> "Matcher": from pytest_matchers.matchers import And @@ -22,6 +29,9 @@ def __invert__(self) -> "Matcher": return Not(self) + def __hash__(self): + return hash(id(self)) + @abstractmethod def matches(self, value: Any) -> bool: pass @@ -30,3 +40,12 @@ def concatenated_repr(self) -> str: from pytest_matchers.utils.repr_utils import non_capitalized return non_capitalized(repr(self)) + + def next_compared_value_repr(self) -> str: + try: + compare_value = self._compared_values[self._compared_values_index] + self._compared_values_index += 1 + value = compare_value + return f"{value!r}" + except IndexError: + return repr(self) diff --git a/src/pytest_matchers/pytest_matchers/matchers/between.py b/src/pytest_matchers/pytest_matchers/matchers/between.py index 66606e2..b4ea659 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/between.py +++ b/src/pytest_matchers/pytest_matchers/matchers/between.py @@ -1,9 +1,11 @@ from typing import Any from pytest_matchers.matchers import Eq, Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.repr_utils import concat_reprs, non_capitalized +@matcher class Between(Matcher): def __init__( self, @@ -13,6 +15,7 @@ def __init__( min_inclusive: bool = None, max_inclusive: bool = None, ): + super().__init__() if min_value is None and max_value is None: raise ValueError("At least one of min or max must be specified") self._min = min_value diff --git a/src/pytest_matchers/pytest_matchers/matchers/case.py b/src/pytest_matchers/pytest_matchers/matchers/case.py index 4636855..836df7a 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/case.py +++ b/src/pytest_matchers/pytest_matchers/matchers/case.py @@ -1,9 +1,11 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.matcher_utils import as_matcher, as_matcher_or_none +@matcher class Case(Matcher): def __init__( self, @@ -11,6 +13,7 @@ def __init__( expectations: dict[Any, Matcher | Any], default_expectation: Matcher | Any | None = None, ): + super().__init__() self._case_value = case_value self._expectations = expectations self._default_expectation = ( diff --git a/src/pytest_matchers/pytest_matchers/matchers/contains.py b/src/pytest_matchers/pytest_matchers/matchers/contains.py index 710a9da..2951c82 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/contains.py +++ b/src/pytest_matchers/pytest_matchers/matchers/contains.py @@ -1,10 +1,13 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher +@matcher class Contains(Matcher): def __init__(self, contained_value: Any): + super().__init__() self._contained_value = contained_value def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/datetime.py b/src/pytest_matchers/pytest_matchers/matchers/datetime.py index a7f0c6f..7c9db75 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/datetime.py +++ b/src/pytest_matchers/pytest_matchers/matchers/datetime.py @@ -2,6 +2,7 @@ from pytest_matchers.matchers import Matcher 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, @@ -10,6 +11,7 @@ from pytest_matchers.utils.repr_utils import concat_reprs +@matcher class Datetime(Matcher): # pylint: disable=too-many-instance-attributes def __init__( self, @@ -22,6 +24,7 @@ def __init__( minute: int = None, second: int = None, ): + super().__init__() self._is_instance_matcher = is_instance_matcher(datetime) self._limit_matcher = between_matcher(min_value, max_value, None, None, None) self._year_matcher = has_attribute_matcher("year", year, value_needed=True) diff --git a/src/pytest_matchers/pytest_matchers/matchers/datetime_string.py b/src/pytest_matchers/pytest_matchers/matchers/datetime_string.py index 260dbd5..8ffbf9d 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/datetime_string.py +++ b/src/pytest_matchers/pytest_matchers/matchers/datetime_string.py @@ -1,10 +1,12 @@ from datetime import datetime -from pytest_matchers.matchers import IsString, Matcher +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.repr_utils import concat_reprs +@matcher class DatetimeString(Matcher): def __init__( self, @@ -13,6 +15,7 @@ def __init__( min_value: datetime | str = None, max_value: datetime | str = None, ): + super().__init__() self._expected_format = expected_format self._between_matcher = between_matcher( self._as_datetime(min_value, "min_value"), @@ -38,7 +41,7 @@ def _as_datetime(self, value: datetime | str, value_name: str) -> datetime | Non return None def matches(self, value: str) -> bool: - return IsString() == value and self._matches_format_and_limit(value) + return String() == value and self._matches_format_and_limit(value) def _matches_format_and_limit(self, value: str) -> bool: try: diff --git a/src/pytest_matchers/pytest_matchers/matchers/dict.py b/src/pytest_matchers/pytest_matchers/matchers/dict.py index 1a1fa5c..5b8fbf3 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/dict.py +++ b/src/pytest_matchers/pytest_matchers/matchers/dict.py @@ -1,12 +1,15 @@ from typing import Any from pytest_matchers.matchers import IsInstance, Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.matcher_utils import as_matcher from pytest_matchers.utils.repr_utils import concat_reprs, non_capitalized +@matcher class Dict(Matcher): def __init__(self, matching: dict = None, *, exclude: list = None): + super().__init__() self._is_instance_matcher = IsInstance(dict) self._matching = matching or {} self._exclude = exclude or [] @@ -19,8 +22,8 @@ def matches(self, value: Any) -> bool: ) def _matches_values(self, value: dict) -> bool: - for key, matcher in self._matching.items(): - if key not in value or not matcher == value.get(key): + for key, expect in self._matching.items(): + if key not in value or not expect == value.get(key): return False return True diff --git a/src/pytest_matchers/pytest_matchers/matchers/different_value.py b/src/pytest_matchers/pytest_matchers/matchers/different_value.py index dc74da3..1aaac1d 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/different_value.py +++ b/src/pytest_matchers/pytest_matchers/matchers/different_value.py @@ -5,6 +5,7 @@ class DifferentValue(Matcher): def __init__(self): + super().__init__() self._matched_value = None def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/ends_with.py b/src/pytest_matchers/pytest_matchers/matchers/ends_with.py index 76f33e0..be511eb 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/ends_with.py +++ b/src/pytest_matchers/pytest_matchers/matchers/ends_with.py @@ -1,10 +1,13 @@ from typing import Any, Sized from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher +@matcher class EndsWith(Matcher): def __init__(self, suffix: Sized | str): + super().__init__() self._suffix = suffix def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/eq.py b/src/pytest_matchers/pytest_matchers/matchers/eq.py index 19ca528..f2f7883 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/eq.py +++ b/src/pytest_matchers/pytest_matchers/matchers/eq.py @@ -1,12 +1,15 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher +@matcher class Eq(Matcher): """Why would you want to use this, be serious""" def __init__(self, match_value: Any): + super().__init__() self._match_value = match_value def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/has_attribute.py b/src/pytest_matchers/pytest_matchers/matchers/has_attribute.py index 3029b1b..d4ac2d6 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/has_attribute.py +++ b/src/pytest_matchers/pytest_matchers/matchers/has_attribute.py @@ -1,11 +1,14 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.matcher_utils import as_matcher, matches_or_none +@matcher class HasAttribute(Matcher): def __init__(self, attribute_name: str, expected_value: Any = None): + super().__init__() self._attribute_name = attribute_name self._expected_value = expected_value self._expected_value_matcher = as_matcher(expected_value) if expected_value else None diff --git a/src/pytest_matchers/pytest_matchers/matchers/if_matcher.py b/src/pytest_matchers/pytest_matchers/matchers/if_matcher.py index 92b1232..643ad20 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/if_matcher.py +++ b/src/pytest_matchers/pytest_matchers/matchers/if_matcher.py @@ -1,11 +1,13 @@ from typing import Any, Callable from pytest_matchers.matchers import Anything, Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.matcher_utils import as_matcher from pytest_matchers.utils.repr_utils import capitalized from pytest_matchers.utils.warn import warn +@matcher class If(Matcher): def __init__( self, @@ -13,15 +15,16 @@ def __init__( then: Matcher | Any = None, or_else: Matcher | Any = None, ): + super().__init__() self._condition = condition self._then = as_matcher(then or Anything()) self._or_else = as_matcher(or_else or Anything()) def matches(self, value: Any) -> bool: - matcher = self._or_else + expect = self._or_else if self._satisfies_condition(value): - matcher = self._then - return matcher == value + expect = self._then + return expect == value def _satisfies_condition(self, value: Any) -> bool: if isinstance(self._condition, bool): diff --git a/src/pytest_matchers/pytest_matchers/matchers/is_instance.py b/src/pytest_matchers/pytest_matchers/matchers/is_instance.py index 30761ce..32e5f5e 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/is_instance.py +++ b/src/pytest_matchers/pytest_matchers/matchers/is_instance.py @@ -1,10 +1,13 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher +@matcher class IsInstance(Matcher): def __init__(self, match_type): + super().__init__() self._match_type = match_type def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/json.py b/src/pytest_matchers/pytest_matchers/matchers/json.py index 53a4906..a7f7f2d 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/json.py +++ b/src/pytest_matchers/pytest_matchers/matchers/json.py @@ -3,12 +3,15 @@ from pytest_matchers.matchers import IsInstance, Matcher from pytest_matchers.matchers.dict import dict_matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.matcher_utils import matches_or_none from pytest_matchers.utils.repr_utils import concat_reprs +@matcher class JSON(Matcher): def __init__(self, matching: dict = None, *, exclude: list = None): + super().__init__() self._is_instance_matcher = IsInstance(str) self._dict_matcher = dict_matcher(matching, exclude) diff --git a/src/pytest_matchers/pytest_matchers/matchers/length.py b/src/pytest_matchers/pytest_matchers/matchers/length.py index f30798e..bbfa90d 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/length.py +++ b/src/pytest_matchers/pytest_matchers/matchers/length.py @@ -1,10 +1,13 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher +@matcher class Length(Matcher): def __init__(self, length=None, min_length=None, max_length=None): + super().__init__() if length: if min_length or max_length: raise ValueError("Cannot specify length with min_length or max_length") diff --git a/src/pytest_matchers/pytest_matchers/matchers/is_list.py b/src/pytest_matchers/pytest_matchers/matchers/list.py similarity index 90% rename from src/pytest_matchers/pytest_matchers/matchers/is_list.py rename to src/pytest_matchers/pytest_matchers/matchers/list.py index b5b2d7c..58123ba 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/is_list.py +++ b/src/pytest_matchers/pytest_matchers/matchers/list.py @@ -1,6 +1,7 @@ from typing import Any, Callable, Type 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, @@ -10,7 +11,8 @@ from pytest_matchers.utils.repr_utils import concat_reprs -class IsList(Matcher): +@matcher +class List(Matcher): def __init__( self, match_type: Type = None, @@ -19,6 +21,7 @@ def __init__( min_length: int = None, max_length: int = None, ): + super().__init__() self._is_instance_matcher = is_instance_matcher(match_type) self._length_matcher = length_matcher(length, min_length, max_length) diff --git a/src/pytest_matchers/pytest_matchers/matchers/matcher_factory.py b/src/pytest_matchers/pytest_matchers/matchers/matcher_factory.py new file mode 100644 index 0000000..308288c --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/matchers/matcher_factory.py @@ -0,0 +1,37 @@ +from typing import IO + +from _pytest._io.pprint import PrettyPrinter + +from pytest_matchers.matchers import Matcher + + +def _print_matcher( + _printer: PrettyPrinter, + matcher_instance: Matcher, + stream: IO[str], + _indent: int, + _allowance: int, + _context: set[int], + _level: int, +): + stream.write(matcher_instance.next_compared_value_repr()) + + +class MatcherFactory: + matchers = {} + + @classmethod + def register(cls, matcher_class: Matcher): + PrettyPrinter._dispatch[matcher_class.__repr__] = ( # pylint: disable=protected-access + _print_matcher + ) + cls.matchers[matcher_class.__name__] = matcher_class + + @classmethod + def get(cls, name): + return cls.matchers[name] + + +def matcher(matcher_class): + MatcherFactory.register(matcher_class) + return matcher_class diff --git a/src/pytest_matchers/pytest_matchers/matchers/not_matcher.py b/src/pytest_matchers/pytest_matchers/matchers/not_matcher.py index 3271835..7847efc 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/not_matcher.py +++ b/src/pytest_matchers/pytest_matchers/matchers/not_matcher.py @@ -1,23 +1,26 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.matcher_utils import as_matcher +@matcher class Not(Matcher): - def __init__(self, matcher: Matcher | Any): - self._matcher = as_matcher(matcher) + def __init__(self, value: Matcher | Any): + super().__init__() + self._value = as_matcher(value) - def __new__(cls, matcher): - if isinstance(matcher, Not): - return matcher._matcher + def __new__(cls, value: Matcher | Any): + if isinstance(value, Not): + return value._value return super().__new__(cls) def __invert__(self) -> Matcher: - return self._matcher + return self._value - def matches(self, value) -> bool: - return not self._matcher == value + def matches(self, value: Any) -> bool: + return not self._value == value def __repr__(self) -> str: - return f"To not be {self._matcher.concatenated_repr()}" + return f"To not be {self._value.concatenated_repr()}" diff --git a/src/pytest_matchers/pytest_matchers/matchers/is_number.py b/src/pytest_matchers/pytest_matchers/matchers/number.py similarity index 92% rename from src/pytest_matchers/pytest_matchers/matchers/is_number.py rename to src/pytest_matchers/pytest_matchers/matchers/number.py index 93afd5a..8603d73 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/is_number.py +++ b/src/pytest_matchers/pytest_matchers/matchers/number.py @@ -1,6 +1,7 @@ from typing import Any, Type 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, @@ -9,7 +10,8 @@ from pytest_matchers.utils.repr_utils import concat_reprs -class IsNumber(Matcher): +@matcher +class Number(Matcher): def __init__( self, match_type: Type = None, @@ -20,6 +22,7 @@ def __init__( min_inclusive: bool = None, max_inclusive: bool = None, ): + super().__init__() self._is_instance_matcher = is_instance_matcher(match_type) self._between_matcher = between_matcher( min_value, diff --git a/src/pytest_matchers/pytest_matchers/matchers/or_matcher.py b/src/pytest_matchers/pytest_matchers/matchers/or_matcher.py index bb893e2..e8cb7b1 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/or_matcher.py +++ b/src/pytest_matchers/pytest_matchers/matchers/or_matcher.py @@ -1,11 +1,14 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.repr_utils import concat_reprs +@matcher class Or(Matcher): def __init__(self, *matchers: Matcher | Any): + super().__init__() self._matchers = matchers def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/same_value.py b/src/pytest_matchers/pytest_matchers/matchers/same_value.py index 796bf69..98d042c 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/same_value.py +++ b/src/pytest_matchers/pytest_matchers/matchers/same_value.py @@ -1,10 +1,13 @@ from typing import Any from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher +@matcher class SameValue(Matcher): def __init__(self): + super().__init__() self._matched_value = None def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/starts_with.py b/src/pytest_matchers/pytest_matchers/matchers/starts_with.py index 29a456f..6de51cc 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/starts_with.py +++ b/src/pytest_matchers/pytest_matchers/matchers/starts_with.py @@ -1,10 +1,13 @@ from typing import Any, Sized from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher +@matcher class StartsWith(Matcher): def __init__(self, prefix: Sized | str): + super().__init__() self._prefix = prefix def matches(self, value: Any) -> bool: diff --git a/src/pytest_matchers/pytest_matchers/matchers/strict_dict.py b/src/pytest_matchers/pytest_matchers/matchers/strict_dict.py index 1aeafa7..f9e2383 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/strict_dict.py +++ b/src/pytest_matchers/pytest_matchers/matchers/strict_dict.py @@ -1,10 +1,12 @@ from typing import Any from pytest_matchers.matchers import IsInstance, Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.matcher_utils import as_matcher from pytest_matchers.utils.repr_utils import concat_reprs, non_capitalized +@matcher class StrictDict(Matcher): def __init__( self, @@ -14,6 +16,7 @@ def __init__( when_true: dict = None, when_false: dict = None, ): + super().__init__() self._is_instance_matcher = IsInstance(dict) self._matching = matching if (when_true or when_false) and extra_condition is None: diff --git a/src/pytest_matchers/pytest_matchers/matchers/is_string.py b/src/pytest_matchers/pytest_matchers/matchers/string.py similarity index 92% rename from src/pytest_matchers/pytest_matchers/matchers/is_string.py rename to src/pytest_matchers/pytest_matchers/matchers/string.py index 1f3e4c1..a4462ce 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/is_string.py +++ b/src/pytest_matchers/pytest_matchers/matchers/string.py @@ -1,6 +1,7 @@ from typing import Any 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, @@ -11,7 +12,8 @@ from pytest_matchers.utils.repr_utils import concat_reprs -class IsString(Matcher): +@matcher +class String(Matcher): def __init__( self, *, @@ -22,6 +24,7 @@ def __init__( max_length: int = None, min_length: int = None, ): + super().__init__() self._starts_with_matcher = starts_with_matcher(starts_with) self._ends_with_matcher = ends_with_matcher(ends_with) self._contains_matcher = contains_matcher(contains) diff --git a/src/pytest_matchers/pytest_matchers/matchers/uuid.py b/src/pytest_matchers/pytest_matchers/matchers/uuid.py index 0dd1d64..fe1c14a 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/uuid.py +++ b/src/pytest_matchers/pytest_matchers/matchers/uuid.py @@ -2,6 +2,7 @@ from typing import Any, Type from pytest_matchers.matchers import Matcher +from pytest_matchers.matchers.matcher_factory import matcher from pytest_matchers.utils.matcher_utils import ( as_matcher_or_none, is_instance_matcher, @@ -10,8 +11,10 @@ from pytest_matchers.utils.repr_utils import concat_matcher_repr, concat_reprs +@matcher class UUID(Matcher): def __init__(self, matching_type: Type = None, *, version: int | Matcher = None): + super().__init__() self._is_instance_matcher = is_instance_matcher(matching_type) self._version_matcher = as_matcher_or_none(version) diff --git a/src/pytest_matchers/pytest_matchers/plugin.py b/src/pytest_matchers/pytest_matchers/plugin.py new file mode 100644 index 0000000..5b56e30 --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/plugin.py @@ -0,0 +1,33 @@ +from typing import Any + +import pytest + +from pytest_matchers.matchers import Matcher +from pytest_matchers.utils.matcher_detector import MatcherDetector + + +def _matches_operation(operation, actual: Any, expected: Matcher) -> bool: + if operation == "==": + return expected == actual + if operation == "!=": + return expected != actual + return False # pragma: no cover + + +@pytest.hookimpl() +def pytest_assertrepr_compare(config, op: str, left: Any, right: Any): + del config + if ( + MatcherDetector(left).uses_matchers() + or MatcherDetector(right).uses_matchers() + and _matches_operation(op, left, right) + ): + return [ + f"{left!r} {op} {right!r}", + "", + "WARNING! " + "Comparison failed because the left object redefines the equality operator.", + "Consider using the 'assert_match' or 'assert_not_match' functions instead.", + "assert_match(actual, expected)", + ] + return None diff --git a/src/pytest_matchers/pytest_matchers/utils/matcher_detector.py b/src/pytest_matchers/pytest_matchers/utils/matcher_detector.py new file mode 100644 index 0000000..1b7aad8 --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/utils/matcher_detector.py @@ -0,0 +1,18 @@ +from typing import Any + +from pytest_matchers.matchers import Matcher + + +class MatcherDetector: + def __init__(self, value: Any): + self._value = value + + def uses_matchers(self) -> bool: + return isinstance(self._value, Matcher) or self._iterable_with_matchers() + + def _iterable_with_matchers(self) -> bool: + if isinstance(self._value, dict): + return any(MatcherDetector(value).uses_matchers() for value in self._value.values()) + if isinstance(self._value, (list, tuple, set)): + return any(MatcherDetector(value).uses_matchers() for value in self._value) + return False diff --git a/src/pytest_matchers/tests/conftest.py b/src/pytest_matchers/tests/conftest.py deleted file mode 100644 index 6a9062e..0000000 --- a/src/pytest_matchers/tests/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -import os - -os.environ["PYTEST_MATCHERS_WARNINGS"] = "False" diff --git a/src/pytest_matchers/tests/utils/__init__.py b/src/tests/__init__.py similarity index 100% rename from src/pytest_matchers/tests/utils/__init__.py rename to src/tests/__init__.py diff --git a/src/tests/asserts/__init__.py b/src/tests/asserts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/asserts/comparers/__init__.py b/src/tests/asserts/comparers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/asserts/comparers/test_dict.py b/src/tests/asserts/comparers/test_dict.py new file mode 100644 index 0000000..2bc7db7 --- /dev/null +++ b/src/tests/asserts/comparers/test_dict.py @@ -0,0 +1,48 @@ +from unittest.mock import MagicMock + +from pytest_matchers import is_number +from pytest_matchers.asserts.comparers.dict import DictComparer + + +def test_compare(): + comparer = DictComparer({"a": 1, "b": 2}, {"a": 1, "b": 2}) + assert comparer.compare() + comparer = DictComparer({"a": 1, "b": 2}, {"a": 1, "b": 3}) + assert not comparer.compare() + comparer = DictComparer({"a": 1, "b": 2}, {"a": 1}) + assert not comparer.compare() + comparer = DictComparer({"a": 1, "b": 2}, {"a": 1, "b": is_number()}) + assert comparer.compare() + comparer = DictComparer({"a": 1, "b": 2}, {"b": 2, "a": 1}) + assert comparer.compare() + + +def test_compare_fail_fast(): + mock = MagicMock() + comparer = DictComparer({"a": 1, "b": 3, "c": mock}, {"a": 1, "b": 2, "c": 3}) + assert not comparer.compare() + mock.__eq__.assert_not_called() # pylint: disable=no-member + comparer = DictComparer({"a": 1, "b": 2, "c": mock}, {"a": 1, "b": 2, "c": 3}) + assert not comparer.compare() + mock.__eq__.assert_called_once_with(3) # pylint: disable=no-member + + +def test_compare_fail_fast_false(): + mock = MagicMock() + comparer = DictComparer({"a": 1, "b": 3, "c": mock}, {"a": 1, "b": 2, "c": 3}, fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_called_once_with(3) # pylint: disable=no-member + mock = MagicMock() + comparer = DictComparer({"a": 1, "b": 2, "c": mock}, {"a": 1, "b": 2, "c": 3}, fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_called_once_with(3) # pylint: disable=no-member + + +def test_compare_fail_fast_false_different_length(): + mock = MagicMock() + comparer = DictComparer({"a": 1, "b": 2, "c": mock}, {"a": 1, "b": 2}, fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_not_called() # pylint: disable=no-member + comparer = DictComparer({"a": 1, "b": 2}, {"a": 1, "b": 2, "c": mock}, fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_not_called() # pylint: disable=no-member diff --git a/src/tests/asserts/comparers/test_list.py b/src/tests/asserts/comparers/test_list.py new file mode 100644 index 0000000..fe2947b --- /dev/null +++ b/src/tests/asserts/comparers/test_list.py @@ -0,0 +1,48 @@ +from unittest.mock import MagicMock + +from pytest_matchers import is_number +from pytest_matchers.asserts.comparers.list import ListComparer + + +def test_compare(): + comparer = ListComparer([1, 2, 3], [1, 2, 3]) + assert comparer.compare() + comparer = ListComparer([1, 2, 3], [1, 2, 4]) + assert not comparer.compare() + comparer = ListComparer([1, 2, 3], [1, 2]) + assert not comparer.compare() + comparer = ListComparer([1, 2], [1, is_number()]) + assert comparer.compare() + comparer = ListComparer([1, 2], [2, 1]) + assert not comparer.compare() + + +def test_compare_fail_fast(): + mock = MagicMock() + comparer = ListComparer([1, 2, 2, mock], [1, 2, 3, 4]) + assert not comparer.compare() + mock.__eq__.assert_not_called() # pylint: disable=no-member + comparer = ListComparer([1, 2, 3, mock], [1, 2, 3, 4]) + assert not comparer.compare() + mock.__eq__.assert_called_once_with(4) # pylint: disable=no-member + + +def test_compare_fail_fast_false(): + mock = MagicMock() + comparer = ListComparer([1, 2, 2, mock], [1, 2, 3, 4], fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_called_once_with(4) # pylint: disable=no-member + mock = MagicMock() + comparer = ListComparer([1, 2, 3, mock], [1, 2, 3, 4], fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_called_once_with(4) # pylint: disable=no-member + + +def test_compare_fail_fast_false_different_length(): + mock = MagicMock() + comparer = ListComparer([1, 2, mock], [1, 2], fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_not_called() # pylint: disable=no-member + comparer = ListComparer([1, 2], [1, 2, mock], fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_not_called() # pylint: disable=no-member diff --git a/src/tests/asserts/comparers/test_set.py b/src/tests/asserts/comparers/test_set.py new file mode 100644 index 0000000..c5cdb1e --- /dev/null +++ b/src/tests/asserts/comparers/test_set.py @@ -0,0 +1,82 @@ +from unittest.mock import MagicMock + +from pytest_matchers import is_number, is_string, one_of +from pytest_matchers.asserts.comparers.set import SetComparer + + +def test_compare(): + comparer = SetComparer({1, 2, 3}, {1, 2, 3}) + assert comparer.compare() + comparer = SetComparer({1, 2, 3}, {1, 2, 4}) + assert not comparer.compare() + comparer = SetComparer({1, 2, 3}, {1, 2}) + assert not comparer.compare() + comparer = SetComparer({1, 2}, {2, 1}) + assert comparer.compare() + + +def test_compare_with_matchers(): + comparer = SetComparer({1, 2, 3}, {3, 2, is_number()}) + assert comparer.compare() + comparer = SetComparer({1, 2, 3}, {is_number(min_value=2), 3}) + assert not comparer.compare() + comparer = SetComparer({1, 2, 3}, {is_number(min_value=2), is_number(min_value=2), 3}) + assert not comparer.compare() + comparer = SetComparer({1, 2, 3}, {is_number(min_value=2), is_number(min_value=1), 3}) + assert comparer.compare() + comparer = SetComparer( + {"abc", "acb", "feeg"}, {is_string(), is_string(starts_with="ac"), "feeg"} + ) + assert comparer.compare() == one_of(True, False) # It's a limitation of the set comparison + + +def test_compare_fail_fast(): + mock = MagicMock() + comparer = SetComparer({1, 2, mock}, {1, 3, 4}) + assert not comparer.compare() + try: + mock.__eq__.assert_not_called() # pylint: disable=no-member + except AssertionError: # pragma: no cover + # Sometimes the mock is the first or second object called, + # in that case it expects to be compared with at least two values + assert mock.__eq__.call_count == one_of(2, 3) # pylint: disable=no-member + comparer = SetComparer({1, 2, 3, mock}, {1, 2, 3, 4}) + assert not comparer.compare() + try: + mock.__eq__.assert_called_once_with(4) # pylint: disable=no-member + except AssertionError: # pragma: no cover + # Same case here, the mock is the first or second object called + assert mock.__eq__.call_count == is_number( # pylint: disable=no-member + int, + min_value=3, + max_value=7, + ) + + +def test_compare_fail_fast_false(): + mock = MagicMock() + comparer = SetComparer({1, 2, 5, mock}, {1, 2, 3, 4}, fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_called_with(4) # pylint: disable=no-member + mock = MagicMock() + comparer = SetComparer({1, 2, 3, mock}, {1, 2, 3, 4}, fail_fast=False) + assert not comparer.compare() + mock.__eq__.assert_called_with(4) # pylint: disable=no-member + + +def test_compare_fail_fast_false_different_length(): + mock = MagicMock() + comparer = SetComparer({1, 2, mock}, {1, 2}, fail_fast=False) + assert not comparer.compare() + try: + mock.__eq__.assert_not_called() # pylint: disable=no-member + except AssertionError: # pragma: no cover + # Sometimes the mock is the first or second object called + assert mock.__eq__.call_count == one_of(1, 2) # pylint: disable=no-member + mock = MagicMock() + comparer = SetComparer({1, 2}, {1, 2, mock}, fail_fast=False) + assert not comparer.compare() + try: + mock.__eq__.assert_not_called() # pylint: disable=no-member + except AssertionError: # pragma: no cover + assert mock.__eq__.call_count == one_of(1, 2) # pylint: disable=no-member diff --git a/src/tests/asserts/test_asserts.py b/src/tests/asserts/test_asserts.py new file mode 100644 index 0000000..95dabec --- /dev/null +++ b/src/tests/asserts/test_asserts.py @@ -0,0 +1,71 @@ +from typing import Any + +import pytest + +from pytest_matchers import assert_match, assert_not_match, is_instance +from src.tests.conftest import CustomEqual + + +def _assert_match_error(actual: Any, expected: Any): + with pytest.raises(AssertionError): + assert_match(actual, expected) + + +def _assert_not_match_error(actual: Any, expected: Any): + with pytest.raises(AssertionError): + assert_not_match(actual, expected) + + +def test_match_eq(): + assert not CustomEqual(3) == is_instance(CustomEqual) # pylint: disable=unnecessary-negation + assert not CustomEqual(3) != is_instance(CustomEqual) # pylint: disable=unnecessary-negation + assert_match(CustomEqual(3), is_instance(CustomEqual)) + assert_match(is_instance(CustomEqual), CustomEqual(3)) + _assert_match_error(CustomEqual(3), is_instance(str)) + _assert_match_error(is_instance(str), CustomEqual(3)) + + +def test_not_match_eq(): + assert not CustomEqual(3) == is_instance(str) # pylint: disable=unnecessary-negation + assert not CustomEqual(3) != is_instance(str) # pylint: disable=unnecessary-negation + assert_not_match(CustomEqual(3), is_instance(str)) + assert_not_match(is_instance(str), CustomEqual(3)) + _assert_not_match_error(CustomEqual(3), is_instance(CustomEqual)) + _assert_not_match_error(is_instance(CustomEqual), CustomEqual(3)) + + +def test_match_lists(): + assert_match([CustomEqual(3), 3], [is_instance(CustomEqual), 3]) + assert_not_match([CustomEqual(3), 3], [3, is_instance(CustomEqual)]) + assert_not_match([CustomEqual(3), 3], [is_instance(str), 3]) + assert_not_match([CustomEqual(3), 3], [is_instance(CustomEqual), 4]) + assert_not_match([CustomEqual(3), 3], [is_instance(CustomEqual), 3, 4]) + + +def test_match_sets(): + assert_match({CustomEqual(3), 3}, {is_instance(CustomEqual), 3}) + assert_match({CustomEqual(3), 3}, {3, is_instance(CustomEqual)}) + assert_not_match({CustomEqual(3), 3}, {is_instance(str), 3}) + assert_not_match({CustomEqual(3), 3}, {is_instance(CustomEqual), 4}) + assert_not_match({CustomEqual(3), 3}, {3, is_instance(CustomEqual), 4}) + + +def test_match_tuples(): + assert_match((CustomEqual(3), 3), (is_instance(CustomEqual), 3)) + assert_not_match((CustomEqual(3), 3), (3, is_instance(CustomEqual))) + assert_not_match((CustomEqual(3), 3), (is_instance(str), 3)) + assert_not_match((CustomEqual(3), 3), (is_instance(CustomEqual), 4)) + assert_not_match((CustomEqual(3), 3), (is_instance(CustomEqual), 3, 4)) + + +def test_match_dictionaries(): + assert_match({"a": CustomEqual(3), "b": 3}, {"a": is_instance(CustomEqual), "b": 3}) + assert_not_match({"a": CustomEqual(3), "b": 3}, {"a": is_instance(str), "b": 3}) + assert_match( + {"a": CustomEqual(3), "b": [1, 2, CustomEqual(4)]}, + {"a": is_instance(CustomEqual), "b": [1, 2, is_instance(CustomEqual)]}, + ) + assert_match( + {"a": {"x": CustomEqual(3), "y": 3}, "b": 3}, + {"a": {"x": is_instance(CustomEqual), "y": 3}, "b": 3}, + ) diff --git a/src/tests/conftest.py b/src/tests/conftest.py new file mode 100644 index 0000000..58589ec --- /dev/null +++ b/src/tests/conftest.py @@ -0,0 +1,23 @@ +import os + +from pytest_matchers import plugin + +os.environ["PYTEST_MATCHERS_WARNINGS"] = "False" + + +def pytest_configure(config): + config.pluginmanager.register(plugin) + + +class CustomEqual: + def __init__(self, value): + self.value = value + + def __eq__(self, other): + return isinstance(other, CustomEqual) and self.value == other.value + + def __ne__(self, other): + return isinstance(other, CustomEqual) and self.value != other.value + + def __hash__(self): + return hash(self.value) diff --git a/src/tests/examples/__init__.py b/src/tests/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pytest_matchers/tests/examples/test_example.py b/src/tests/examples/test_example.py similarity index 94% rename from src/pytest_matchers/tests/examples/test_example.py rename to src/tests/examples/test_example.py index f2a9fbe..86894c3 100644 --- a/src/pytest_matchers/tests/examples/test_example.py +++ b/src/tests/examples/test_example.py @@ -6,6 +6,7 @@ from pytest_matchers import ( anything, + assert_match, between, case, different_value, @@ -24,6 +25,13 @@ one_of, same_value, ) +from src.tests.conftest import CustomEqual + + +def test_deal_with_custom_equals(): + assert not CustomEqual(3) == is_instance(CustomEqual) # pylint: disable=unnecessary-negation + assert is_instance(CustomEqual) == CustomEqual(3) + assert_match(CustomEqual(3), is_instance(CustomEqual)) def test_random_number(): diff --git a/src/tests/matchers/__init__.py b/src/tests/matchers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pytest_matchers/tests/matchers/test_and.py b/src/tests/matchers/test_and.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_and.py rename to src/tests/matchers/test_and.py diff --git a/src/pytest_matchers/tests/matchers/test_anything.py b/src/tests/matchers/test_anything.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_anything.py rename to src/tests/matchers/test_anything.py diff --git a/src/pytest_matchers/tests/matchers/test_between.py b/src/tests/matchers/test_between.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_between.py rename to src/tests/matchers/test_between.py diff --git a/src/pytest_matchers/tests/matchers/test_case.py b/src/tests/matchers/test_case.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_case.py rename to src/tests/matchers/test_case.py diff --git a/src/pytest_matchers/tests/matchers/test_contains.py b/src/tests/matchers/test_contains.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_contains.py rename to src/tests/matchers/test_contains.py diff --git a/src/pytest_matchers/tests/matchers/test_datetime.py b/src/tests/matchers/test_datetime.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_datetime.py rename to src/tests/matchers/test_datetime.py diff --git a/src/pytest_matchers/tests/matchers/test_datetime_string.py b/src/tests/matchers/test_datetime_string.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_datetime_string.py rename to src/tests/matchers/test_datetime_string.py diff --git a/src/pytest_matchers/tests/matchers/test_dict.py b/src/tests/matchers/test_dict.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_dict.py rename to src/tests/matchers/test_dict.py diff --git a/src/pytest_matchers/tests/matchers/test_different_value.py b/src/tests/matchers/test_different_value.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_different_value.py rename to src/tests/matchers/test_different_value.py diff --git a/src/pytest_matchers/tests/matchers/test_ends_with.py b/src/tests/matchers/test_ends_with.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_ends_with.py rename to src/tests/matchers/test_ends_with.py diff --git a/src/pytest_matchers/tests/matchers/test_eq.py b/src/tests/matchers/test_eq.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_eq.py rename to src/tests/matchers/test_eq.py diff --git a/src/pytest_matchers/tests/matchers/test_has_attribute.py b/src/tests/matchers/test_has_attribute.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_has_attribute.py rename to src/tests/matchers/test_has_attribute.py diff --git a/src/pytest_matchers/tests/matchers/test_if.py b/src/tests/matchers/test_if.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_if.py rename to src/tests/matchers/test_if.py diff --git a/src/pytest_matchers/tests/matchers/test_is_instance.py b/src/tests/matchers/test_is_instance.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_is_instance.py rename to src/tests/matchers/test_is_instance.py diff --git a/src/pytest_matchers/tests/matchers/test_is_list.py b/src/tests/matchers/test_is_list.py similarity index 62% rename from src/pytest_matchers/tests/matchers/test_is_list.py rename to src/tests/matchers/test_is_list.py index a8c9c3f..2022254 100644 --- a/src/pytest_matchers/tests/matchers/test_is_list.py +++ b/src/tests/matchers/test_is_list.py @@ -1,67 +1,67 @@ # pylint: disable=use-implicit-booleaness-not-comparison import pytest -from pytest_matchers.matchers import IsList +from pytest_matchers.matchers import List def test_create(): - matcher = IsList(int) - assert isinstance(matcher, IsList) + matcher = List(int) + assert isinstance(matcher, List) with pytest.raises( ValueError, match="Cannot specify length with min_length or max_length", ): - IsList(int, min_length=1, max_length=4, length=2) + List(int, min_length=1, max_length=4, length=2) def test_repr(): - matcher = IsList() + matcher = List() assert repr(matcher) == "To be a list" - matcher = IsList(int) + matcher = List(int) assert repr(matcher) == "To be a list of 'int' instance" - matcher = IsList(str) + matcher = List(str) assert repr(matcher) == "To be a list of 'str' instance" - matcher = IsList(int, length=2) + matcher = List(int, length=2) assert repr(matcher) == "To be a list of 'int' instance and with length of 2" - matcher = IsList(int, min_length=2) + matcher = List(int, min_length=2) assert repr(matcher) == "To be a list of 'int' instance and with length greater or equal than 2" - matcher = IsList(int, max_length=2) + matcher = List(int, max_length=2) assert repr(matcher) == "To be a list of 'int' instance and with length lower or equal than 2" - matcher = IsList(int, min_length=2, max_length=4) + matcher = List(int, min_length=2, max_length=4) assert repr(matcher) == "To be a list of 'int' instance and with length between 2 and 4" - matcher = IsList(None, length=2) + matcher = List(None, length=2) assert repr(matcher) == "To be a list with length of 2" def test_matches_type(): - assert [] == IsList(int) - assert IsList(int) == [] - assert [1, 2, 3] == IsList(int) - assert IsList(int) == [1, 2, 3] - assert [1, 2, 3] != IsList(str) - assert [1, 2, 3] != IsList(str) - assert "string" != IsList(str) + assert [] == List(int) + assert List(int) == [] + assert [1, 2, 3] == List(int) + assert List(int) == [1, 2, 3] + assert [1, 2, 3] != List(str) + assert [1, 2, 3] != List(str) + assert "string" != List(str) def test_is_list_without_match_type(): - matcher = IsList() + matcher = List() assert matcher == [] assert matcher == [1] assert matcher == ["a"] - matcher = IsList(length=2) + matcher = List(length=2) assert matcher == [1, 2] def test_matches_length(): - matcher = IsList(int, length=2) + matcher = List(int, length=2) assert matcher == [1, 2] assert matcher != [1] assert matcher != [1, 2, 3] def test_matches_min_length(): - matcher = IsList(int, min_length=2) + matcher = List(int, min_length=2) assert matcher == [1, 2] assert matcher == [1, 2, 3] assert matcher != [1] @@ -69,7 +69,7 @@ def test_matches_min_length(): def test_matches_max_length(): - matcher = IsList(int, max_length=2) + matcher = List(int, max_length=2) assert [] == matcher assert matcher == [] assert matcher == [1, 2] diff --git a/src/pytest_matchers/tests/matchers/test_is_number.py b/src/tests/matchers/test_is_number.py similarity index 62% rename from src/pytest_matchers/tests/matchers/test_is_number.py rename to src/tests/matchers/test_is_number.py index 547487d..1228893 100644 --- a/src/pytest_matchers/tests/matchers/test_is_number.py +++ b/src/tests/matchers/test_is_number.py @@ -1,45 +1,45 @@ import pytest -from pytest_matchers.matchers import IsNumber +from pytest_matchers.matchers import Number def test_create(): - matcher = IsNumber() - assert isinstance(matcher, IsNumber) - matcher = IsNumber(int) - assert isinstance(matcher, IsNumber) - matcher = IsNumber(min_value=1, max_value=2) - assert isinstance(matcher, IsNumber) - matcher = IsNumber(min_value=1, max_value=2, min_inclusive=False) - assert isinstance(matcher, IsNumber) + matcher = Number() + assert isinstance(matcher, Number) + matcher = Number(int) + assert isinstance(matcher, Number) + matcher = Number(min_value=1, max_value=2) + assert isinstance(matcher, Number) + matcher = Number(min_value=1, max_value=2, min_inclusive=False) + assert isinstance(matcher, Number) with pytest.raises( ValueError, match="Cannot specify inclusive and min_inclusive or max_inclusive", ): - IsNumber(min_value=1, max_value=2, inclusive=True, min_inclusive=True) + Number(min_value=1, max_value=2, inclusive=True, min_inclusive=True) def test_repr(): - matcher = IsNumber() + matcher = Number() assert repr(matcher) == "To be a number" - matcher = IsNumber(int) + matcher = Number(int) assert repr(matcher) == "To be a number of 'int' instance" - matcher = IsNumber(min_value=1, max_value=2) + matcher = Number(min_value=1, max_value=2) assert repr(matcher) == "To be a number between 1 and 2" - matcher = IsNumber(min_value=1, max_value=2, inclusive=False) + matcher = Number(min_value=1, max_value=2, inclusive=False) assert repr(matcher) == "To be a number between 1 and 2 exclusive" - matcher = IsNumber(min_value=1, max_value=2, min_inclusive=False) + matcher = Number(min_value=1, max_value=2, min_inclusive=False) assert repr(matcher) == "To be a number greater than 1 and lower or equal than 2" - matcher = IsNumber(min_value=1, max_value=2, max_inclusive=False) + matcher = Number(min_value=1, max_value=2, max_inclusive=False) assert repr(matcher) == "To be a number greater or equal than 1 and lower than 2" - matcher = IsNumber(float, min_value=1, max_value=2) + matcher = Number(float, min_value=1, max_value=2) assert repr(matcher) == "To be a number of 'float' instance and between 1 and 2" - matcher = IsNumber(min_value=1, max_value=1) + matcher = Number(min_value=1, max_value=1) assert repr(matcher) == "To be a number equal to 1" def test_matches_number(): - matcher = IsNumber() + matcher = Number() assert matcher == 20 assert matcher == 20.0 assert matcher != "string" @@ -47,7 +47,7 @@ def test_matches_number(): def test_matches_type(): - matcher = IsNumber(int) + matcher = Number(int) assert matcher == 20 assert matcher != 20.0 assert matcher != "string" @@ -55,7 +55,7 @@ def test_matches_type(): def test_matches_limit_inclusive(): - matcher = IsNumber(min_value=1, max_value=2) + matcher = Number(min_value=1, max_value=2) assert matcher == 1 assert matcher == 1.5 assert matcher == 2 @@ -65,7 +65,7 @@ def test_matches_limit_inclusive(): def test_matches_limit_exclusive(): - matcher = IsNumber(min_value=1, max_value=2, inclusive=False) + matcher = Number(min_value=1, max_value=2, inclusive=False) assert matcher == 1.5 assert matcher != 1 assert matcher != 2 @@ -75,7 +75,7 @@ def test_matches_limit_exclusive(): def test_matches_min_exclusive(): - matcher = IsNumber(min_value=1, max_value=2, min_inclusive=False) + matcher = Number(min_value=1, max_value=2, min_inclusive=False) assert matcher == 1.5 assert matcher == 2 assert matcher != 1 @@ -85,7 +85,7 @@ def test_matches_min_exclusive(): def test_matches_max_exclusive(): - matcher = IsNumber(min_value=1, max_value=2, max_inclusive=False) + matcher = Number(min_value=1, max_value=2, max_inclusive=False) assert matcher == 1 assert matcher == 1.5 assert matcher != 2 @@ -95,7 +95,7 @@ def test_matches_max_exclusive(): def test_matches_without_max(): - matcher = IsNumber(min_value=1) + matcher = Number(min_value=1) assert matcher == 1 assert matcher == 1.5 assert matcher == 3 diff --git a/src/pytest_matchers/tests/matchers/test_is_string.py b/src/tests/matchers/test_is_string.py similarity index 70% rename from src/pytest_matchers/tests/matchers/test_is_string.py rename to src/tests/matchers/test_is_string.py index 168a098..c5e6c90 100644 --- a/src/pytest_matchers/tests/matchers/test_is_string.py +++ b/src/tests/matchers/test_is_string.py @@ -1,37 +1,37 @@ import pytest -from pytest_matchers.matchers import IsString +from pytest_matchers.matchers import String def test_create(): - matcher = IsString() - assert isinstance(matcher, IsString) - matcher = IsString(starts_with="a", ends_with="b", contains="c") - assert isinstance(matcher, IsString) - matcher = IsString(length=1) - assert isinstance(matcher, IsString) + matcher = String() + assert isinstance(matcher, String) + matcher = String(starts_with="a", ends_with="b", contains="c") + assert isinstance(matcher, String) + matcher = String(length=1) + assert isinstance(matcher, String) with pytest.raises( ValueError, match="Cannot specify length with min_length or max_length", ): - IsString(min_length=1, max_length=4, length=2) + String(min_length=1, max_length=4, length=2) def test_repr(): - matcher = IsString() + matcher = String() assert repr(matcher) == "To be a string" - matcher = IsString(contains="ab") + matcher = String(contains="ab") assert repr(matcher) == "To be a string containing 'ab'" - matcher = IsString(starts_with="ab") + matcher = String(starts_with="ab") assert repr(matcher) == "To be a string starting with 'ab'" - matcher = IsString(ends_with="bc") + matcher = String(ends_with="bc") assert repr(matcher) == "To be a string ending with 'bc'" - matcher = IsString(length=1) + matcher = String(length=1) assert repr(matcher) == "To be a string with length of 1" - matcher = IsString(min_length=1, max_length=3) + matcher = String(min_length=1, max_length=3) assert repr(matcher) == "To be a string with length between 1 and 3" - matcher = IsString( + matcher = String( contains="ab", starts_with="ab", ends_with="bc", @@ -45,14 +45,14 @@ def test_repr(): def test_matches_type(): - matcher = IsString() + matcher = String() assert matcher == "string" assert matcher != 20 assert matcher != ["string"] def test_matches_exact_length(): - matcher = IsString(length=1) + matcher = String(length=1) assert matcher == "a" assert matcher != "" assert matcher != "ab" @@ -60,7 +60,7 @@ def test_matches_exact_length(): def test_matches_min_and_max_length(): - matcher = IsString(min_length=1, max_length=3) + matcher = String(min_length=1, max_length=3) assert matcher == "a" assert matcher == "ab" assert matcher == "abc" @@ -69,7 +69,7 @@ def test_matches_min_and_max_length(): def test_matches_contains(): - matcher = IsString(contains="ab") + matcher = String(contains="ab") assert matcher == "ab" assert matcher == "abc" assert matcher == "dabc" @@ -80,7 +80,7 @@ def test_matches_contains(): def test_matches_starts_with(): - matcher = IsString(starts_with="ab") + matcher = String(starts_with="ab") assert matcher == "ab" assert matcher == "abc" assert matcher != "dabc" @@ -91,7 +91,7 @@ def test_matches_starts_with(): def test_matches_ends_with(): - matcher = IsString(ends_with="bc") + matcher = String(ends_with="bc") assert matcher == "abc" assert matcher == "bc" assert matcher != "dabcx" diff --git a/src/pytest_matchers/tests/matchers/test_json.py b/src/tests/matchers/test_json.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_json.py rename to src/tests/matchers/test_json.py diff --git a/src/pytest_matchers/tests/matchers/test_length.py b/src/tests/matchers/test_length.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_length.py rename to src/tests/matchers/test_length.py diff --git a/src/tests/matchers/test_matcher_factory.py b/src/tests/matchers/test_matcher_factory.py new file mode 100644 index 0000000..9b9b673 --- /dev/null +++ b/src/tests/matchers/test_matcher_factory.py @@ -0,0 +1,47 @@ +import pytest + +from pytest_matchers.matchers import MatcherFactory +from pytest_matchers.matchers.matcher_factory import matcher + + +class FakeMatcher: + def test_me(self) -> bool: + return True + + def __repr__(self): + return "Fake Matcher" + + +def test_register(): + MatcherFactory.matchers = {} + MatcherFactory.register(FakeMatcher) + assert MatcherFactory.matchers == {"FakeMatcher": FakeMatcher} + + +def test_get(): + MatcherFactory.matchers = {} + with pytest.raises(KeyError): + MatcherFactory.get("FakeMatcher") + MatcherFactory.register(FakeMatcher) + assert MatcherFactory.get("FakeMatcher") == FakeMatcher + assert MatcherFactory.get("FakeMatcher")().test_me() is True + assert MatcherFactory.get("FakeMatcher").__name__ == "FakeMatcher" + assert repr(MatcherFactory.get("FakeMatcher")()) == "Fake Matcher" + + +def test_matcher_decorator(): + MatcherFactory.matchers = {} + with pytest.raises(KeyError): + MatcherFactory.get("FakeMatcher2") + + @matcher + class FakeMatcher2: + def test_me(self) -> bool: + return True + + def __repr__(self): + return "Fake Matcher 2" + + assert MatcherFactory.get("FakeMatcher2") == FakeMatcher2 + assert MatcherFactory.get("FakeMatcher2")().test_me() is True + assert repr(MatcherFactory.get("FakeMatcher2")()) == "Fake Matcher 2" diff --git a/src/pytest_matchers/tests/matchers/test_not.py b/src/tests/matchers/test_not.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_not.py rename to src/tests/matchers/test_not.py diff --git a/src/pytest_matchers/tests/matchers/test_or.py b/src/tests/matchers/test_or.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_or.py rename to src/tests/matchers/test_or.py diff --git a/src/pytest_matchers/tests/matchers/test_same_value.py b/src/tests/matchers/test_same_value.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_same_value.py rename to src/tests/matchers/test_same_value.py diff --git a/src/pytest_matchers/tests/matchers/test_starts_with.py b/src/tests/matchers/test_starts_with.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_starts_with.py rename to src/tests/matchers/test_starts_with.py diff --git a/src/pytest_matchers/tests/matchers/test_strict_dict.py b/src/tests/matchers/test_strict_dict.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_strict_dict.py rename to src/tests/matchers/test_strict_dict.py diff --git a/src/pytest_matchers/tests/matchers/test_uuid.py b/src/tests/matchers/test_uuid.py similarity index 100% rename from src/pytest_matchers/tests/matchers/test_uuid.py rename to src/tests/matchers/test_uuid.py diff --git a/src/pytest_matchers/tests/test_main.py b/src/tests/test_main.py similarity index 88% rename from src/pytest_matchers/tests/test_main.py rename to src/tests/test_main.py index 60edcda..1d36ffc 100644 --- a/src/pytest_matchers/tests/test_main.py +++ b/src/tests/test_main.py @@ -3,8 +3,12 @@ from unittest.mock import MagicMock from uuid import uuid4 +import pytest + from pytest_matchers import ( anything, + assert_match, + assert_not_match, between, case, different_value, @@ -24,6 +28,33 @@ one_of, same_value, ) +from src.tests.conftest import CustomEqual + + +def test_assert_match(): + assert is_instance(CustomEqual) == CustomEqual(3) + with pytest.raises( + AssertionError, + match="WARNING! Comparison failed because the left object redefines the equality operator.", + ): + assert CustomEqual(3) == is_instance(CustomEqual) + assert_match(is_instance(CustomEqual), CustomEqual(3)) + assert_match(CustomEqual(3), is_instance(CustomEqual)) + with pytest.raises(AssertionError): + assert_match(is_instance(str), CustomEqual(3)) + + +def test_assert_not_match(): + assert is_instance(str) != CustomEqual(3) + with pytest.raises( + AssertionError, + match="WARNING! Comparison failed because the left object redefines the equality operator.", + ): + assert CustomEqual(3) != is_instance(str) + assert_not_match(is_instance(str), CustomEqual(3)) + assert_not_match(CustomEqual(3), is_instance(str)) + with pytest.raises(AssertionError): + assert_not_match(is_instance(CustomEqual), CustomEqual(3)) def test_anything(): diff --git a/src/tests/test_plugin.py b/src/tests/test_plugin.py new file mode 100644 index 0000000..366929e --- /dev/null +++ b/src/tests/test_plugin.py @@ -0,0 +1,154 @@ +import pytest + +from pytest_matchers import is_instance, is_string +from src.tests.conftest import CustomEqual + + +@pytest.fixture(autouse=True) +def _set_verbosity(request): + previous_verbosity = request.config.option.verbose + request.config.option.verbose = 2 + yield + request.config.option.verbose = previous_verbosity + + +def _expected_base(actual, expected, operator): + return f"assert {actual!r} {operator} {expected!r}" + + +def _expected_warning(actual, expected, operator): + return _expected_base(actual, expected, operator) + ( + "\n \n" + " WARNING! Comparison failed because the left object redefines the equality operator.\n" + " Consider using the 'assert_match' or 'assert_not_match' functions instead.\n" + " assert_match(actual, expected)" + ) + + +def _expected_list_diff(results: list) -> str: + message = ["", "", "Full diff:", " ["] + batch_adds = [] + for value in results: + if isinstance(value, tuple): + actual, expected = value + message.append(f"- {repr(expected)},") + batch_adds.append(f"+ {repr(actual)},") + else: + message.extend(batch_adds) + batch_adds = [] + message.append(f" {repr(value)},") + message.extend(batch_adds) + message.append(" ]") + return "\n ".join(message) + + +def test_non_custom_assert_repr(): + actual = "string" + expected = is_instance(int) + try: + assert actual == expected + except AssertionError as error: + assert str(error) == _expected_base(actual, expected, "==") + + +def test_custom_assert_repr(): + actual = CustomEqual(3) + expected = is_instance(CustomEqual) + try: + assert actual == expected + except AssertionError as error: + assert str(error) == _expected_warning(actual, expected, "==") + + +def test_custom_assert_repr_not(): + actual = CustomEqual(3) + expected = is_instance(str) + try: + assert actual != expected + except AssertionError as error: + assert str(error) == _expected_warning(actual, expected, "!=") + + +def test_custom_assert_repr_dictionary(): + custom = CustomEqual(3) + actual = { + "string": "string", + "custom": custom, + "list": [1, 2, 3], + } + expected = { + "string": is_instance(str), + "custom": is_instance(CustomEqual), + "list": is_instance(list), + } + try: + assert actual == expected + except AssertionError as error: + assert str(error) == _expected_warning(actual, expected, "==") + + +def test_custom_assert_repr_dictionary_matcher_replace(): + custom = CustomEqual(3) + actual = { + "string": "string", + "custom": custom, + "list": [1, 2, 3], + } + expected = { + "string": is_instance(str), + "custom": is_instance(str), + "list": is_instance(list), + } + expected_diff = ( + "\n \n" + " Common items:\n" + " {'list': [1, 2, 3], 'string': 'string'}\n" + " \n" + " Full diff:\n" + " {\n" + " - 'custom': To be instance of 'str',\n" + " - 'list': [1, 2, 3],\n" + f" + 'custom': {custom},\n" + " + 'list': [\n" + " + 1,\n" + " + 2,\n" + " + 3,\n" + " + ],\n" + " 'string': 'string',\n" + " }" + ) + try: + assert actual == expected + except AssertionError as error: + assert str(error) == _expected_base(actual, expected, "==") + expected_diff + + +def test_custom_assert_repr_list(): + custom = CustomEqual(3) + actual = [custom, "hey"] + expected = [is_instance(CustomEqual), is_string()] + try: + assert actual == expected + except AssertionError as error: + assert str(error) == _expected_warning(actual, expected, "==") + + +def test_custom_assert_repr_list_matcher_replace(): + custom = CustomEqual(3) + actual = ["hey", custom] + expected = [is_string(), is_instance(str)] + try: + assert actual == expected + except AssertionError as error: + assert str(error) == _expected_base(actual, expected, "==") + _expected_list_diff( + ["hey", (custom, is_instance(str))] + ) + + +def test_not_equal_operator(): + actual = "string" + expected = is_instance(str) + try: + assert actual != expected + except AssertionError as error: + assert str(error) == _expected_base(actual, expected, "!=") diff --git a/src/tests/utils/__init__.py b/src/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/utils/test_matcher_detector.py b/src/tests/utils/test_matcher_detector.py new file mode 100644 index 0000000..c3362b2 --- /dev/null +++ b/src/tests/utils/test_matcher_detector.py @@ -0,0 +1,50 @@ +from pytest_matchers import is_instance +from pytest_matchers.utils.matcher_detector import MatcherDetector + + +def test_uses_matcher(): + detector = MatcherDetector(3) + assert not detector.uses_matchers() + detector = MatcherDetector(is_instance(int)) + assert detector.uses_matchers() + + +def test_uses_matcher_in_a_list(): + detector = MatcherDetector([3, 4]) + assert not detector.uses_matchers() + detector = MatcherDetector([3, is_instance(int)]) + assert detector.uses_matchers() + + +def test_uses_matcher_in_a_tuple(): + detector = MatcherDetector((3, 4)) + assert not detector.uses_matchers() + detector = MatcherDetector((3, is_instance(int))) + assert detector.uses_matchers() + + +def test_uses_matcher_in_a_set(): + detector = MatcherDetector({3, 4}) + assert not detector.uses_matchers() + detector = MatcherDetector({3, is_instance(int)}) + assert detector.uses_matchers() + + +def test_uses_matcher_in_a_dict(): + detector = MatcherDetector({"a": 3, "b": 4}) + assert not detector.uses_matchers() + detector = MatcherDetector({"a": 3, "b": is_instance(int)}) + assert detector.uses_matchers() + + +def test_uses_matcher_in_nested_iterables(): + detector = MatcherDetector({"a": [3, 4], "b": (5, 6)}) + assert not detector.uses_matchers() + detector = MatcherDetector({"a": [3, is_instance(int)], "b": (5, 6)}) + assert detector.uses_matchers() + detector = MatcherDetector({"a": [3, 4], "b": (5, is_instance(int))}) + assert detector.uses_matchers() + detector = MatcherDetector({"a": {3: is_instance(int)}}) + assert detector.uses_matchers() + detector = MatcherDetector([{"a": 3}, {"b": is_instance(int)}]) + assert detector.uses_matchers() diff --git a/src/pytest_matchers/tests/utils/test_matcher_utils.py b/src/tests/utils/test_matcher_utils.py similarity index 100% rename from src/pytest_matchers/tests/utils/test_matcher_utils.py rename to src/tests/utils/test_matcher_utils.py diff --git a/src/pytest_matchers/tests/utils/test_repr_utils.py b/src/tests/utils/test_repr_utils.py similarity index 100% rename from src/pytest_matchers/tests/utils/test_repr_utils.py rename to src/tests/utils/test_repr_utils.py diff --git a/src/pytest_matchers/tests/utils/test_warn.py b/src/tests/utils/test_warn.py similarity index 100% rename from src/pytest_matchers/tests/utils/test_warn.py rename to src/tests/utils/test_warn.py diff --git a/vulture/whitelist.py b/vulture/whitelist.py index e69de29..a4425b5 100644 --- a/vulture/whitelist.py +++ b/vulture/whitelist.py @@ -0,0 +1,7 @@ +from pytest_matchers.plugin import pytest_assertrepr_compare +from src.tests.conftest import pytest_configure +from src.tests.test_plugin import _set_verbosity + +pytest_configure +pytest_assertrepr_compare +_set_verbosity