Skip to content

Commit 9511f17

Browse files
authored
Nano-Utils 0.3.0 (#4)
* Added the ``SetAttr`` context manager. * Updated the development status from alpha to beta.
1 parent ca488dc commit 9511f17

14 files changed

+313
-8
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ All notable changes to this project will be documented in this file.
66
This project adheres to `Semantic Versioning <http://semver.org/>`_.
77

88

9+
0.3.0
10+
*****
11+
* Added the ``SetAttr`` context manager.
12+
* Updated the development status from alpha to beta.
13+
14+
915
0.2.0
1016
*****
1117
* Added new NumPy-specific functions: ``as_nd_array()``, ``array_combinations()`` & ``fill_diagonal_blocks()``.

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121

2222
################
23-
Nano-Utils 0.2.0
23+
Nano-Utils 0.3.0
2424
################
2525
Utility functions used throughout the various nlesc-nano repositories.
2626

docs/0_documentation.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ API
66
3_schema.rst
77
4_typing.rst
88
5_numpy.rst
9+
6_set_attr.rst

docs/6_set_attr.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
nanoutils.set_attr
2+
==================
3+
.. automodule:: nanoutils.set_attr

nanoutils/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
"""Nano-Utils."""
1+
"""The **Nano-Utils** package."""
22

33
# flake8: noqa: F403,F401
44

55
from .__version__ import __version__
66

7-
from . import typing_utils, empty, utils, numpy_utils, schema
7+
from . import typing_utils, empty, utils, numpy_utils, schema, set_attr
88
from .typing_utils import *
99
from .empty import *
1010
from .utils import *
1111
from .numpy_utils import *
12+
from .set_attr import *
1213
from .schema import (
1314
Default, Formatter, supports_float, supports_int,
1415
isinstance_factory, issubclass_factory, import_factory
@@ -24,3 +25,4 @@
2425
__all__ += utils.__all__
2526
__all__ += numpy_utils.__all__
2627
__all__ += schema.__all__
28+
__all__ += set_attr.__all__

nanoutils/__version__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
"""The Nano-Utils version."""
1+
"""The **Nano-Utils** version."""
22

3-
__version__ = '0.2.0'
3+
__version__ = '0.3.0'

nanoutils/set_attr.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""A module for containing the :class:`SetAttr` class.
2+
3+
Index
4+
-----
5+
.. currentmodule:: nanoutils
6+
.. autosummary::
7+
{autosummary}
8+
9+
API
10+
---
11+
{autofunction}
12+
13+
"""
14+
15+
import reprlib
16+
from types import TracebackType
17+
from typing import Generic, TypeVar, NoReturn, Dict, Any, Optional, Type
18+
from threading import RLock
19+
20+
from .utils import construct_api_doc
21+
22+
__all__ = ['SetAttr']
23+
24+
T1 = TypeVar('T1')
25+
T2 = TypeVar('T2')
26+
ST = TypeVar('ST', bound='SetAttr')
27+
28+
29+
class SetAttr(Generic[T1, T2]):
30+
"""A context manager for temporarily changing an attribute's value.
31+
32+
The :class:`SetAttr` context manager is thread-safe, reusable and reentrant.
33+
34+
Warnings
35+
--------
36+
Note that while :meth:`SetAttr.__enter__` and :meth:`SetAttr.__exit__` are thread-safe,
37+
the same does *not* hold for :meth:`SetAttr.__init__`.
38+
39+
Examples
40+
--------
41+
.. code:: python
42+
43+
>>> from nanoutils import SetAttr
44+
45+
>>> class Test:
46+
... a = False
47+
48+
>>> print(Test.a)
49+
False
50+
51+
>>> set_attr = SetAttr(Test, 'a', True)
52+
>>> with set_attr:
53+
... print(Test.a)
54+
True
55+
56+
"""
57+
58+
__slots__ = ('__weakref__', '_obj', '_name', '_value', '_value_old', '_lock', '_hash')
59+
60+
@property
61+
def obj(self) -> T1:
62+
""":class:`T1<typing.TypeVar>`: The to-be modified object."""
63+
return self._obj
64+
65+
@property
66+
def name(self) -> str:
67+
""":class:`str`: The name of the to-be modified attribute."""
68+
return self._name
69+
70+
@property
71+
def value(self) -> T2:
72+
""":class:`T2<typing.TypeVar>`: The value to-be assigned to the :attr:`name` attribute of :attr:`SetAttr.obj`.""" # noqa: E501
73+
return self._value
74+
75+
@property
76+
def attr(self) -> T2:
77+
""":class:`T2<typing.TypeVar>`: Get or set the :attr:`~SetAttr.name` attribute of :attr:`SetAttr.obj`.""" # noqa: E501
78+
return getattr(self.obj, self.name)
79+
80+
@attr.setter
81+
def attr(self, value: T2) -> None:
82+
with self._lock:
83+
setattr(self.obj, self.name, value)
84+
85+
def __init__(self, obj: T1, name: str, value: T2) -> None:
86+
"""Initialize the :class:`SetAttr` context manager.
87+
88+
Parameters
89+
----------
90+
obj : :class:`T1<typing.TypeVar>`
91+
The to-be modified object.
92+
name : :class:`str`
93+
The name of the to-be modified attribute.
94+
value : :class:`T2<typing.TypeVar>`
95+
The value to-be assigned to the **name** attribute of **obj**.
96+
97+
98+
:rtype: :data:`None`
99+
100+
"""
101+
self._obj = obj
102+
self._name = name
103+
self._value = value
104+
105+
self._value_old = self.attr
106+
self._lock = RLock()
107+
108+
@reprlib.recursive_repr()
109+
def __repr__(self) -> str:
110+
"""Implement :func:`str(self)<str>` and :func:`repr(self)<repr>`."""
111+
obj = object.__repr__(self.obj)
112+
value = reprlib.repr(self.value)
113+
return f'{self.__class__.__name__}(obj={obj}, name={self.name!r}, value={value})'
114+
115+
def __eq__(self, value: object) -> bool:
116+
"""Implement :func:`self == value<object.__eq__>`."""
117+
if type(self) is not type(value):
118+
return False
119+
return self.obj is value.obj and self.name == value.name and self.value == value.value # type: ignore # noqa: E501
120+
121+
def __reduce__(self) -> NoReturn:
122+
"""A Helper function for :mod:`pickle`.
123+
124+
Warnings
125+
--------
126+
Unsupported operation, raises a :exc:`TypeError`.
127+
128+
"""
129+
raise TypeError(f"can't pickle {self.__class__.__name__} objects")
130+
131+
def __copy__(self: ST) -> ST:
132+
"""Implement :func:`copy.copy(self)<copy.copy>`."""
133+
return self
134+
135+
def __deepcopy__(self: ST, memo: Optional[Dict[int, Any]] = None) -> ST:
136+
"""Implement :func:`copy.deepcopy(self, memo=memo)<copy.deepcopy>`."""
137+
return self
138+
139+
def __hash__(self) -> int:
140+
"""Implement :func:`hash(self)<hash>`.
141+
142+
Warnings
143+
--------
144+
A :exc:`TypeError` will be raised if :attr:`SetAttr.value` is not hashable.
145+
146+
"""
147+
try:
148+
return self._hash
149+
except AttributeError:
150+
args = (type(self), (id(self.obj), self.name, self.value))
151+
self._hash: int = hash(args)
152+
return self._hash
153+
154+
def __enter__(self) -> None:
155+
"""Enter the context manager, modify :attr:`SetAttr.obj`."""
156+
self.attr = self.value
157+
158+
def __exit__(self, exc_type: Optional[Type[BaseException]],
159+
exc_value: Optional[BaseException],
160+
traceback: Optional[TracebackType]) -> None:
161+
"""Exit the context manager, restore :attr:`SetAttr.obj`."""
162+
self.attr = self._value_old
163+
164+
165+
__doc__ = construct_api_doc(globals())

nanoutils/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,10 @@ def split_dict(dct: MutableMapping[KT, VT], keep_keys: Iterable[KT],
241241
if keep_order:
242242
difference: Iterable[KT] = [k for k in dct if k not in keep_keys]
243243
else:
244-
difference = set(dct.keys()).difference(keep_keys)
244+
try:
245+
difference = dct.keys() - keep_keys # type: ignore
246+
except (TypeError, AttributeError):
247+
difference = set(dct.keys()).difference(keep_keys)
245248

246249
return {k: dct.pop(k) for k in difference}
247250

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
'libraries'
6161
],
6262
classifiers=[
63-
'Development Status :: 3 - Alpha',
63+
'Development Status :: 4 - Beta',
6464
'Intended Audience :: Developers',
6565
'License :: OSI Approved :: Apache Software License',
6666
'Natural Language :: English',

tests/test_empty.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Tests for :mod:`nanoutils.empty`."""
2+
3+
from collections import abc
4+
5+
from nanoutils import EMPTY_SEQUENCE, EMPTY_MAPPING, EMPTY_COLLECTION, EMPTY_SET, EMPTY_CONTAINER
6+
from assertionlib import assertion
7+
8+
9+
def test_empty() -> None:
10+
"""Tests for :mod:`nanoutils.empty`."""
11+
assertion.isinstance(EMPTY_SEQUENCE, abc.Sequence)
12+
assertion.isinstance(EMPTY_MAPPING, abc.Mapping)
13+
assertion.isinstance(EMPTY_COLLECTION, abc.Collection)
14+
assertion.isinstance(EMPTY_SET, abc.Set)
15+
assertion.isinstance(EMPTY_CONTAINER, abc.Container)

tests/test_set_attr.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Tests for :mod:`nanoutils.set_attr`."""
2+
3+
import copy
4+
import reprlib
5+
from assertionlib import assertion
6+
7+
from nanoutils import SetAttr
8+
9+
10+
class _Test:
11+
a: bool = True
12+
13+
14+
OBJ = SetAttr(_Test, 'a', False)
15+
16+
17+
def test_setattr() -> None:
18+
"""Test :class:`~nanoutils.SetAttr`."""
19+
assertion.is_(OBJ.obj, _Test)
20+
assertion.eq(OBJ.name, 'a')
21+
assertion.is_(OBJ.value, False)
22+
23+
assertion.is_(OBJ.attr, _Test.a)
24+
try:
25+
OBJ.attr = False
26+
assertion.is_(OBJ.attr, False)
27+
finally:
28+
OBJ.attr = True
29+
30+
assertion.contains(repr(OBJ), SetAttr.__name__)
31+
assertion.contains(repr(OBJ), object.__repr__(OBJ.obj))
32+
assertion.contains(repr(OBJ), reprlib.repr(OBJ.value))
33+
assertion.contains(repr(OBJ), 'a')
34+
35+
obj2 = SetAttr(_Test, 'a', False)
36+
obj3 = SetAttr(_Test, 'a', True)
37+
assertion.eq(OBJ, obj2)
38+
assertion.ne(OBJ, obj3)
39+
assertion.ne(OBJ, 0)
40+
41+
assertion.assert_(OBJ.__reduce__, exception=TypeError)
42+
assertion.is_(copy.copy(OBJ), OBJ)
43+
assertion.is_(copy.deepcopy(OBJ), OBJ)
44+
assertion.truth(hash(OBJ))
45+
assertion.eq(hash(OBJ), OBJ._hash)
46+
47+
with OBJ:
48+
assertion.is_(_Test.a, False)
49+
assertion.is_(_Test.a, True)
File renamed without changes.

tests/test_typing.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Tests for :mod:`nanoutils.typing_utils`."""
2+
3+
import sys
4+
5+
from assertionlib import assertion
6+
from nanoutils import Literal, Final, final, Protocol, TypedDict, SupportsIndex, runtime_checkable
7+
8+
if sys.version_info < (3, 8):
9+
import typing_extensions as t
10+
else:
11+
import typing as t
12+
13+
14+
def test_typing() -> None:
15+
"""Tests for :mod:`nanoutils.typing_utils`."""
16+
assertion.is_(Literal, t.Literal)
17+
assertion.is_(Final, t.Final)
18+
assertion.is_(final, t.final)
19+
assertion.is_(Protocol, t.Protocol)
20+
assertion.is_(TypedDict, t.TypedDict)
21+
assertion.is_(runtime_checkable, t.runtime_checkable)
22+
23+
if sys.version_info >= (3, 8):
24+
assertion.is_(SupportsIndex, t.SupportsIndex)
25+
else:
26+
assertion.contains(SupportsIndex.__bases__, Protocol)
27+
assertion.hasattr(SupportsIndex, '__index__')

tests/test_utils.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Tests for :mod:`nanoutils.utils`."""
22

3+
from inspect import isclass
4+
from functools import partial
5+
36
from assertionlib import assertion
4-
from nanoutils import set_docstring
7+
from nanoutils import set_docstring, get_importable, VersionInfo, version_info
58

69

710
def test_set_docstring() -> None:
@@ -17,3 +20,34 @@ def func2():
1720

1821
assertion.eq(func1.__doc__, 'TEST')
1922
assertion.eq(func2.__doc__, None)
23+
24+
25+
def test_get_importable() -> None:
26+
"""Tests for :func:`nanoutils.get_importable`."""
27+
class Test:
28+
split = True
29+
30+
assertion.is_(get_importable('builtins.dict'), dict)
31+
assertion.is_(get_importable('builtins.dict', validate=isclass), dict)
32+
33+
assertion.assert_(get_importable, None, exception=TypeError)
34+
assertion.assert_(get_importable, Test, exception=TypeError)
35+
assertion.assert_(get_importable, 'builtins.len', validate=isclass, exception=RuntimeError)
36+
assertion.assert_(get_importable, 'builtins.len', validate=partial(isclass),
37+
exception=RuntimeError)
38+
39+
40+
def test_version_info() -> None:
41+
"""Tests for :func:`nanoutils.VersionInfo`."""
42+
tup1 = VersionInfo(0, 1, 2)
43+
tup2 = VersionInfo.from_str('0.1.2')
44+
assertion.eq(tup1, (0, 1, 2))
45+
assertion.eq(tup2, (0, 1, 2))
46+
47+
assertion.eq(tup1.micro, tup1.patch)
48+
49+
assertion.assert_(VersionInfo.from_str, b'0.1.2', exception=TypeError)
50+
assertion.assert_(VersionInfo.from_str, '0.1.2a', exception=ValueError)
51+
assertion.assert_(VersionInfo.from_str, '0.1.2.3.4', exception=ValueError)
52+
53+
assertion.isinstance(version_info, VersionInfo)

0 commit comments

Comments
 (0)