Skip to content

Commit

Permalink
Merge pull request #34 from nlesc-nano/key
Browse files Browse the repository at this point in the history
ENH,REL: Nano-Utils 2.3.0
  • Loading branch information
BvB93 authored Oct 16, 2021
2 parents 44513ce + adcbcd8 commit 32e3a9d
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 38 deletions.
30 changes: 14 additions & 16 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,15 @@ jobs:

- name: Install dependencies
shell: bash
env:
SPECIAL: ${{ matrix.special }}
run: |
if [[ $SPECIAL == '; no-optional' ]]; then
pip install -e .[test_no_optional]
elif [[ $SPECIAL == '; pre-release' ]]; then
pip install --pre -e .[test] --upgrade --force-reinstall
else
pip install -e .[test]
fi
case "${{ matrix.special }}" in
"; no-optional")
pip install -e .[test_no_optional] ;;
"; pre-release")
pip install --pre -e .[test] --upgrade --force-reinstall ;;
*)
pip install -e .[test] ;;
esac
- name: Python info
run: |
Expand All @@ -65,14 +64,13 @@ jobs:

- name: Test with pytest
shell: bash
env:
SPECIAL: ${{ matrix.special }}
run: |
if [[ $SPECIAL == '; no-optional' ]]; then
pytest --mypy
else
pytest --mypy --doctest-modules
fi
case "${{ matrix.special }}" in
"; no-optional")
pytest --mypy ;;
*)
pytest --mypy --doctest-modules ;;
esac
- name: Run codecov
uses: codecov/codecov-action@v2
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ 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 ``UserMapping`` entry points for the IPython key completioner
and pretty printer.
* Added a decorator for applying the effect of ``warnings.filterwarnings``
to the decorated function.


2.2.0
*****
* Added a decorator for constructing positional-only signatures.
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@


################
Nano-Utils 2.2.0
Nano-Utils 2.3.0
################
Utility functions used throughout the various nlesc-nano repositories.

Expand Down
2 changes: 1 addition & 1 deletion nanoutils/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""The **Nano-Utils** version."""

__version__ = '2.2.0'
__version__ = '2.3.0'
40 changes: 32 additions & 8 deletions nanoutils/_user_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
from .utils import positional_only
from .typing_utils import Protocol, runtime_checkable

if TYPE_CHECKING:
from IPython.lib.pretty import RepresentationPrinter

class _ReprFunc(Protocol[_KT, _VT]):
def __call__(self, __dct: dict[_KT, _VT], *, width: int) -> str: ...

__all__ = ["UserMapping", "MutableUserMapping", "_DictLike", "_SupportsKeysAndGetItem"]

_SENTINEL = object()
Expand Down Expand Up @@ -62,6 +68,17 @@ def __getitem__(self, __key: _KT) -> _VT_co:
]


def _repr_func(self: UserMapping[_KT, _VT], func: _ReprFunc[_KT, _VT]) -> str:
"""Helper function for :meth:`UserMapping.__repr__`."""
cls = type(self)
dict_repr = func(self._dict, width=76)
if len(dict_repr) <= 76:
return f"{cls.__name__}({dict_repr})"
else:
dict_repr2 = textwrap.indent(dict_repr[1:-1], 3 * " ")
return f"{cls.__name__}({{\n {dict_repr2},\n}})"


class UserMapping(Mapping[_KT, _VT_co]):
"""Base class for user-defined immutable mappings."""

Expand Down Expand Up @@ -104,14 +121,21 @@ def copy(self: _ST1) -> _ST1:
@reprlib.recursive_repr(fillvalue='...')
def __repr__(self) -> str:
"""Implement :func:`repr(self) <repr>`."""
cls = type(self)
width = 80 - 2 - len(cls.__name__)
dct_repr = pformat(self._dict, width=width)
if len(dct_repr) <= width:
return f"{cls.__name__}({dct_repr})"
else:
dct_repr2 = textwrap.indent(dct_repr[1:-1], 3 * " ")
return f"{cls.__name__}({{\n {dct_repr2},\n}})"
return _repr_func(self, func=pformat)

def _repr_pretty_(self, p: RepresentationPrinter, cycle: bool) -> None:
"""Entry point for the :mod:`IPython <IPython.lib.pretty>` pretty printer."""
if cycle:
p.text(f"{type(self).__name__}(...)")
return None

from IPython.lib.pretty import pretty
string = _repr_func(self, func=lambda dct, width: pretty(dct, max_width=width))
p.text(string)

def _ipython_key_completions_(self) -> KeysView[_KT]:
"""Entry point for the IPython key completioner."""
return self.keys()

def __hash__(self) -> int:
"""Implement :func:`hash(self) <hash>`.
Expand Down
76 changes: 74 additions & 2 deletions nanoutils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import warnings
import importlib
import inspect
import functools
from types import ModuleType
from functools import wraps
from typing import (
Expand All @@ -34,9 +35,10 @@
MutableMapping,
Collection,
cast,
overload
overload,
)

