Skip to main content

Handy wrappers for advanced object comparing and matching.

Project description

compr - flexible comparing and matching

Rationale

compr is 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 what pytest.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!

Installation

pip install compr

List available comparator functions

>>> import compr
>>> list(compr._COMPARATORS)

Usage

We 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
True

The 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 with kwargs2compr demonstrates 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'))
1

Example 2: Validate HTTP response object attributes

Imagine requests-like response object:

>>> dir(response)
['status', 'headers', 'body', 'body_len'] # int, dict, str, int respectively

Without 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 when validate tries 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 (see all_attrs docstring 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 comparators

2 special functions: match_all and match_any allow 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))
False

Creating Your own comparator object:

@compr.comparator
def somewhat_equal(actual, expected):
    """Compare with 10% precision"""
    return 0.9 * expected < actual < 1.1 * expected

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

compr-0.1.4.tar.gz (5.1 kB view hashes)

Uploaded Source

Built Distribution

compr-0.1.4-py3-none-any.whl (5.2 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page