compr 0.1.4
Handy wrappers for advanced object comparing and matching.

# 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
```