Package: compr
compr - flexible comparing and matchingRationalecompris a collection of wrappers (comparators) that enable flexible and complex comparing scenarios via simple code.Comparators bind value(s) AND compare logic into single object.
Quite similar to whatpytest.approx(...)does.Comparators redefine__eq__. The goal: exploit==to simplify underlying code (mainly by getting rid of extra if-else's later). Works well with assertions, filters, dispatchers etc.Human readable__repr__is also available!Installationpip install comprList available comparator functions>>> import compr
>>> list(compr._COMPARATORS)UsageWe create comparators via functions in compr module:>>> import copmr
>>> contains_5 = compr.contains(5)  # matches iterable with 5 as one of elements
>>> [3, 4, 6,] == contains_5
False
>>> range(10) == contains_5
TrueThe real profit comes by using comparators as arguments.
So underlying handlers can stay simple (check only==). While flexibility is achieved by providing different comparators when needed.Please see examples (also the one withkwargs2comprdemonstrates in-depth usage scenario).NOTE: These examples are intentionally synthetic for better illustration.Example 0. Basic assert (note the assert message):# Assert actual value is greater than 5
>>> import compr
>>> expected = compr.gt(5)  # gt stands for greater than as in __gt__
>>> actual = 3
>>> assert expected == actual, f'{actual=} does not match {expected=}'
...
AssertionError: actual=3 does not match expected=gt(5)Example 1. Searhing list by condition:# Find index of the first word starting with 't'
>>> from compr import startswith
>>> words = 'one', 'two', 'three', 'four'
>>> words.index(startswith('t'))
1Example 2: Validate HTTP response object attributesImagine requests-likeresponseobject:>>> dir(response)
['status', 'headers', 'body', 'body_len'] # int, dict, str, int respectivelyWithout compr validator with parateters may look like:>>> def validate(response, **kwargs):
>>>     """
>>>     Check if all given kwargs equal to response attributes
>>>     """
>>>     for k, v in kwargs.items():
>>>         if getattr(response, k) == v:
>>>             continue
>>>         else:
>>>             raise AssertionError(k, v)Usage (check if status is 200 and body is 'asdf'):>>> validate(response, status=200, body='asdf')Now let's expand our requirements. We need to check if:body_len is between 300 and 400 bytes (inclusive)status code is < 206.Pushing compare logic into validator above will eventually overwhelm it with complex
code, extra arguments or even magic.Instead we may keep method above unchanged and use comparators:>>> from compr import lt, within  # less-than, fits-range
>>> validate(response, status=lt(206), body_len=within(300, 400))Now whenvalidatetries to==status, it will actually check if its less-than 206.
Similarly with==body_len, it will check if it fits into range [300..400].Profit!Few more steps can make things prettier (introducing kwargs2compr):>>> from compr import kwargs2compr
>>>
>>> def validate(response, **kwargs):
>>>     for k, v in kwargs2compr(kwargs):  # arg name parsing kicks in here
>>>     # allow kwargs to be interpreted as
>>>     # <attribute-name>_<comparator-func-name>=<comparator-func-argument>
>>>         if getattr(response, k) == v:
>>>             continue
>>>         else:
>>>             raise AssertionError(k, v)And usage becomes:>>> validate(response, status_lt=206, body_len_within=[300, 400])After more refactoring (seeall_attrsdocstring for help):>>> from compr import all_attrs
>>>
>>> def validate(response, **kwargs):
>>>         if not all_attrs(response, kwargs2compr(kwargs)):
>>>             raise AssertionError(response, kwargs)Example 3: Combining multiple comparators2 special functions:match_allandmatch_anyallow including other comparators to
check against complex conditions# Check if list shorter than 3 elements OR contains 1
>>> from compr import match_any, shorter_than, contains
>>> [2, 3] == match_any(shorter_than(3), contains(1))
True
>>> [1, 2, 3, 5] == match_any(shorter_than(3), contains(1))
True
>>> [2, 3, 5, 6, 7] == match_any(shorter_than(3), contains(1))
FalseCreating Your own comparator object:@compr.comparator
def somewhat_equal(actual, expected):
    """Compare with 10% precision"""
    return 0.9 * expected < actual < 1.1 * expected