Skip to content

Commit d8549bd

Browse files
authored
Merge pull request #35 from nlesc-nano/version
ENH: Allow `VersionInfo.from_str` to accept any PEP 440-compatible version string
2 parents 32e3a9d + 7906796 commit d8549bd

15 files changed

+115
-51
lines changed

.github/workflows/pythonpackage.yml

+11-11
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,12 @@ jobs:
1717
fail-fast: false
1818
matrix:
1919
os: [ubuntu-latest, macos-latest, windows-latest]
20-
version: [3.7, 3.8, 3.9]
20+
version: ['3.7', '3.8', '3.9', '3.10']
2121
special: ['']
2222
include:
2323
- os: ubuntu-latest
2424
special: '; pre-release'
25-
version: 3.9
26-
- os: ubuntu-latest
27-
special: '; no-optional'
28-
version: 3.9
25+
version: '3.10'
2926
- os: ubuntu-latest
3027
special: '; no-optional'
3128
version: '3.10'
@@ -67,9 +64,9 @@ jobs:
6764
run: |
6865
case "${{ matrix.special }}" in
6966
"; no-optional")
70-
pytest --mypy ;;
67+
pytest ;;
7168
*)
72-
pytest --mypy --doctest-modules ;;
69+
pytest --doctest-modules ;;
7370
esac
7471
7572
- name: Run codecov
@@ -89,7 +86,9 @@ jobs:
8986
python-version: 3.9
9087

9188
- name: Install linters
92-
run: pip install flake8 pydocstyle>=5.0.0
89+
run: |
90+
pip install flake8 pydocstyle>=5.0.0
91+
pip install mypy types-PyYAML "assertionlib>=3.2.1" numpy
9392
9493
- name: Python info
9594
run: |
@@ -101,8 +100,9 @@ jobs:
101100

102101
- name: Run flake8
103102
run: flake8 nanoutils docs tests
104-
continue-on-error: true
105103

106104
- name: Run pydocstyle
107-
run: pydocstyle nanoutils docs tests
108-
continue-on-error: true
105+
run: pydocstyle nanoutils
106+
107+
- name: Run mypy
108+
run: mypy nanoutils

CHANGELOG.rst

+6
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+
2.3.1
10+
*****
11+
* Allow ``VersionInfo.from_str`` to take any pep 440-compatible version string.
12+
* Add ``VersionInfo.bug`` and ``VersionInfo.maintenance`` as aliases for ``VersionInfo.micro``.
13+
14+
915
2.3.0
1016
*****
1117
* Added ``UserMapping`` entry points for the IPython key completioner

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
################
25-
Nano-Utils 2.3.0
25+
Nano-Utils 2.3.1
2626
################
2727
Utility functions used throughout the various nlesc-nano repositories.
2828

mypy.ini

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
[mypy]
2-
ignore_missing_imports = True
32
warn_unused_ignores = True
43
warn_redundant_casts = True
54
warn_return_any = True
65
show_error_codes = True
6+
7+
[mypy-IPython.*]
8+
ignore_missing_imports = True
9+
10+
[mypy-h5py.*]
11+
ignore_missing_imports = True

nanoutils/_dtype_mapping.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _repr_helper(self: DTypeMapping, dtype_repr: Callable[[np.dtype[Any]], str])
4444
return f"{cls.__name__}(\n{values}\n)"
4545

4646

47-
class DTypeMapping(UserMapping[str, dtype]):
47+
class DTypeMapping(UserMapping[str, "dtype[Any]"]):
4848
"""A mapping for creating structured dtypes.
4949
5050
Examples
@@ -249,7 +249,10 @@ def __ror__(self: _ST1, other: Mapping[str, npt.DTypeLike]) -> _ST1:
249249
return cls._reconstruct({k: np.dtype(v) for k, v in other.items()} | self._dict)
250250

251251

252-
class MutableDTypeMapping(DTypeMapping, MutableUserMapping[str, dtype]): # type: ignore[misc]
252+
class MutableDTypeMapping( # type: ignore[misc]
253+
DTypeMapping,
254+
MutableUserMapping[str, "dtype[Any]"],
255+
):
253256
"""A mutable mapping for creating structured dtypes.
254257
255258
Examples
@@ -312,7 +315,11 @@ def __delattr__(self, name: str) -> None:
312315
return object.__delattr__(self, name)
313316

314317
@positional_only
315-
def update(self, __iterable: None | _DictLike = None, **fields: npt.DTypeLike) -> None:
318+
def update(
319+
self,
320+
__iterable: None | _DictLike[str, npt.DTypeLike] = None,
321+
**fields: npt.DTypeLike,
322+
) -> None:
316323
"""Update the mapping from the passed mapping or iterable."""
317324
if __iterable is None:
318325
pass

nanoutils/_lazy_import.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ def __init__(self) -> None:
7373
super().__init__()
7474
self.maxdict = 2
7575

