From ca488dcd561955f0e25ada98aa106cbe4ee7506c Mon Sep 17 00:00:00 2001
From: Bas van Beek <43369155+BvB93@users.noreply.github.com>
Date: Wed, 3 Jun 2020 15:20:13 +0200
Subject: [PATCH] Nano-Utils 0.2.0 (#3)
* Added new NumPy-specific functions: ``as_nd_array()``, ``array_combinations()`` & ``fill_diagonal_blocks()``.
* Expanded the ``typing_utils`` module with a number of, previously missing, objects.
* Added the ``EMPTY_CONTAINER`` constaint.
* Added the ``VersionInfo`` namedtuple and the ``raise_if()`` & ``split_dict()`` functions.
* Added the ``version_info`` attribute to the package.
---
.github/workflows/pythonpackage.yml | 4 +-
CHANGELOG.rst | 9 +
README.rst | 2 +-
codecov.yaml | 8 +
docs/0_documentation.rst | 1 +
docs/5_numpy.rst | 3 +
docs/conf.py | 10 +-
nanoutils/__init__.py | 6 +-
nanoutils/__version__.py | 2 +-
nanoutils/empty.py | 44 +++--
nanoutils/numpy_utils.py | 232 ++++++++++++++++++++++++
nanoutils/schema.py | 2 +-
nanoutils/typing_utils.py | 39 +++-
nanoutils/utils.py | 271 ++++++++++++++++++++++++++--
setup.py | 9 +-
15 files changed, 595 insertions(+), 47 deletions(-)
create mode 100644 codecov.yaml
create mode 100644 docs/5_numpy.rst
create mode 100644 nanoutils/numpy_utils.py
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 0965cdf..41c933e 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -27,7 +27,9 @@ 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: Python info
run: |
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5f788fa..3b40dd0 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,15 @@ All notable changes to this project will be documented in this file.
This project adheres to `Semantic Versioning `_.
+0.2.0
+*****
+* Added new NumPy-specific functions: ``as_nd_array()``, ``array_combinations()`` & ``fill_diagonal_blocks()``.
+* Expanded the ``typing_utils`` module with a number of, previously missing, objects.
+* Added the ``EMPTY_CONTAINER`` constaint.
+* Added the ``VersionInfo`` namedtuple and the ``raise_if()`` & ``split_dict()`` functions.
+* Added the ``version_info`` attribute to the package.
+
+
0.1.1
*****
* Updated the badges.
diff --git a/README.rst b/README.rst
index 43c2d83..5d21087 100644
--- a/README.rst
+++ b/README.rst
@@ -20,7 +20,7 @@
################
-Nano-Utils 0.1.1
+Nano-Utils 0.2.0
################
Utility functions used throughout the various nlesc-nano repositories.
diff --git a/codecov.yaml b/codecov.yaml
new file mode 100644
index 0000000..63797c3
--- /dev/null
+++ b/codecov.yaml
@@ -0,0 +1,8 @@
+coverage:
+ status:
+ project:
+ default:
+ target: 0
+ patch:
+ default:
+ target: 0
diff --git a/docs/0_documentation.rst b/docs/0_documentation.rst
index 78632c4..5167c81 100755
--- a/docs/0_documentation.rst
+++ b/docs/0_documentation.rst
@@ -5,3 +5,4 @@ API
2_empty.rst
3_schema.rst
4_typing.rst
+ 5_numpy.rst
diff --git a/docs/5_numpy.rst b/docs/5_numpy.rst
new file mode 100644
index 0000000..331aad9
--- /dev/null
+++ b/docs/5_numpy.rst
@@ -0,0 +1,3 @@
+nanoutils.numpy_utils
+=====================
+.. automodule:: nanoutils.numpy_utils
diff --git a/docs/conf.py b/docs/conf.py
index 9c25ba0..8d201bf 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -181,7 +181,8 @@
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
- 'python': ('https://docs.python.org/3/', None)
+ 'python': ('https://docs.python.org/3/', None),
+ 'numpy': ('https://numpy.org/doc/stable/', None)
}
@@ -220,3 +221,10 @@
# False to use the .. rubric:: directive instead.
# Defaults to False.
napoleon_use_admonition_for_references = True
+
+
+# A string of reStructuredText that will be included at the end of every source file that is read.
+# This is a possible place to add substitutions that should be available in every file (another being rst_prolog).
+rst_epilog = """
+.. _semantic: https://semver.org/
+"""
diff --git a/nanoutils/__init__.py b/nanoutils/__init__.py
index 476201a..a4b355d 100644
--- a/nanoutils/__init__.py
+++ b/nanoutils/__init__.py
@@ -4,10 +4,11 @@
from .__version__ import __version__
-from . import typing_utils, empty, utils, schema
+from . import typing_utils, empty, utils, numpy_utils, schema
from .typing_utils import *
from .empty import *
from .utils import *
+from .numpy_utils import *
from .schema import (
Default, Formatter, supports_float, supports_int,
isinstance_factory, issubclass_factory, import_factory
@@ -15,10 +16,11 @@
__author__ = 'B. F. van Beek'
__email__ = 'b.f.van.beek@vu.nl'
-__version__ = __version__
+version_info = VersionInfo.from_str(__version__)
__all__ = []
__all__ += typing_utils.__all__
__all__ += empty.__all__
__all__ += utils.__all__
+__all__ += numpy_utils.__all__
__all__ += schema.__all__
diff --git a/nanoutils/__version__.py b/nanoutils/__version__.py
index b1314c8..cd0b272 100644
--- a/nanoutils/__version__.py
+++ b/nanoutils/__version__.py
@@ -1,3 +1,3 @@
"""The Nano-Utils version."""
-__version__ = '0.1.1'
+__version__ = '0.2.0'
diff --git a/nanoutils/empty.py b/nanoutils/empty.py
index 104c887..ec7ba5d 100644
--- a/nanoutils/empty.py
+++ b/nanoutils/empty.py
@@ -1,45 +1,58 @@
"""A module with empty (immutable) iterables.
-can be used as default arguments for functions.
+Can be used as default arguments for functions.
Index
-----
===================================== =================================================
-:data:`~nanoutils.EMPTY_SEQUENCE` An empty :class:`~collections.abc.Sequence`.
-:data:`~nanoutils.EMPTY_MAPPING` An empty :class:`~collections.abc.Mapping`.
+:data:`~nanoutils.EMPTY_CONTAINER` An empty :class:`~collections.abc.Container`.
:data:`~nanoutils.EMPTY_COLLECTION` An empty :class:`~collections.abc.Collection`.
:data:`~nanoutils.EMPTY_SET` An empty :class:`~collections.abc.Set`.
+:data:`~nanoutils.EMPTY_SEQUENCE` An empty :class:`~collections.abc.Sequence`.
+:data:`~nanoutils.EMPTY_MAPPING` An empty :class:`~collections.abc.Mapping`.
===================================== =================================================
API
---
.. currentmodule:: nanoutils
-.. data:: EMPTY_SEQUENCE
- :value: ()
-
- An empty :class:`~collections.abc.Sequence`.
-
-.. data:: EMPTY_MAPPING
- :value: mappingproxy({})
+.. data:: EMPTY_CONTAINER
+ :type: Container
+ :value: frozenset()
- An empty :class:`~collections.abc.Mapping`.
+ An empty :class:`~collections.abc.Container`.
.. data:: EMPTY_COLLECTION
+ :type: Collection
:value: frozenset()
An empty :class:`~collections.abc.Collection`.
.. data:: EMPTY_SET
+ :type: Set
:value: frozenset()
An empty :class:`~collections.abc.Set`.
-""" # noqa: E501
+.. data:: EMPTY_SEQUENCE
+ :type: Sequence
+ :value: ()
+
+ An empty :class:`~collections.abc.Sequence`.
+
+.. data:: EMPTY_MAPPING
+ :type: Mapping
+ :value: mappingproxy({})
+
+ An empty :class:`~collections.abc.Mapping`.
+
+"""
from types import MappingProxyType
-from typing import Mapping, Collection, Sequence, AbstractSet
+from typing import Mapping, Collection, Sequence, AbstractSet, Container
-__all__ = ['EMPTY_SEQUENCE', 'EMPTY_MAPPING', 'EMPTY_COLLECTION', 'EMPTY_SET']
+__all__ = [
+ 'EMPTY_SEQUENCE', 'EMPTY_MAPPING', 'EMPTY_COLLECTION', 'EMPTY_SET', 'EMPTY_CONTAINER'
+]
#: An empty :class:`~collections.abc.Sequence`.
EMPTY_SEQUENCE: Sequence = ()
@@ -52,3 +65,6 @@
#: An empty :class:`~collections.abc.Set`.
EMPTY_SET: AbstractSet = frozenset()
+
+#: An empty :class:`~collections.abc.Container`.
+EMPTY_CONTAINER: Container = frozenset()
diff --git a/nanoutils/numpy_utils.py b/nanoutils/numpy_utils.py
new file mode 100644
index 0000000..dc32538
--- /dev/null
+++ b/nanoutils/numpy_utils.py
@@ -0,0 +1,232 @@
+""":mod:`numpy` related utility functions.
+
+Note that these functions require the numpy package.
+
+See Also
+--------
+.. image:: https://badge.fury.io/py/numpy.svg
+ :target: https://badge.fury.io/py/numpy
+
+**NumPy** is the fundamental package needed for scientific computing with Python.
+It provides:
+
+* a powerful N-dimensional array object
+* sophisticated (broadcasting) functions
+* tools for integrating C/C++ and Fortran code
+* useful linear algebra, Fourier transform, and random number capabilities
+
+
+Index
+-----
+.. currentmodule:: nanoutils
+.. autosummary::
+{autosummary}
+
+API
+---
+{autofunction}
+
+"""
+
+from math import factorial, nan
+from typing import TYPE_CHECKING, Optional, Union, Iterable
+from itertools import combinations
+from collections import abc
+
+from .utils import raise_if, construct_api_doc
+
+try:
+ import numpy as np
+ NUMPY_EX: Optional[ImportError] = None
+except ImportError as ex:
+ NUMPY_EX = ex
+
+if TYPE_CHECKING:
+ from numpy import ndarray
+ from numpy.typing import DtypeLike, ArrayLike
+else:
+ DtypeLike = 'numpy.dtype'
+ ArrayLike = ndarray = 'numpy.ndarray'
+
+__all__ = ['as_nd_array', 'array_combinations', 'fill_diagonal_blocks']
+
+
+@raise_if(NUMPY_EX)
+def as_nd_array(value: Union[Iterable, ArrayLike], dtype: DtypeLike, ndmin: int = 1) -> ndarray:
+ """Construct a numpy array from an iterable or array-like object.
+
+ Examples
+ --------
+ .. code:: python
+
+ >>> from nanoutils import as_nd_array
+
+ >>> as_nd_array(1, int)
+ array([1])
+
+ >>> as_nd_array([1, 2, 3, 4], int)
+ array([1, 2, 3, 4])
+
+ >>> iterator = iter([1, 2, 3, 4])
+ >>> as_nd_array(iterator, int)
+ array([1, 2, 3, 4])
+
+
+ Parameters
+ ----------
+ value : :class:`~collections.abc.Iterable` or array-like
+ An array-like object or an iterable consisting of scalars.
+ dtype : data-type
+ The data type of the to-be returned array.
+ ndmin : :class:`int`
+ The minimum dimensionality of the to-be returned array.
+
+ Returns
+ -------
+ :class:`numpy.ndarray`
+ A numpy array constructed from **value**.
+
+ """
+ try:
+ return np.array(value, dtype=dtype, ndmin=ndmin, copy=False)
+
+ except TypeError as ex:
+ if not isinstance(value, abc.Iterable):
+ raise ex
+
+ ret = np.fromiter(value, dtype=dtype)
+ ret.shape += (ndmin - ret.ndim) * (1,)
+ return ret
+
+
+@raise_if(NUMPY_EX)
+def array_combinations(array: ArrayLike, r: int = 2, axis: int = -1) -> ndarray:
+ r"""Construct an array with all :func:`~itertools.combinations` of **ar** along a use-specified axis.
+
+ Examples
+ --------
+ .. code:: python
+
+ >>> from nanoutils import array_combinations
+
+ >>> array = [[1, 2, 3, 4],
+ ... [5, 6, 7, 8]]
+
+ >>> array_combinations(array, r=2)
+ array([[[1, 2],
+ [5, 6]],
+
+ [[1, 3],
+ [5, 7]],
+
+ [[1, 4],
+ [5, 8]],
+
+ [[2, 3],
+ [6, 7]],
+
+ [[2, 4],
+ [6, 8]],
+
+ [[3, 4],
+ [7, 8]]])
+
+ Parameters
+ ----------
+ array : array-like, shape :math:`(m, \dotsc)`
+ An :math:`n` dimensional array-like object.
+ r : :class:`int`
+ The length of each combination.
+ axis : :class:`int`
+ The axis used for constructing the combinations.
+
+ Returns
+ -------
+ :class:`numpy.ndarray`, shape :math:`(k, \dotsc, r)`
+ A :math:`n+1` dimensional array with all **ar** combinations (of length ``r``)
+ along axis -1.
+ :math:`k` represents the number of combinations: :math:`k = \dfrac{m! / r!}{(m-r)!}`.
+
+ """ # noqa: E501
+ ar = np.array(array, ndmin=1, copy=False)
+ n = ar.shape[axis]
+
+ # Identify the number of combinations
+ try:
+ combinations_len = int(factorial(n) / factorial(r) / factorial(n - r))
+ except ValueError as ex:
+ raise ValueError(f"'r' ({r!r}) expects a positive integer larger than or equal to the "
+ f"length of 'array' axis {axis!r} ({n!r})") from ex
+
+ # Define the shape of the to-be returned array
+ _shape = list(ar.shape)
+ del _shape[axis]
+ shape = (combinations_len,) + tuple(_shape) + (r,)
+
+ # Create, fill and return the new array
+ ret = np.empty(shape, dtype=ar.dtype)
+ for i, idx in enumerate(combinations(range(n), r=r)):
+ ret[i] = ar.take(idx, axis=axis)
+ return ret
+
+
+@raise_if(NUMPY_EX)
+def fill_diagonal_blocks(array: ndarray, i: int, j: int, val: float = nan) -> None:
+ """Fill diagonal blocks in **array** of size :math:`(i, j)`.
+
+ The blocks are filled along the last 2 axes in **array**.
+ Performs an inplace update of **array**.
+
+ Examples
+ --------
+ .. code:: python
+
+ >>> import numpy as np
+ >>> from nanoutils import fill_diagonal_blocks
+
+ >>> array = np.zeros((10, 15), dtype=int)
+ >>> i = 2
+ >>> j = 3
+
+ >>> fill_diagonal_blocks(array, i, j, val=1)
+ >>> print(array)
+ [[1 1 1 0 0 0 0 0 0 0 0 0 0 0 0]
+ [1 1 1 0 0 0 0 0 0 0 0 0 0 0 0]
+ [0 0 0 1 1 1 0 0 0 0 0 0 0 0 0]
+ [0 0 0 1 1 1 0 0 0 0 0 0 0 0 0]
+ [0 0 0 0 0 0 1 1 1 0 0 0 0 0 0]
+ [0 0 0 0 0 0 1 1 1 0 0 0 0 0 0]
+ [0 0 0 0 0 0 0 0 0 1 1 1 0 0 0]
+ [0 0 0 0 0 0 0 0 0 1 1 1 0 0 0]
+ [0 0 0 0 0 0 0 0 0 0 0 0 1 1 1]
+ [0 0 0 0 0 0 0 0 0 0 0 0 1 1 1]]
+
+ Parameters
+ ----------
+ array : :class:`nump.ndarray`
+ A >= 2D NumPy array whose diagonal blocks are to be filled.
+ Gets modified in-place.
+ i : :class:`int`
+ The size of the diagonal blocks along axis -2.
+ j : :class:`int`
+ The size of the diagonal blocks along axis -1.
+ fill_value : :class:`float`
+ Value to be written on the diagonal.
+ Its type must be compatible with that of the array **a**.
+
+
+ :rtype: :data:`None`
+
+ """
+ if (j <= 0) or (i <= 0):
+ raise ValueError(f"'i' and 'j' should be larger than 0; observed values: {i} & {j}")
+
+ i0 = j0 = 0
+ dim1 = array.shape[-2]
+ while dim1 > i0:
+ array[..., i0:i0+i, j0:j0+j] = val
+ i0 += i
+ j0 += j
+
+
+__doc__ = construct_api_doc(globals())
diff --git a/nanoutils/schema.py b/nanoutils/schema.py
index ed57ad6..319aad1 100644
--- a/nanoutils/schema.py
+++ b/nanoutils/schema.py
@@ -321,7 +321,7 @@ def issubclass_factory(class_or_tuple: ClassOrTuple) -> Callable[[type], bool]:
Returns
-------
- :data:`Callable[[object], bool]`
+ :data:`Callable[[type], bool]`
A function which asserts the passed type is a subclass of **class_or_tuple**.
See Also
diff --git a/nanoutils/typing_utils.py b/nanoutils/typing_utils.py
index ee6b867..e500dbf 100644
--- a/nanoutils/typing_utils.py
+++ b/nanoutils/typing_utils.py
@@ -4,20 +4,39 @@
Index
-----
-========================= ===================================================================
-:data:`~typing.Literal` Special typing form to define literal types (a.k.a. value types).
-:data:`~typing.Final` Special typing construct to indicate final names to type checkers.
-:func:`~typing.final` A decorator to indicate final methods and final classes.
-:class:`~typing.Protocol` Base class for protocol classes.
-========================= ===================================================================
+=================================== ======================================================================================================================================
+:data:`~typing.Literal` Special typing form to define literal types (a.k.a. value types).
+:data:`~typing.Final` Special typing construct to indicate final names to type checkers.
+:func:`~typing.final` A decorator to indicate final methods and final classes.
+:class:`~typing.Protocol` Base class for protocol classes.
+:class:`~typing.SupportsIndex` An ABC with one abstract method :meth:`~SupportsIndex.__index__`.
+:class:`~typing.TypedDict` A simple typed name space. At runtime it is equivalent to a plain :class:`dict`.
+:func:`~typing.runtime_checkable` Mark a protocol class as a runtime protocol, so that it an be used with :func:`isinstance()` and :func:`issubclass()`.
+=================================== ======================================================================================================================================
-"""
+""" # noqa: E501
import sys
+from abc import abstractmethod
+from typing import Union, Iterable
if sys.version_info < (3, 8):
- from typing_extensions import Literal, Final, final, Protocol
+ from typing_extensions import Literal, Final, final, Protocol, TypedDict, runtime_checkable
+
+ @runtime_checkable
+ class SupportsIndex(Protocol):
+ """An ABC with one abstract method :meth:`__index__`."""
+
+ __slots__: Union[str, Iterable[str]] = ()
+
+ @abstractmethod
+ def __index__(self) -> int:
+ """Return **self** converted to an :class:`int`, if **self** is suitable for use as an index into a :class:`list`.""" # noqa: E501
+ pass
+
else:
- from typing import Literal, Final, final, Protocol
+ from typing import Literal, Final, final, Protocol, TypedDict, SupportsIndex, runtime_checkable
-__all__ = ['Literal', 'Final', 'final', 'Protocol']
+__all__ = [
+ 'Literal', 'Final', 'final', 'Protocol', 'SupportsIndex', 'TypedDict', 'runtime_checkable'
+]
diff --git a/nanoutils/utils.py b/nanoutils/utils.py
index cb39690..753558d 100644
--- a/nanoutils/utils.py
+++ b/nanoutils/utils.py
@@ -14,23 +14,36 @@
import importlib
from types import ModuleType
-from functools import partial
+from functools import partial, wraps
from typing import (
List,
Tuple,
Optional,
Callable,
TypeVar,
- Any,
Iterable,
Dict,
- Container
+ Container,
+ Mapping,
+ NamedTuple,
+ NoReturn,
+ MutableMapping,
+ cast,
+ overload,
+ TYPE_CHECKING
)
-from .empty import EMPTY_COLLECTION
+from .empty import EMPTY_CONTAINER
+
+if TYPE_CHECKING:
+ from .utils import VersionInfo as VersionInfoType
+else:
+ VersionInfoType = 'nanoutils.VersionInfo'
__all__ = [
- 'PartialPrepend', 'group_by_values', 'get_importable', 'set_docstring', 'construct_api_doc'
+ 'PartialPrepend', 'VersionInfo',
+ 'group_by_values', 'get_importable', 'set_docstring', 'construct_api_doc', 'raise_if',
+ 'split_dict'
]
T = TypeVar('T')
@@ -160,7 +173,25 @@ def get_importable(string: str, validate: Optional[Callable[[T], bool]] = None)
class PartialPrepend(partial):
- """A :func:`~functools.partial` subclass where the ``*args`` are appended rather than prepended.""" # noqa: E501
+ """A :func:`~functools.partial` subclass where the ``*args`` are appended rather than prepended.
+
+ Examples
+ --------
+ .. code:: python
+
+ >>> from functools import partial
+ >>> from nanoutils import PartialPrepend
+
+ >>> func1 = partial(isinstance, 1) # isinstance(1, ...)
+ >>> func2 = PartialPrepend(isinstance, float) # isinstance(..., float)
+
+ >>> func1(int) # isinstance(1, int)
+ True
+
+ >>> func2(1.0) # isinstance(1.0, float)
+ True
+
+ """ # noqa: E501
def __call__(self, *args, **keywords):
"""Call and return :attr:`~PartialReversed.func`."""
@@ -168,8 +199,158 @@ def __call__(self, *args, **keywords):
return self.func(*args, *self.args, **keywords)
+def split_dict(dct: MutableMapping[KT, VT], keep_keys: Iterable[KT],
+ keep_order: bool = True) -> Dict[KT, VT]:
+ """Pop all items from **dct** which are not in **keep_keys** and use them to construct a new dictionary.
+
+ Note that, by popping its keys, the passed **dct** will also be modified inplace.
+
+ Examples
+ --------
+ .. code:: python
+
+ >>> from nanoutils import split_dict
+
+ >>> dict1 = {1: 'a', 2: 'b', 3: 'c', 4: 'd'}
+ >>> dict2 = split_dict(dict1, keep_keys={1, 2})
+
+ >>> print(dict1)
+ {1: 'a', 2: 'b'}
+
+ >>> print(dict2)
+ {3: 'c', 4: 'd'}
+
+ Parameters
+ ----------
+ dct : :class:`MutableMapping[KT, VT]`
+ A mutable mapping.
+ keep_keys : :class:`Iterable[KT]`
+ An iterable with keys that should remain in **dct**.
+ keep_order : :class:`bool`
+ If :data:`True`, preserve the order of the items in **dct**.
+ Note that :code:`keep_order = False` is generally faster.
+
+ Returns
+ -------
+ :class:`Dict[KT, VT]`
+ A new dictionaries with all key/value pairs from **dct** not specified in **keep_keys**.
+
+ """ # noqa: E501
+ # The ordering of dict elements is preserved in this manner,
+ # as opposed to the use of set.difference()
+ if keep_order:
+ difference: Iterable[KT] = [k for k in dct if k not in keep_keys]
+ else:
+ difference = set(dct.keys()).difference(keep_keys)
+
+ return {k: dct.pop(k) for k in difference}
+
+
+@overload
+def raise_if(ex: None) -> Callable[[FT], FT]:
+ ...
+@overload # noqa: E302
+def raise_if(ex: BaseException) -> Callable[[Callable], Callable[..., NoReturn]]:
+ ...
+def raise_if(ex: Optional[BaseException]) -> Callable: # noqa: E302
+ """A decorator which raises the passed exception whenever calling the decorated function.
+
+ Examples
+ --------
+ .. code:: python
+
+ >>> from nanoutils import raise_if
+
+ >>> ex1 = None
+ >>> ex2 = TypeError("This is an exception")
+
+ >>> @raise_if(ex1)
+ ... def func1() -> bool:
+ ... return True
+
+ >>> @raise_if(ex2)
+ ... def func2() -> bool:
+ ... return True
+
+ >>> func1()
+ True
+
+ >>> func2()
+ Traceback (most recent call last):
+ ...
+ TypeError: This is an exception
+
+
+ Parameters
+ ----------
+ ex : :exc:`BaseException`, optional
+ An exception.
+ If :data:`None` is passed then the decorated function will be called as usual.
+
+ """
+ if ex is None:
+ def decorator(func: FT):
+ return func
+
+ elif isinstance(ex, BaseException):
+ def decorator(func: FT):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ raise ex
+ return wrapper
+
+ else:
+ raise TypeError(f"{ex.__class__.__name__!r}")
+ return decorator
+
+
+class VersionInfo(NamedTuple):
+ """A :func:`~collections.namedtuple` representing the version of a package.
+
+ Examples
+ --------
+ .. code:: python
+
+ >>> from nanoutils import VersionInfo
+
+ >>> version = '0.8.2'
+ >>> VersionInfo.from_str(version)
+ VersionInfo(major=0, minor=8, micro=2)
+
+ """
+
+ #: :class:`int`: The semantic_ major version.
+ major: int
+
+ #: :class:`int`: The semantic_ minor version.
+ minor: int
+
+ #: :class:`int`: The semantic_ micro version (a.k.a. :attr:`VersionInfo.patch`).
+ micro: int
+
+ @property
+ def patch(self) -> int:
+ """An alias for :attr:`VersionInfo.micro`."""
+ return self.micro
+
+ @classmethod
+ def from_str(cls, version: str) -> VersionInfoType:
+ """Construct a :class:`VersionInfo` from a string; *e.g.* :code:`version = "0.8.2"`."""
+ if not isinstance(version, str):
+ cls_name = version.__class__.__name__
+ raise TypeError(f"'version' expected a string; observed type: {cls_name!r}")
+
+ try:
+ major, minor, micro = (int(i) for i in version.split('.'))
+ except (ValueError, TypeError) as ex:
+ raise ValueError("'version' expected a string consisting of three '.'-separated "
+ f"integers (e.g. '0.8.2'); observed value: {version!r}") from ex
+ return cls(major=major, minor=minor, micro=micro)
+
+
def _get_directive(obj: object, name: str,
- decorators: Container[str] = EMPTY_COLLECTION) -> str:
+ decorators: Container[str] = EMPTY_CONTAINER) -> str:
+ """A helper function for :func:`~nanoutils.construct_api_doc`."""
if isinstance(obj, type):
if issubclass(obj, BaseException):
return f'.. autoexception:: {name}'
@@ -187,11 +368,75 @@ def _get_directive(obj: object, name: str,
return f'.. autodata:: {name}'
-def construct_api_doc(glob_dict: Dict[str, Any],
- decorators: Container[str] = EMPTY_COLLECTION) -> str:
- """A helper function for updating **Nano-Utils** docstrings."""
- __doc__ = glob_dict['__doc__']
- __all__ = glob_dict['__all__']
+def construct_api_doc(glob_dict: Mapping[str, object],
+ decorators: Container[str] = EMPTY_CONTAINER) -> str:
+ '''Format a **Nano-Utils** module-level docstring.
+
+ Examples
+ --------
+ .. code:: python
+
+ >>> __doc__ = """
+ ... Index
+ ... -----
+ ... .. autosummary::
+ ... {autosummary}
+ ...
+ ... API
+ ... ---
+ ... {autofunction}
+ ...
+ ... """
+
+ >>> from nanoutils import construct_api_doc
+
+ >>> __all__ = ['obj', 'func', 'Class']
+
+ >>> obj = ...
+
+ >>> def func(obj: object) -> None:
+ ... pass
+
+ >>> class Class(object):
+ ... pass
+
+ >>> doc = construct_api_doc(locals())
+ >>> print(doc)
+
+ Index
+ -----
+ .. autosummary::
+ obj
+ func
+ Class
+
+ API
+ ---
+ .. autodata:: obj
+ .. autofunction:: func
+ .. autoclass:: Class
+ :members:
+
+
+
+ Parameters
+ ----------
+ glob_dict : :class:`Mapping[str, object]`
+ A mapping containg a module-level namespace.
+ Note that the mapping *must* contain the ``"__doc__"`` and ``"__all__"`` keys.
+
+ decorators : :class:`Container[str]`
+ A container with the names of all decorators.
+ If not specified, all functions will use the Sphinx ``autofunction`` domain.
+
+ Returns
+ -------
+ :class:`str`
+ The formatted string.
+
+ '''
+ __doc__ = cast(str, glob_dict['__doc__'])
+ __all__ = cast(List[str], glob_dict['__all__'])
return __doc__.format(
autosummary='\n'.join(f' {i}' for i in __all__),
@@ -199,4 +444,4 @@ def construct_api_doc(glob_dict: Dict[str, Any],
)
-__doc__ = construct_api_doc(globals(), decorators={'set_docstring'})
+__doc__ = construct_api_doc(globals(), decorators={'set_docstring', 'raise_if'})
diff --git a/setup.py b/setup.py
index a3de28d..0aaf1bc 100644
--- a/setup.py
+++ b/setup.py
@@ -13,8 +13,8 @@
with open(version_path, encoding='utf-8') as f:
exec(f.read(), version)
-with open('README.rst') as readme_file:
- readme = readme_file.read()
+with open('README.rst', encoding='utf-8') as f:
+ readme = f.read()
# Requirements for building the documentation
docs_require = [
@@ -26,9 +26,11 @@
tests_require = [
'assertionlib',
'schema',
+ 'numpy',
'pytest>=4.1.0',
'pytest-cov',
'pytest-flake8>=1.0.5',
+ 'pydocstyle>=5.0.0',
'pytest-pydocstyle>=2.1',
'typing-extensions>=3.7.4; python_version<"3.8"',
'pytest-mypy>=0.6.2'
@@ -54,7 +56,8 @@
'python-3',
'python-3-6',
'python-3-7',
- 'python-3-8'
+ 'python-3-8',
+ 'libraries'
],
classifiers=[
'Development Status :: 3 - Alpha',