Skip to content
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
Collection of matchers for testing

..tbd

TO ADD: WithAttrs, EachIs, Regexp, FileList
4 changes: 2 additions & 2 deletions majava/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from .matchers import Matcher, And, Or, Any, MayBe, Absent, matcher
from .basic import InInterval, IsInstance, DictContains
from .basic import InInterval, IsInstance, DictContains, Round, Contains, Unordered


__all__ = [
"Matcher", "And", "Or", "Any", "MayBe", "Absent", "matcher",
"InInterval", "IsInstance", "DictContains"
"InInterval", "IsInstance", "DictContains", "Round", "Contains", "Unordered"
]
78 changes: 78 additions & 0 deletions majava/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,81 @@ def __repr__(self):
def _match(self, other):
if not isinstance(other, self.types):
raise Mismatch(other, "", f"not {self}")


class Round(Matcher):
def __init__(self, value, digits=0):
self.value = round(value, digits)
self.digits = digits

def __repr__(self):
return f"Round({self.value})"

def _match(self, other):
if self.value != round(other, self.digits):
raise Mismatch(other, "", f"not ~{self.value}")


class Length(Matcher):
def __init__(self, v):
self.v = v

def __repr__(self):
return f"Length({self.v})"

def _match(self, other):
if len(other) != self.v:
raise Mismatch(other, "", f"len is not {self.v}")


class ContainsOrdered(Matcher):
def __init__(self, items):
self.items = items

def __repr__(self):
return f"ContainsOrdered({self.items})"

def _match(self, other):
idx = 0
for it in self.items:
try:
idx = other[idx:].index(it) + 1
except ValueError:
raise Mismatch(other, "", f"{repr(it)} is not in order")


class _Contains(Matcher):
def __init__(self, items, ordered=False):
self.items = items
self.ordered = ordered

def __repr__(self):
return f"Contains({self.items})"

def _match(self, other):
missing_items = []
for it in self.items:
if it not in other:
missing_items.append(it)

if missing_items:
raise Mismatch.missing_items(other, missing_items)


class Unordered(_Contains):
""" Value must contains all expected items in any order
"""

def __repr__(self):
return f"Unordered({self.items})"

def _match(self, other):
if len(self.items) != len(other):
raise Mismatch(other, "", f"len is not {len(self.items)}")
super()._match(other)


def Contains(items, ordered=False):
if ordered:
return ContainsOrdered(items)
return _Contains(items)
33 changes: 28 additions & 5 deletions majava/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ def invalid_len(cls, value, expected_len, path=""):
return cls(value, path, f"invalid length - got {len(value)}, expected {expected_len}")

@classmethod
def key_missing(cls, value, key, path=""):
return cls(value, path, f"key '{key}' not found")
def missing_keys(cls, value, keys, path=""):
keys_str = ", ".join(repr(i) for i in keys)
return cls(value, path, f"missing items with keys: {keys_str}")

@classmethod
def missing_items(cls, value, items, path=""):
items_str = ", ".join(repr(i) for i in items)
raise Mismatch(value, "", f"missing items: {items_str}")

def __init__(self, value, path, msg):
self.value = value
Expand Down Expand Up @@ -95,13 +101,16 @@ def _match(matcher, value):


class And(Matcher):
def __init__(self, *matchers):
def __init__(self, *matchers, repr=None):
self.matchers = matchers
self._repr = repr

def __and__(self, other):
return And(*self.matchers, other)

def __repr__(self):
if self._repr is not None:
return self._repr
return '&'.join(repr(it) for it in self.matchers)

def _match(self, other):
Expand Down Expand Up @@ -157,6 +166,9 @@ def __repr__(self):


class MayBe(Matcher):
""" To be used in dicts; such items may not exist or must match
"""

def __init__(self, v):
self.v = v

Expand All @@ -169,6 +181,18 @@ def _match(self, other):
_match(self.v, other)


class Lambda(Matcher):
def __init__(self, callback):
self.cb = callable

def __repr__(self):
return f"Lambda({self.cb})"

def _match(self, other):
if not self.cb(other):
raise Mismatch(other, "", "callback returned False")


def _is_missing(val):
return not isinstance(val, (MayBe, _Absent))

Expand Down Expand Up @@ -198,8 +222,7 @@ def _match_dict(matcher: dict, value: dict, allow_unexpected=False):

missing_keys = sorted(filter(lambda k: _is_missing(matcher[k]), missing_keys))
if missing_keys:
missing_keys_str = ", ".join(repr(i) for i in sorted(missing_keys))
raise Mismatch(value, "", f"missing items with keys: {missing_keys_str}")
raise Mismatch.missing_keys(value, sorted(missing_keys))

if unexpected_keys:
unexpected_keys_str = ", ".join(repr(i) for i in unexpected_keys)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ readme = "README.md"
license.text = "MIT"
authors = [{name = "wsnk"}]

version = "0.1.1"
version = "0.2.0"
dependencies = ["pytest"]
optional-dependencies.tests = ["flake8"]

Expand Down
62 changes: 61 additions & 1 deletion tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pytest
from majava import InInterval, IsInstance, DictContains, Absent
from majava import InInterval, IsInstance, DictContains, Absent, Round, Contains, Unordered
from .common import raises_assertion_error


Expand Down Expand Up @@ -62,3 +62,63 @@ def test_dict_contains__mismatch(actual, expected, message):
def test_dict__nested_mismatch(actual, expected, message):
with raises_assertion_error(message):
assert actual == DictContains(expected)


def test_round():
assert 3.14 == Round(3)
assert 3.144 == Round(3.14, 2)
with raises_assertion_error("Value 3.146 does not match: not ~3.14"):
assert 3.146 == Round(3.14, 2)


def test_contains__list():
m = Contains([1, 2, 3])

assert repr(m) == "Contains([1, 2, 3])"
assert [1, 2, 3, 4] == m
assert [4, 3, 2, 1] == m
with raises_assertion_error("Value [3, 4, 5] does not match: missing items: 1, 2"):
assert [3, 4, 5] == m


def test_contains_ordered__list():
m = Contains([2, 4], ordered=True)

assert repr(m) == "ContainsOrdered([2, 4])"
assert [1, 2, 3, 4] == m
with raises_assertion_error("Value [4, 3, 2] does not match: 4 is not in order"):
assert [4, 3, 2] == m


def test_contains__str():
m = Contains(["ab", "cd"])

assert repr(m) == "Contains(['ab', 'cd'])"
assert "_ab_cd_" == m
assert "cd__ab" == m
with raises_assertion_error("Value '_ab_c_d_' does not match: missing items: 'cd'"):
assert "_ab_c_d_" == m


def test_contains_ordered__str():
m = Contains(["ab", "cd"], ordered=True)

assert repr(m) == "ContainsOrdered(['ab', 'cd'])"
assert "_ab cd_" == m
with raises_assertion_error("Value '_cd ab_' does not match: 'cd' is not in order"):
assert "_cd ab_" == m


def test_unordered__list():
m = Unordered([1, 2, 3])

assert repr(m) == "Unordered([1, 2, 3])"

assert [1, 2, 3] == m
assert [2, 3, 1] == m
assert [3, 1, 2] == m

with raises_assertion_error("Value [1, 2, 3, 4] does not match: len is not 3"):
assert [1, 2, 3, 4] == m
with raises_assertion_error("Value [1, 2, 4] does not match: missing items: 3"):
assert [1, 2, 4] == m