from .typing_utils import Literal
from .empty import EMPTY_CONTAINER

__all__ = [
Expand All @@ -58,6 +60,7 @@
'positional_only',
'UserMapping',
'MutableUserMapping',
'warning_filter',
]

_T = TypeVar('_T')
Expand Down Expand Up @@ -737,6 +740,75 @@ def positional_only(func: _FT) -> _FT:
return func


def warning_filter(
action: Literal["default", "error", "ignore", "always", "module", "once"],
message: str = "",
category: type[Warning] = Warning,
module: str = "",
lineno: int = 0,
append: bool = False,
) -> Callable[[_FT], _FT]:
"""A decorator for wrapping function calls with :func:`warnings.filterwarnings`.
Examples
--------
.. code-block:: python
>>> from nanoutils import warning_filter
>>> import warnings
>>> @warning_filter("error", category=UserWarning)
... def func():
... warnings.warn("test", UserWarning)
>>> func()
Traceback (most recent call last):
...
UserWarning: test
Parameters
----------
action : :class:`str`
One of the following strings:
* ``"default"``: Print the first occurrence of matching warnings for each location (module + line number) where the warning is issued
* ``"error"``: Turn matching warnings into exceptions
* ``"ignore"``: Never print matching warnings
* ``"always"``: Always print matching warnings
* ``"module"``: Print the first occurrence of matching warnings for each module where the warning is issued (regardless of line number)
* ``"once"``: Print only the first occurrence of matching warnings, regardless of location
message : :class:`str`, optional
A string containing a regular expression that the start of the warning message must match.
The expression is compiled to always be case-insensitive.
category : :class:`type[Warning] <type>`
The to-be affected :class:`Warning` (sub-)class.
module : :class:`str`, optional
A string containing a regular expression that the module name must match.
The expression is compiled to be case-sensitive.
lineno : :class:`int`
An integer that the line number where the warning occurred must match,
or 0 to match all line numbers.
append : :class:`bool`
Whether the warning entry is inserted at the end.
See Also
--------
:func:`warnings.filterwarnings` :
Insert a simple entry into the list of warnings filters (at the front).
"""
def decorator(func: _FT) -> _FT:
@functools.wraps(func)
def wrapper(*args, **kwargs):
with warnings.catch_warnings():
warnings.filterwarnings(action, message, category, module, lineno, append)
ret = func(*args, **kwargs)
return ret
return cast(_FT, wrapper)
return decorator


# Move to the end to reduce the risk of circular imports
from ._partial import PartialPrepend
from ._set_attr import SetAttr
Expand All @@ -747,5 +819,5 @@ def positional_only(func: _FT) -> _FT:

__doc__ = construct_api_doc(
globals(),
decorators={'set_docstring', 'raise_if', 'ignore_if', 'positional_only'},
decorators={'set_docstring', 'raise_if', 'ignore_if', 'positional_only', 'warning_filter'},
)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
'pyyaml',
'h5py',
'numpy',
'ipython',
]
tests_require += docs_require
tests_require += build_requires
Expand Down
25 changes: 19 additions & 6 deletions tests/test_dtype_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
import textwrap
from typing import TYPE_CHECKING, no_type_check
from collections.abc import Iterator
from collections.abc import Iterator, Callable

import pytest
from assertionlib import assertion
Expand All @@ -18,6 +18,14 @@
import numpy.typing as npt
import _pytest

try:
from IPython.lib.pretty import pretty
except ModuleNotFoundError:
IPYTHON: bool = False
pretty = NotImplemented
else:
IPYTHON = True