76-
def repr_mappingproxy(self, x, level):
77-
return self.repr_dict(x, level)
76+
def repr_mappingproxy(self, x: types.MappingProxyType[Any, Any], level: int) -> str:
77+
return self.repr_dict(x, level) # type: ignore[arg-type]
7878

7979

8080
_repr = _CustomRepr().repr

nanoutils/_seq_view.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def count(self, *args, **kwargs):
181181
"""Return the number of times **value** occurs in the instance."""
182182
return self._seq.count(*args, **kwargs)
183183

184-
def __len__(self):
184+
def __len__(self) -> int:
185185
"""Implement :func:`len(self) <len>`."""
186186
return len(self._seq)
187187

nanoutils/_set_attr.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
_T1 = TypeVar('_T1')
1818
_T2 = TypeVar('_T2')
19-
_ST = TypeVar('_ST', bound='SetAttr')
19+
_ST = TypeVar('_ST', bound='SetAttr[Any, Any]')
2020

2121

2222
class SetAttr(Generic[_T1, _T2]):

nanoutils/_user_dict.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
from .utils import positional_only
2727
from .typing_utils import Protocol, runtime_checkable
2828

29+
_T = TypeVar("_T")
30+
_ST1 = TypeVar("_ST1", bound="UserMapping[Any, Any]")
31+
_ST2 = TypeVar("_ST2", bound="MutableUserMapping[Any, Any]")
32+
_KT = TypeVar("_KT")
33+
_VT = TypeVar("_VT")
34+
_VT_co = TypeVar("_VT_co", covariant=True)
35+
2936
if TYPE_CHECKING:
3037
from IPython.lib.pretty import RepresentationPrinter
3138

@@ -36,13 +43,6 @@ def __call__(self, __dct: dict[_KT, _VT], *, width: int) -> str: ...
3643

3744
_SENTINEL = object()
3845

39-
_T = TypeVar("_T")
40-
_ST1 = TypeVar("_ST1", bound="UserMapping[Any, Any]")
41-
_ST2 = TypeVar("_ST2", bound="MutableUserMapping[Any, Any]")
42-
_KT = TypeVar("_KT")
43-
_VT = TypeVar("_VT")
44-
_VT_co = TypeVar("_VT_co", covariant=True)
45-
4646

4747
@runtime_checkable
4848
class _SupportsKeysAndGetItem(Protocol[_KT, _VT_co]):

nanoutils/schema.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def format(self, obj: object) -> str: # type: ignore
259259
return repr(obj)
260260

261261
@property
262-
def __mod__(self):
262+
def __mod__(self) -> Callable[[object], str]: # type: ignore[override]
263263
"""Get :meth:`Formatter.format`."""
264264
return self.format
265265

