cmplib
cmplib is a library used for writing composable matchers for your tests, validators etc. cmplib's matchers are compared for equality (or inequality) against your values.
Each matcher provides a human-readable string representation (implemented
by __repr__ dunder method).
List of Matchers
Eq, Ne, Gt, Ge, Lt, Le, Not
Check whether a checked-against value is equal to value stored in matcher,
not equal, greater than, greater or equal, less than or less or equal. Using
of Not negates the meaning of stored sub-matcher or value.
assert "abc" == Eq("abc")
assert "abc" == Eq(And(Contains("b"), Len(3)))
assert 1 != Eq(2)
assert 1 == Gt(0)
assert False == Not(Truish())
assert True != Not(Truish())
assert 1 == Not(Gt(1))
assert 1 == Not(2)
Is
Check whether a value is the same object as the one stored in a matcher.
obj = object()
assert obj == Is(obj)
assert list() != Is(list())
IsNone
Check whether a value is None.
assert None == IsNone()
assert 0 == Not(IsNone())
assert [] != IsNone()
And, Or
Check whether all sub-matchers match the value (for And), or any of them
(for Or).
l = [1, 2, 3]
assert l == And(Contains(1), Contains(2))
assert l != And(Contains(1), Contains(2), Contains(3), Contains(4))
assert l == Or(Contains(5), Contains(2))
assert l != Or(Contains(5), Contains(6), Contains(7), Contains(8))
Instead of passing matchers directly to And or Or, they can be composed
with bitwise and operator (&) and bitwise or operator (|).
l = [1, 2, 3]
assert l == Contains(1) & Contains(2)
assert l == Contains(5) | Contains(2)
Truish, Falsy
Check whether value casts to True or False boolean, as in bool(value).
assert True == Truish()
assert False == Falsy()
assert 1 == Truish()
assert 0 == Falsy()
assert "foo" == Truish()
assert "" == Falsy()
assert False != Truish()
assert True != Falsy()
Len
Check whether container's size matches the matcher.
assert [] == Len(0)
assert [1, 2, 3, "aaa"] == Len(And(Gt(0), Lt(5)))
assert [1, 2, 3, "aaa"] != Len(And(Gt(0), Lt(4)))
IsEmpty
Check whetner container is empty (it's size is 0).
assert [] == IsEmpty()
assert "" == IsEmpty()
assert dict() == IsEmpty()
assert [1] != IsEmpty()
assert "a" != IsEmpty()
Each
Check whether each element of iterable matches the stored sub-matcher or value.
assert [1, 1, 1] == Each(1)
assert [1, 2, 3] == Each(Not(0))
Values
Check whether iterable contains at least 1 value which matches a stored value
or sub-matcher. Number of required matching values may be changed with
keyword-only argument at_least.
Implementation detail: for checking whether a container contains a single
value, it's better to use Contains which will perform better for
certain types of containers (like sets or dictionaries).
assert [0, 2, 3, 1, 2] == Values(1)
assert [] == Values(1, at_least=0)
assert [0, 2, 3, 1, 2] == Values(Gt(1), at_least=2)
assert [] != Values(1)
assert [0, 2, 3, 1, 2] != Values(1, at_least=2)
Contains
Check whether iterable contains a value which matches stored value or sub-matcher.
assert [1, 2, 3] == Contains(1)
assert [1, 2, 3] == Contains(Or("a", 2, "b"))
Unordered
Check whether iterable matches stored values or matchers. Each item must match exactly one matcher, but matching order doesn't matter.
assert [] == Unordered()
assert [1, 2, 3] == Unordered(3, 2, 1)
assert [1, 2, 3] == Unordered(Eq(3), Or(7, 2), 1)
assert [1, 2, 3] != Unordered("1", "2", "3")
To perform ordered search, simply construct an iterable of matchers.
assert [1, 2, 3] == [Eq(1), Or(7, 2), 3]
assert [1, 2, 3] != [Eq(3), Or(7, 2), 1]
KeyEq, AttrEq
For KeyEq check whether a object stores a value under a key. Key can be
either a key in a dict-like object, or index in a list-like or tuple-like
objects.
d = {"foo": "bar", "baz": "blah"}
assert d == KeyEq("foo", "bar")
assert d == KeyEq("foo", Not(IsEmpty()))
assert d == KeyEq("foo", Not(Contains("h")) & Contains("b") & Contains("a"))
lst = ["foo", "bar"]
assert lst == KeyEq(1, "bar")
assert lst == KeyEq(-1, "bar")
For AttrEq check whether an object stores a value under attribute.
@dataclass
class Foo:
foo: str = "bar"
baz: str = "blah"
o = Foo()
assert o == AttrEq("foo", "bar")
assert o != AttrEq("foo", "blah")
IsInstance
Check whether a value is an instance of a given type.
assert 1 == IsInstance(int)
assert datetime.now() == IsInstance(datetime)
Object, DictFields
Composes a matcher from a keyword-only arguments. Each of these arguments
must match a corresponding attribute (for Object) or key (for DictFields)
of checked item.
Object additionally accepts a single optional, non-keyword argument, which
matches against a type of matched item.
These are convenience matchers. The same effect can be accomplished by using
bare AttrEq and KeyEq matchers.
@dataclass
class Foo:
foo: int = 1
bar: int = 2
baz: str = "s"
o: Optional["Foo"] = None
assert Foo() == Object(foo=1, bar=Ge(2))
assert Foo(o=Foo()) == Object(foo=1, o=Object(bar=2, o=None))
assert Foo() != Object(dict, foo=1)
assert Foo() != Object(nonexisting=1)
d = {"foo": 1, "bar": 2, "baz": "s", "o": {"foo": 1}}
assert d == DictFields(foo=1, bar=Ge(2))
assert d == DictFields(foo=1, o=DictFields(foo=1))
assert d != DictFields(foo=1, o=DictFields(foo=2))
assert d != DictFields(foo=1, o=DictFields(nonexisting=2))
Items
Composes a matcher from a index-matcher pairs. Each matcher must match a value in the compared container under the corresponding index.
lst = ["foo", "bar", "baz", "blah"]
assert lst == Items((0, "foo"), (-1, "blah"))
assert lst == Items((-1, "blah"), (0, "foo"))
assert lst != Items((0, "foo"), (-1, "blah2"))
assert lst != Items((0, "foo"), (100, "foo"))
Items is best suited for checking particular items of a sequence. If you want to check a sequence as a whole, it's better to use Seq.
Seq
Composes a matcher which compares each element of matched sequence
against a counterpart submatcher. This is very similar to Items, but the
compared sequence is checked as a whole.
lst = ["foo", "bar", "baz", "blah"]
assert lst == Seq("foo", SKIP, Ne("blah"), Len(4))
assert lst != Seq(SKIP, SKIP, SKIP)
Glob
Test whether a astring matches a given globbing (wildcard) expression.
Matching rules of fnmatch.fnmatchcase apply.
assert "foo" == Glob("*")
assert ["foo", "bar"] == [Glob("f*"), Glob("b*")]
Glob module supports the following wildcards (same as fnmatch module):
*: matches everything?: matches any single character[seq]: matches any character in seq[!seq]: matches any character not in seq
It is possible to automatically convert checked values to string. This way non-string types which support such conversion can be matched as well.
from pathlib import Path
assert Path("/foo/bar") == Glob("/foo/*", coerce=True)
Matching is case-sensitive, but it can be changed to case-insensitive by
passing case=False.
assert "Foo" == Glob("f*", case=False)
assert "Foo" != Glob("f*")
Re
Test whether a astring matches a given regular expression. Matching rules of
re.match apply. Re matcher doesn't store capture groups.
assert "foo bar baz" == Re("^foo.*z$")
assert ["foo", "bar"] == [Re("f.*"), Re("b.*")]
It is possible to automatically convert checked values to string. This way non-string types which support such conversion can be matched as well.
assert 11 == Re(r"\d+", coerce=True)
Re matcher accepts the same flags as functions in ordinary re module.
import re
assert "fOO bar baz\n" == Re("Foo.*", flags = re.IGNORECASE | re.DOTALL)
CanBeTimestamp
Test whether a value can be converted to a UNIX timestamp. UNIX timestamps are floating point numbers, so this means that any value which can be converted to the correct float are considered as such.
assert 0 == CanBeTimestamp()
assert "0" == CanBeTimestamp()
assert "0.123" == CanBeTimestamp()
assert datetime.now().timestamp() == CanBeTimestamp()
assert "" != CanBeTimestamp()
assert datetime.now() != CanBeTimestamp()
IsIsoDateTime
Check whether a value represents a valid datetime (either a date or
datetime object or is a value which follows ISO 8601 format and can be
converted to such value).
assert datetime.now() == IsIsoDateTime()
assert datetime.today() == IsIsoDateTime()
assert datetime.now().isoformat() == IsIsoDateTime()
assert datetime.today().isoformat() == IsIsoDateTime()
assert "2021-01-01" == IsIsoDateTime()
assert "2022-03" != IsIsoDateTime()
IsUnique
Caches values which were already checked against IsUnique and matches them
as long as no such value was previously matched. It is possible to specify a
cache name which should hold values. Values are only compared against other
values stored in the same cache. It is possible to clear a particular cache
or all caches with IsUnique.clear. Caches are shared between IsUnique
instances.
IsUnique is thread-safe.
assert 1 == IsUnique("cache-1")
assert 1 == IsUnique("cache-2")
assert 2 == IsUnique("cache-1")
assert 1 != IsUnique("cache-1")
IsUnique.clear("cache-1")
assert 1 == IsUnique("cache-1")
assert 1 != IsUnique("cache-2")
IsUnique.clear()
assert 1 == IsUnique("cache-1")
assert 1 == IsUnique("cache-2")
Creating IsUnique without a cache name results in generating a new cache
for each instance created this way.
U = IsUnique()
assert 1 == U
assert 1 != U
assert 1 == IsUnique()
assert 1 == IsUnique()
assert [1, 2, 3, 4] == Each(IsUnique())
assert [1, 2, 3, 4, 1, 1, 7] != Each(IsUnique())
Fn
Matches true when a function called with a value passed as the first argument
returns True. When coerce is set to True, stored function doesn't have to
return a boolean, but instead its return value is casted to boolean.
assert 1 == Fn(lambda x: x == 1)
assert "1" == Fn((lambda x: x), coerce=True)
SKIP
SKIP is a sentinel which always matches true and can be used to mark not
interesing items.
assert [1, 2, 3] == [SKIP, SKIP, SKIP]
assert [1, 2, 3] == [1, SKIP, SKIP]
Reporting errors
With complex compound matchers it can be cumbersome to find which part exactly doesn't match. You can use the following techniques to get a human-friendly output which pinpoints which matchers failed. Note that for each matcher instance
pytest
cmplib implements a pytest plugin which should nicely report errors out of the
box. You may disable this feature by passing --cmplib-no-explain flag to
pytest invocation
assert_eq, assert_ne
Outside of pytest, you may use assert_eq and assert_ne to raise assertions
with friendly messages:
assert_eq([1], Contains(Or(Gt(3), Lt(0))))
assert_ne([1], Contains(And(1, Or(Lt(2), Gt(0))))
You can change exception type raised by these functions:
assert_eq(1, Gt(2), exc=ValueError)
explain_failures
Underneath both pytest plugin and custom assert functions use explain_failures
function which receives an instance of matcher, match operator as a string and
matched-against object. It returns a string which should explain the last
failure of passed matcher:
validator = And(1, Or(Gt(2), Lt(1)))
if foo != validator:
# `foo != validator` returns True, which means that `foo == validator`
# fails, so we must get our results against it:
print("validation failure:", explain_failures(foo, "==", validator))
# printout: validation failure: foo == (>2 OR <1)
License
cmplib is licensed under the terms of LGPLv3.
Metadata
Release files for cmplib 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cmplib-1.4.0.tar.gz | 26.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cmplib-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.0 kB
Release files / cmplib-1.4.0.tar.gz
| Download URL | cmplib-1.4.0.tar.gz |
|---|---|
| Size | 26.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b9868ab182a94641dc104ef7299a575f3ab31399b043168369d22793131d057e
|
|
BLAKE2b-256 checksum How to use checksums |
2ebd213f2fde013cd81de61296fa64019535f402686e4a9914dbc4f82b37af64
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / cmplib-1.4.0-py3-none-any.whl
| Download URL | cmplib-1.4.0-py3-none-any.whl |
|---|---|
| Size | 14.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4288b40403465ab6fbab4f1173fee619752658e4aa9778ee33952bd535f89de0
|
|
BLAKE2b-256 checksum How to use checksums |
172582392751e1ce451a86e0d864f0e5130b08ccb8a4c0401a0e03bd640137a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|