Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/jwodder/anys

Matchers for pytest
https://github.com/jwodder/anys

available-on-pypi comparison matchers pytest python testing unit-test

Last synced: 8 days ago
JSON representation

Matchers for pytest

Awesome Lists containing this project

README

        

|repostatus| |ci-status| |coverage| |pyversions| |conda| |license|

.. |repostatus| image:: https://www.repostatus.org/badges/latest/active.svg
:target: https://www.repostatus.org/#active
:alt: Project Status: Active — The project has reached a stable, usable
state and is being actively developed.

.. |ci-status| image:: https://github.com/jwodder/anys/actions/workflows/test.yml/badge.svg
:target: https://github.com/jwodder/anys/actions/workflows/test.yml
:alt: CI Status

.. |coverage| image:: https://codecov.io/gh/jwodder/anys/branch/master/graph/badge.svg
:target: https://codecov.io/gh/jwodder/anys

.. |pyversions| image:: https://img.shields.io/pypi/pyversions/anys.svg
:target: https://pypi.org/project/anys/

.. |conda| image:: https://img.shields.io/conda/vn/conda-forge/anys.svg
:target: https://anaconda.org/conda-forge/anys
:alt: Conda Version

.. |license| image:: https://img.shields.io/github/license/jwodder/anys.svg
:target: https://opensource.org/licenses/MIT
:alt: MIT License

`GitHub `_
| `PyPI `_
| `Issues `_
| `Changelog `_

``anys`` provides matchers for pytest_-style assertions. What's a "matcher,"
you say? Well, say you're writing a unit test and you want to assert that a
given object contains the correct values. Normally, you'd just write:

.. code:: python

assert foo == {
"widgets": 42,
"name": "Alice",
"subfoo": {
"created_at": "2021-06-24T18:41:59Z",
"name": "Bob",
"widgets": 23,
}
}

But wait! What if the value of ``foo["subfoo"]["created_at"]`` can't be
determined in advance, but you still need to check that it's a valid timestamp?
You'd have to compare everything in ``foo`` other than that one field to the
expected values and then separately check the timestamp field for validity.
This is where matchers come in: they're magic objects that compare equal to any
& all values that meet given criteria. For the case above, ``anys`` allows you
to just write:

.. code:: python

from anys import ANY_DATETIME_STR

assert foo == {
"widgets": 42,
"name": "Alice",
"subfoo": {
"created_at": ANY_DATETIME_STR,
"name": "Bob",
"widgets": 23,
}
}

and the assertion will do what you mean.

.. _pytest: https://docs.pytest.org

Installation
============
``anys`` requires Python 3.8 or higher. Just use `pip `_
for Python 3 (You have pip, right?) to install it::

python3 -m pip install anys

API
===

``anys`` provides the following classes & constants for matching against values
meeting certain criteria. Matching is performed by comparing a value against
an ``anys`` matcher with ``==``, either directly or as a result of comparing
two larger structures with ``==``.

If a comparison raises a ``TypeError`` or ``ValueError`` (say, because you
evaluated ``42 == AnyMatch(r'\d+')``, which tries to match a regex against an
integer), the exception is suppressed, and the comparison evaluates to
``False``; all other exceptions are propagated out.

``anys`` matchers can be combined using the ``&`` operator to produce new
matchers that require both operands to succeed; for example, ``AnyGT(23) &
AnyLT(42)`` will match any number between 23 and 42, exclusive, and nothing
else.

``anys`` matchers can be combined using the ``|`` operator to produce new
matchers that require at least one of the operands to succeed; for example,
``ANY_INT | ANY_STR`` will match any value that is an ``int`` or a ``str``.

Classes
-------

Note that, unless stated otherwise, ``anys`` class constructors cannot take
``anys`` matchers as arguments.

.. code:: python

AnyContains(key: Any, /)

A matcher that matches any value for which ``key in value`` is true. If
``key`` is an ``anys`` matcher, ``value == AnyContains(key)`` will instead be
evaluated by iterating through the elements of ``value`` and checking whether
any match ``key``.

.. code:: python

AnyFullmatch(pattern: Union[AnyStr, re.Pattern[AnyStr]], /)

A matcher that matches any string ``s`` for which ``re.fullmatch(pattern, s)``
succeeds

.. code:: python

AnyFunc(func: Callable, /)

A matcher that matches any value ``x`` for which ``func(x)`` is true. If
``func(x)`` raises a ``TypeError`` or ``ValueError``, it will be suppressed,
and ``x == AnyFunc(func)`` will evaluate to ``False``. All other exceptions
are propagated out.

.. code:: python

AnyGE(bound: Any, /)

A matcher that matches any value greater than or equal to ``bound``

.. code:: python

AnyGT(bound: Any, /)

A matcher that matches any value greater than ``bound``

.. code:: python

AnyIn(iterable: Iterable, /)

A matcher that matches any value that equals or matches an element of
``iterable`` (which may contain ``anys`` matchers). Note that, if ``iterable``
is a string, only individual characters in the string will match; to match
substrings, use ``AnySubstr()`` instead.