nanoutils/testing_utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def delete_finally(
9898

9999
def decorator(func: _FT) -> _FT:
100100
@wraps(func)
101-
def wrapper(*args, **kwargs):
101+
def wrapper(*args: Any, **kwargs: Any) -> Any:
102102
try:
103103
return func(*args, **kwargs)
104104
finally:

nanoutils/utils.py

+64-14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
1313
"""
1414

15+
# flake8: noqa: E402
16+
1517
from __future__ import annotations
1618

1719
import re
@@ -419,8 +421,8 @@ def decorator1(func: _FT) -> _FT:
419421
elif isinstance(exception, BaseException):
420422
def decorator2(func: _FT) -> _FT:
421423
@wraps(func)
422-
def wrapper(*args, **kwargs):
423-
raise exception
424+
def wrapper(*args: Any, **kwargs: Any) -> Any:
425+
raise exception # type: ignore[misc]
424426
return wrapper # type: ignore[return-value]
425427
return decorator2
426428

@@ -503,7 +505,41 @@ def wrapper(*args: Any, **kwargs: Any) -> None:
503505
raise TypeError(f"{exception.__class__.__name__!r}")
504506

505507

506-
_PATTERN = re.compile("([0-9]+).([0-9]+).([0-9]+)")
508+
# See PEP 440 Apendix B
509+
_PATTERN_STR = r"""
510+
v?
511+
(?:
512+
(?:(?P<epoch>[0-9]+)!)? # epoch
513+
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
514+
(?P<pre> # pre-release
515+
[-_\.]?
516+
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
517+
[-_\.]?
518+
(?P<pre_n>[0-9]+)?
519+
)?
520+
(?P<post> # post release
521+
(?:-(?P<post_n1>[0-9]+))
522+
|
523+
(?:
524+
[-_\.]?
525+
(?P<post_l>post|rev|r)
526+
[-_\.]?
527+
(?P<post_n2>[0-9]+)?
528+
)
529+
)?
530+
(?P<dev> # dev release
531+
[-_\.]?
532+
(?P<dev_l>dev)
533+
[-_\.]?
534+
(?P<dev_n>[0-9]+)?
535+
)?
536+
)
537+
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
538+
"""
539+
_PATTERN = re.compile(
540+
r"^\s*" + _PATTERN_STR + r"\s*$",
541+
re.VERBOSE | re.IGNORECASE,
542+
)
507543

508544

509545
class VersionInfo(NamedTuple):
@@ -525,16 +561,26 @@ class VersionInfo(NamedTuple):
525561
major: int
526562

527563
#: :class:`int`: The semantic_ minor version.
528-
minor: int
564+
minor: int = 0
529565

530-
#: :class:`int`: The semantic_ micro version (a.k.a. :attr:`VersionInfo.patch`).
531-
micro: int
566+
#: :class:`int`: The semantic_ micro version.
567+
micro: int = 0
532568

533569
@property
534570
def patch(self) -> int:
535571
""":class:`int`: An alias for :attr:`VersionInfo.micro`."""
536572
return self.micro
537573

574+
@property
575+
def maintenance(self) -> int:
576+
""":class:`int`: An alias for :attr:`VersionInfo.micro`."""
577+
return self.micro
578+
579+
@property
580+
def bug(self) -> int:
581+
""":class:`int`: An alias for :attr:`VersionInfo.micro`."""
582+
return self.micro
583+
538584
@classmethod
539585
def from_str(
540586
cls,
@@ -547,23 +593,27 @@ def from_str(
547593
Parameters
548594
----------
549595
version : :class:`str`
550-
A string representation of a version (*e.g.* :code:`version = "0.8.2"`).
551-
The string should contain three ``"."`` separated integers, respectively,
552-
representing the major, minor and micro/patch versions.
596+
A PEP 440-compatible version string(*e.g.* :code:`version = "0.8.2"`).
597+
Note that version representations are truncated at up to three integers.
553598
fullmatch : :class:`bool`
554-
Whether the version-string must consist exclusivelly of three
555-
period-separated integers, or if a substring is also allowed.
599+
Whether to perform a full or partial match on the passed string.
556600
557601
Returns
558602
-------
559603
:class:`nanoutils.VersionInfo`
560604
A new VersionInfo instance.
561605
606+
See Also
607+
--------
608+
:pep:`440`
609+
This PEP describes a scheme for identifying versions of Python software distributions,
610+
and declaring dependencies on particular versions.
611+
562612
"""
563613
match = _PATTERN.fullmatch(version) if fullmatch else _PATTERN.match(version)
564614
if match is None:
565615
raise ValueError(f"Failed to parse {version!r}")
566-
return cls._make(int(i) for i in match.groups())
616+
return cls._make(int(i) for i in match["release"].split(".")[:3])
567617

568618

569619
def _get_directive(
@@ -797,10 +847,10 @@ def warning_filter(
797847
:func:`warnings.filterwarnings` :
798848
Insert a simple entry into the list of warnings filters (at the front).
799849
800-
"""
850+
""" # noqa: E501
801851
def decorator(func: _FT) -> _FT:
802852
@functools.wraps(func)
803-
def wrapper(*args, **kwargs):
853+
def wrapper(*args: Any, **kwargs: Any) -> Any:
804854
with warnings.catch_warnings():
805855
warnings.filterwarnings(action, message, category, module, lineno, append)
806856
ret = func(*args, **kwargs)

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[metadata]
2-
description-file = README.rst
2+
description_file = README.rst
33

44
[aliases]
55
# Define `python setup.py test`

setup.py

-3
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,8 @@
3131
# Requirements for running tests
3232
tests_no_optional_require = [
3333
'assertionlib>=3.2.1',
34-
'mypy>=0.900',
3534
'pytest>=5.4.0',
3635
'pytest-cov',
37-
'pytest-mypy>=0.6.2',
38-
'types-PyYAML',
3936
]
4037
tests_require = tests_no_optional_require + [
4138
'schema',

tests/test_utils.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,14 @@ def test_version_info() -> None:
4646
assertion.eq(tup2, (0, 1, 2))
4747

4848
assertion.eq(tup1.micro, tup1.patch)
49+
assertion.eq(tup1.micro, tup1.bug)
50+
assertion.eq(tup1.micro, tup1.maintenance)
4951

5052
assertion.assert_(VersionInfo.from_str, b'0.1.2', exception=TypeError)
51-
assertion.assert_(VersionInfo.from_str, '0.1.2a', exception=ValueError)
52-
assertion.assert_(VersionInfo.from_str, '0.1.2.3.4', exception=ValueError)
53+
assertion.assert_(VersionInfo.from_str, '0.1.2bob', exception=ValueError)
5354

5455
assertion.isinstance(version_info, VersionInfo)
5556

56-
assertion.eq(VersionInfo.from_str('0.1.2a', fullmatch=False), (0, 1, 2))
57-
5857

5958
def test_split_dict() -> None:
6059
"""Test :func:`nanoutils.split_dict`."""

0 commit comments

Comments
 (0)