Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

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

Check whether value casts to True boolean, as in bool(value).

assert True == Truish()
assert 1 == Truish()
assert "foo" == Truish()
assert False != Truish()

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"))

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.0rc0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cmplib 1.4.0rc0
File Size Uploaded
cmplib-1.4.0rc0.tar.gz 26.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cmplib 1.4.0rc0
File Interpreter ABI Platform
cmplib-1.4.0rc0-py3-none-any.whl Python 3 none any Details

Total release size: 39.8 kB

Release files / cmplib-1.4.0rc0.tar.gz

Download URL cmplib-1.4.0rc0.tar.gz
Size 26.0 kB
Tags Source
SHA-256 checksum
How to use checksums
ba95f66c345cd3cdb1e75aba018e8809a2c77d98e65a94c4c60f8726755a19e5
BLAKE2b-256 checksum
How to use checksums
4c9192a7c7c95985c54b71dc4ee300ec4880ce6d3c100b52c3aec8c9b16d1ee0
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.0rc0-py3-none-any.whl

Download URL cmplib-1.4.0rc0-py3-none-any.whl
Size 13.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
57573b7ab4c4d0b63d04f018ce0cb6c4d36fc15ca00acd0ea02b519f6d15534d
BLAKE2b-256 checksum
How to use checksums
4e0d1591ddabb4949116b537a5c7bc45f04609e23b8d7b1d54d9518d17633ae2
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 history Release notifications | RSS feed

1.4.1

2 release files

1.4.0

2 release files

This release

1.4.0rc0 This release

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page