.. code:: python

AnyInstance(classinfo, /)

A matcher that matches any value that is an instance of ``classinfo``.
``classinfo`` can be either a type or a tuple of types (or, starting in Python
3.10, a ``Union`` of types).

A number of pre-composed ``AnyInstance()`` values are provided as constants for
your convenience; see "Constants_" below.

.. code:: python

AnyLE(bound: Any, /)

A matcher that matches any value less than or equal to ``bound``

.. code:: python

AnyLT(bound: Any, /)

A matcher that matches any value less than ``bound``

.. code:: python

AnyMatch(pattern: Union[AnyStr, re.Pattern[AnyStr]], /)

A matcher that matches any string ``s`` for which ``re.match(pattern, s)``
succeeds

.. code:: python

AnySearch(pattern: Union[AnyStr, re.Pattern[AnyStr]], /)

A matcher that matches any string ``s`` for which ``re.search(pattern, s)``
succeeds

.. code:: python

AnySubstr(s: AnyStr, /)

A matcher that matches any substring of ``s``

.. code:: python

AnyWithAttrs(mapping: Mapping, /)

A matcher that matches any object ``obj`` such that ``getattr(obj, k) == v``
for all ``k,v`` in ``mapping.items()``.

The values (but not the keys) of ``mapping`` can be ``anys`` matchers.

.. code:: python

AnyWithEntries(mapping: Mapping, /)

A matcher that matches any object ``obj`` such that ``obj[k] == v`` for all
``k,v`` in ``mapping.items()``.

The values (but not the keys) of ``mapping`` can be ``anys`` matchers.

.. code:: python

Maybe(arg: Any, /)

A matcher that matches ``None`` and any value that equals or matches ``arg``
(which can be an ``anys`` matcher)

.. code:: python

Not(arg: Any, /)

A matcher that matches anything that does not equal or match ``arg`` (which can
be an ``anys`` matcher)

Constants
---------

The following constants match values of the given type:

- ``ANY_BOOL``
- ``ANY_BYTES``
- ``ANY_COMPLEX``
- ``ANY_DATE`` — Matches ``date`` instances. You may not be aware, but
``datetime`` is a subclass of ``date``, and so this also matches
``datetime``\s. If you only want to match actual ``date``\s, use
``ANY_STRICT_DATE``.
- ``ANY_DATETIME``
- ``ANY_DICT``
- ``ANY_FLOAT``
- ``ANY_INT``
- ``ANY_ITERABLE``
- ``ANY_ITERATOR``
- ``ANY_LIST``
- ``ANY_MAPPING``
- ``ANY_NUMBER``
- ``ANY_SEQUENCE``
- ``ANY_SET``
- ``ANY_STR``
- ``ANY_STRICT_DATE`` — Matches any instance of ``date`` that is not an
instance of ``datetime``
- ``ANY_TUPLE``

The following constants match `aware or naïve`__ ``datetime`` or ``time``
values:

__ https://docs.python.org/3/library/datetime.html#aware-and-naive-objects

- ``ANY_AWARE_DATETIME``
- ``ANY_AWARE_TIME``
- ``ANY_NAIVE_DATETIME``
- ``ANY_NAIVE_TIME``

The following constants match ISO 8601-style date, time, & datetime strings.
"Aware" matchers require timezone information, while "naïve" matchers forbid
it.

- ``ANY_AWARE_DATETIME_STR``
- ``ANY_AWARE_TIME_STR``
- ``ANY_DATETIME_STR``
- ``ANY_DATE_STR``
- ``ANY_NAIVE_DATETIME_STR``
- ``ANY_NAIVE_TIME_STR``
- ``ANY_TIME_STR``

Other constants:

- ``ANY_FALSY`` — Matches anything considered false
- ``ANY_TRUTHY`` — Matches anything considered true

Note: If you're after a matcher that matches absolutely everything, Python
already provides that as the `unittest.mock.ANY`__ constant.

__ https://docs.python.org/3/library/unittest.mock.html#any

Caveat: Custom Classes
======================

When a well-behaved class defines an ``__eq__`` method, it will only test
against values of the same class, returning ``NotImplemented`` for other types,
[1]_ which signals Python to evaluate ``x == y`` by instead calling ``y``'s
``__eq__`` method. Thus, when comparing an ``anys`` matcher against an
instance of a well-behaved class, the matcher can be on either the left or the
right of the ``==``. All of the classes in the Python standard library are
well-behaved, as are classes that don't define ``__eq__`` methods, but some
custom classes in third-party code are not well-behaved. In order to
successfully compare an ``anys`` matcher against an ill-behaved class, the
matcher must be on the **left** side of the ``==`` operator; if it is on the
right, only the custom class's ``__eq__`` method will be consulted, which
usually means that the comparison will always evaluate to false.

.. [1] In order to work their magic, ``anys`` matchers do not follow this rule,
and so they are not well-behaved. "Do as I say, not as I do," as they
say.