class BasicMapping:
def __init__(self, dct: dict[str, npt.DTypeLike]) -> None:
Expand Down Expand Up @@ -128,16 +136,21 @@ def test_repr(self, obj: DTypeMapping) -> None:
)""").strip()
assertion.str_eq(obj, string1, str_converter=repr)

string2 = textwrap.dedent(f"""
string2 = f"{type(obj).__name__}()"
assertion.str_eq(type(obj)(), string2, str_converter=repr)

@pytest.mark.parametrize("str_func", [
str,
pytest.param(pretty, marks=pytest.mark.skipif(not IPYTHON, reason="Requires IPython")),
], ids=["str", "pretty"])
def test_str(self, obj: DTypeMapping, str_func: Callable[[object], str]) -> None:
string = textwrap.dedent(f"""
{type(obj).__name__}(
a = int64,
b = float64,
c = <U5,
)""").strip()
assertion.str_eq(obj, string2, str_converter=str)

string3 = f"{type(obj).__name__}()"
assertion.str_eq(type(obj)(), string3, str_converter=repr)
assertion.str_eq(obj, string, str_converter=str_func)

@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires python >= 3.9")
@no_type_check
Expand Down
30 changes: 26 additions & 4 deletions tests/test_user_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import textwrap
import string
from typing import TYPE_CHECKING, no_type_check
from collections.abc import KeysView, ValuesView, ItemsView, Iterator
from collections.abc import KeysView, ValuesView, ItemsView, Iterator, Callable

import pytest
from assertionlib import assertion
Expand All @@ -17,6 +17,14 @@
if TYPE_CHECKING:
import _pytest

try:
from IPython.lib.pretty import pretty
except ModuleNotFoundError:
IPYTHON: bool = False
pretty = NotImplemented
else:
IPYTHON = True


class BasicMapping:
def __init__(self, dct: dict[str, int]) -> None:
Expand Down Expand Up @@ -75,9 +83,14 @@ def test_eq(self, obj: UserMapping[str, int]) -> None:
def test_getitem(self, obj: UserMapping[str, int], key: str, value: int) -> None:
assertion.eq(obj[key], value)

def test_repr(self, obj: UserMapping[str, int]) -> None:
@pytest.mark.parametrize("str_func", [
str,
repr,
pytest.param(pretty, marks=pytest.mark.skipif(not IPYTHON, reason="Requires IPython")),
], ids=["str", "repr", "pretty"])
def test_repr(self, obj: UserMapping[str, int], str_func: Callable[[object], str]) -> None:
string1 = f"{type(obj).__name__}({{'a': 0, 'b': 1, 'c': 2}})"
assertion.str_eq(obj, string1)
assertion.str_eq(obj, string1, str_converter=str_func)

cls = type(obj)
ref2 = cls(zip(string.ascii_lowercase[:12], range(12)))
Expand All @@ -97,7 +110,12 @@ def test_repr(self, obj: UserMapping[str, int]) -> None:
'l': 11,
}})
""").strip()
assertion.str_eq(ref2, string2)
assertion.str_eq(ref2, string2, str_converter=str_func)

@pytest.mark.skipif(not IPYTHON, reason="Rquires IPython")
def test_pretty_repr(self, obj: UserMapping[str, int]) -> None:
string1 = f"{type(obj).__name__}({{'a': 0, 'b': 1, 'c': 2}})"
assertion.str_eq(obj, string1, str_converter=pretty)

def test_hash(self, obj: UserMapping[str, int]) -> None:
if isinstance(obj, MutableUserMapping):
Expand Down Expand Up @@ -134,6 +152,10 @@ def test_fromkeys(self, obj: UserMapping[str, int]) -> None:
assertion.isinstance(dct, cls)
assertion.eq(dct.keys(), obj.keys())

def test_key_completions(self, obj: UserMapping[str, int]) -> None:
assertion.isinstance(obj._ipython_key_completions_(), KeysView)
assertion.eq(obj._ipython_key_completions_(), obj.keys())

def test_get(self, obj: UserMapping[str, int]) -> None:
assertion.eq(obj.get("a"), 0)
assertion.is_(obj.get("d"), None)
Expand Down

0 comments on commit 32e3a9d

Please sign in to comment.