Skip to content

Commit

Permalink
AssertionLib 2.3.0 (#19)
Browse files Browse the repository at this point in the history
AssertionLib 2.3.0
------------------

* Added the ``AssertionManager.xor()``, ``AssertionManager.isdisjoint()`` and ``AssertionManager.length_hint()`` methods.
* Annotate most ``AssertionManager`` methods using Protocols.
* Moved Protocols to their own separate stub module.
* Cleaned up the ``_MetaAM`` metaclass.
* Reworked some of the internals of ``AssertionManager``.
* Added tests using [pydocstyle ](https://github.com/henry0312/pytest-pydocstyle).
  • Loading branch information
BvB93 committed May 9, 2020
1 parent 66d7e71 commit 05ab861
Show file tree
Hide file tree
Showing 29 changed files with 1,349 additions and 986 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: pip install -e .[test]
run: pip install -e .[test]; pip install git+https://github.com/numpy/numpy-stubs@master

- name: Test with pytest
run: pytest assertionlib tests docs
run: pytest assertionlib tests
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ install:

# Install the tests
- pip install .[test] --upgrade
- pip install git+https://github.com/numpy/numpy-stubs@master

script:
# Run the unitary tests excluding the expensive computations
- pytest assertionlib tests docs
- pytest assertionlib tests
- coverage xml && coverage report -m

branches:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ All notable changes to this project will be documented in this file.
This project adheres to `Semantic Versioning <http://semver.org/>`_.


2.3.0
*****
* Added the ``AssertionManager.xor()``, ``AssertionManager.isdisjoint()`` and ``AssertionManager.length_hint()`` methods.
* Annotate most ``AssertionManager`` methods using Protocols.
* Moved Protocols to their own separate stub module.
* Cleaned up the ``_MetaAM`` metaclass.
* Reworked some of the internals of ``AssertionManager``.
* Added tests using `pydocstyle <https://github.com/henry0312/pytest-pydocstyle>`_.


2.2.3
*****
* Windows bug fix: Check for the presence of the ``AssertionManager._isdir()``
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


##################
AssertionLib 2.2.3
AssertionLib 2.3.0
##################

A package for performing assertions and providing informative exception messages.
Expand Down
6 changes: 0 additions & 6 deletions assertionlib/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ A module for holding the ``assertionlib.NDRepr()`` class,
a subclass of the builtin ``reprlib.Repr()`` class.


``assertionlib.signature_utils``
--------------------------------
Various functions for manipulating the signatures of callables.


.. _`Python 3.7`: https://www.python.org/dev/peps/pep-0557/

.. |dataclasses| replace:: :mod:`dataclasses`
Expand All @@ -38,7 +33,6 @@ Various functions for manipulating the signatures of callables.
.. |assertionlib.functions| replace:: :mod:`assertionlib.functions`
.. |assertionlib.manager| replace:: :mod:`assertionlib.manager`
.. |assertionlib.ndrepr| replace:: :mod:`assertionlib.ndrepr`
.. |assertionlib.signature| replace:: :mod:`assertionlib.signature`

.. |assertionlib.AssertionManager| replace:: :class:`assertionlib.AssertionManager<assertionlib.manager.AssertionManager>`
.. |assertionlib.NDRepr| replace:: :class:`assertionlib.NDRepr<assertionlib.ndrepr.NDRepr>`
Expand Down
2 changes: 2 additions & 0 deletions assertionlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""AssertionLib."""

from .__version__ import __version__

from .ndrepr import NDRepr, aNDRepr
Expand Down
4 changes: 3 additions & 1 deletion assertionlib/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
__version__ = '2.2.3'
"""The AssertionLib version."""

__version__ = '2.3.0'
185 changes: 185 additions & 0 deletions assertionlib/assertion_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""A module with various new assertion functions.
Index
-----
.. currentmodule:: assertionlib.assertion_functions
.. autosummary::
len_eq
str_eq
shape_eq
isdisjoint
function_eq
API
---
.. autofunction:: len_eq
.. autofunction:: str_eq
.. autofunction:: shape_eq
.. autofunction:: isdisjoint
.. autofunction:: function_eq
"""

import dis
from types import FunctionType
from itertools import zip_longest
from typing import (
Sized,
Callable,
TypeVar,
Iterable,
Union,
Tuple,
Hashable,
TYPE_CHECKING
)

from .functions import to_positional

if TYPE_CHECKING:
from numpy import ndarray # type: ignore
else:
ndarray = 'numpy.ndarray'

__all__ = ['len_eq', 'str_eq', 'shape_eq', 'isdisjoint', 'function_eq']

T = TypeVar('T')
IT = TypeVar('IT', bound=Union[None, dis.Instruction])


@to_positional
def len_eq(a: Sized, b: int) -> bool:
"""Check if the length of **a** is equivalent to **b**: :code:`len(a) == b`.
Parameters
----------
a : :class:`~collections.abc.Sized`
The object whose size will be evaluated.
b : :class:`int`
The integer that will be matched against the size of **a**.
"""
return len(a) == b


@to_positional
def str_eq(a: T, b: str, *, str_converter: Callable[[T], str] = repr) -> bool:
"""Check if the string-representation of **a** is equivalent to **b**: :code:`repr(a) == b`.
Parameters
----------
a : :class:`T<typing.TypeVar>`
An object whose string represention will be evaluated.
b : :class:`str`
The string that will be matched against the string-output of **a**.
Keyword Arguments
-----------------
str_converter : :data:`Callable[[T], str]<typing.Callable>`
The callable for constructing **a**'s string representation.
Uses :func:`repr` by default.
"""
return str_converter(a) == b


@to_positional
def shape_eq(a: ndarray, b: Union[ndarray, Tuple[int, ...]]) -> bool:
"""Check if the shapes of **a** and **b** are equivalent: :code:`a.shape == getattr(b, 'shape', b)`.
**b** should be either an object with the ``shape`` attribute (*e.g.* a NumPy array)
or a :class:`tuple` representing a valid array shape.
Parameters
----------
a : :class:`numpy.ndarray`
A NumPy array.
b : :class:`numpy.ndarray` or :class:`tuple` [:class:`int`, ...]
A NumPy array or a tuple of integers representing the shape of **a**.
""" # noqa
return a.shape == getattr(b, 'shape', b)


@to_positional
def isdisjoint(a: Iterable[Hashable], b: Iterable[Hashable]) -> bool:
"""Check if **a** has no elements in **b**.
Parameters
----------
a/b : :class:`~collections.abc.Iterable` [:class:`~collections.abc.Hashable`]
Two to-be compared iterables.
Note that both iterables must consist of hashable objects.
See Also
--------
:meth:`set.isdisjoint()<frozenset.isdisjoint>`
Return ``True`` if two sets have a null intersection.
"""
try:
return a.isdisjoint(b) # type: ignore

# **a** does not have the isdisjoint method
except AttributeError:
return set(a).isdisjoint(b)

# **a.isdisjoint** is not a callable or
# **a** and/or **b** do not consist of hashable elements
except TypeError as ex:
if callable(a.isdisjoint): # type: ignore
raise ex
return set(a).isdisjoint(b)


@to_positional
def function_eq(func1: FunctionType, func2: FunctionType) -> bool:
"""Check if two functions are equivalent by checking if their :attr:`__code__` is identical.
**func1** and **func2** should be instances of :data:`~types.FunctionType`
or any other object with access to the :attr:`__code__` attribute.
Parameters
----------
func1/func2 : :data:`~types.FunctionType`
Two functions.
Examples
--------
.. code:: python
>>> from assertionlib.assertion_functions import function_eq
>>> func1 = lambda x: x + 5
>>> func2 = lambda x: x + 5
>>> func3 = lambda x: 5 + x
>>> print(function_eq(func1, func2))
True
>>> print(function_eq(func1, func3))
False
"""
code1 = None
try:
code1 = func1.__code__
code2 = func2.__code__
except AttributeError as ex:
name, obj = ('func1', func1) if code1 is None else ('func2', func2)
raise TypeError(f"{name!r} expected a function or object with the '__code__' attribute; "
f"observed type: {obj.__class__.__name__!r}") from ex

iterator = zip_longest(dis.get_instructions(code1), dis.get_instructions(code2))
tup_iter = ((_sanitize_instruction(i), _sanitize_instruction(j)) for i, j in iterator)
return all([i == j for i, j in tup_iter])


def _sanitize_instruction(instruction: IT) -> IT:
"""Sanitize the supplied instruction by setting :attr:`~dis.Instruction.starts_line` to :data:`None`.""" # noqa
if instruction is None:
return None
return instruction._replace(starts_line=None) # type: ignore

0 comments on commit 05ab861

Please sign in to comment.