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',