pythonic-fp-gadgets 3.0.1__tar.gz → 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/CHANGELOG.rst +29 -11
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/PKG-INFO +16 -13
- pythonic_fp_gadgets-4.0.0/README.rst +32 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/pyproject.toml +6 -6
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/__init__.py +83 -0
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/__init__.pyi +6 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/box.py +22 -26
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/box.pyi +3 -2
- pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/iterate_arguments.py → pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/__init__.py +3 -22
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/__init__.pyi +0 -0
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/flavored.py +72 -0
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/flavored.pyi +8 -0
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/novalue.py +124 -0
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/novalue.pyi +6 -0
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/wrap.py +124 -0
- pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/wrap.pyi +23 -0
- pythonic_fp_gadgets-4.0.0/tests/sentinels/test_novalue.py +83 -0
- pythonic_fp_gadgets-4.0.0/tests/sentinels/test_sentinel_hidden_implemetation_detail.py +70 -0
- pythonic_fp_gadgets-4.0.0/tests/sentinels/test_sentinel_with_functions.py +31 -0
- pythonic_fp_gadgets-4.0.0/tests/test_first_common_ancestor.py +115 -0
- pythonic_fp_gadgets-4.0.0/tests/test_hwrap.py +76 -0
- pythonic_fp_gadgets-3.0.1/tests/test_it.py → pythonic_fp_gadgets-4.0.0/tests/test_iterate_over_arguments.py +1 -1
- pythonic_fp_gadgets-4.0.0/tests/test_wrap.py +71 -0
- pythonic_fp_gadgets-3.0.1/README.rst +0 -29
- pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/__init__.py +0 -35
- pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/iterate_arguments.pyi +0 -5
- pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/latest_common_ancestor.py +0 -51
- pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/latest_common_ancestor.pyi +0 -3
- pythonic_fp_gadgets-3.0.1/tests/test_lca.py +0 -114
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/.gitignore +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/LICENSE +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/py.typed +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/tests/test_box.py +0 -0
|
@@ -17,16 +17,32 @@ See `Semantic Versioning 2.0.0 <https://semver.org>`_.
|
|
|
17
17
|
Releases and Important Milestones
|
|
18
18
|
---------------------------------
|
|
19
19
|
|
|
20
|
+
PyPI v4.0.0 - TBD
|
|
21
|
+
~~~~~~~~~~~~~~~~~
|
|
22
|
+
|
|
23
|
+
Move functions out of their own packages to __init__.py (2025-10-13).
|
|
24
|
+
|
|
25
|
+
- latest_common_ancestor.lca -> first_common_ancestor
|
|
26
|
+
- iterate_arguments.ita -> iterate_over_arguments
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
PyPI v3.0.1 - 2025-09-09
|
|
30
|
+
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
31
|
+
|
|
32
|
+
Corrected incorrect dependencies.
|
|
33
|
+
|
|
20
34
|
PyPI v3.0.0 - 2025-08-31
|
|
21
35
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
22
36
|
|
|
23
|
-
Discovered I was not quite compliant with
|
|
37
|
+
Discovered I was not quite compliant with
|
|
38
|
+
Python typing 3.12+ conventions.
|
|
24
39
|
|
|
25
40
|
- No longer explicitly using TypeVar directly.
|
|
26
|
-
- Removed Unions with Never from .py files
|
|
27
41
|
|
|
28
|
-
-
|
|
29
|
-
|
|
42
|
+
- Removed Unions with Never from .py files
|
|
43
|
+
|
|
44
|
+
- the "happy path" returns just types (pythonic convention)
|
|
45
|
+
- stubgen actually puts them back in .pyi files
|
|
30
46
|
|
|
31
47
|
- renamed it.it -> iterate_arguments.ita
|
|
32
48
|
- renamed lca.latest_common_ancestor -> latest_common_ancestor.lca
|
|
@@ -34,11 +50,12 @@ Discovered I was not quite compliant with Python typing 3.12+ conventions.
|
|
|
34
50
|
PyPI v2.2.0 - 2025-08-30
|
|
35
51
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
36
52
|
|
|
37
|
-
Moved for pythonic_fp.gadgets package to a new GitHub repo,
|
|
38
|
-
Replaced it with the empty Python
|
|
53
|
+
Moved for pythonic_fp.gadgets package to a new GitHub repo,
|
|
54
|
+
pythonic-fp-gadgets. Replaced it with the empty Python
|
|
55
|
+
module pythonic_fp.name_claim in pythonic-fp.
|
|
39
56
|
|
|
40
|
-
The gadgets package being different from the other namespace
|
|
41
|
-
was throwing off my workflow.
|
|
57
|
+
The gadgets package being different from the other namespace
|
|
58
|
+
packages was throwing off my workflow.
|
|
42
59
|
|
|
43
60
|
- added function it.it
|
|
44
61
|
- added function lca.latest_common_ancestor
|
|
@@ -59,6 +76,7 @@ Released pythonic-fp v1.1.0 which contains pythonic_fp.gadgets package.
|
|
|
59
76
|
Update - 2025-08-01
|
|
60
77
|
~~~~~~~~~~~~~~~~~~~
|
|
61
78
|
|
|
62
|
-
Added package pythonic_fp.gadgets to the "name-claim" PyPI
|
|
63
|
-
The gadgets library is for simple,
|
|
64
|
-
with minimal
|
|
79
|
+
Added package pythonic_fp.gadgets to the "name-claim" PyPI
|
|
80
|
+
project pythonic-fp. The gadgets library is for simple,
|
|
81
|
+
but useful, functions and data structures with minimal
|
|
82
|
+
dependencies.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonic-fp-gadgets
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: **Module level function names.**
|
|
5
5
|
Keywords: gadgets
|
|
6
6
|
Author-email: "Geoffrey R. Scheller" <geoffrey@scheller.com>
|
|
7
7
|
Requires-Python: >=3.13
|
|
@@ -14,11 +14,11 @@ Classifier: Operating System :: OS Independent
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Typing :: Typed
|
|
16
16
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: pythonic-fp-sentinels>=2.1.0
|
|
18
17
|
Requires-Dist: pytest>=8.4.1 ; extra == "test"
|
|
18
|
+
Requires-Dist: pythonic-fp-circulararray>=6.0.0 ; extra == "test"
|
|
19
19
|
Project-URL: Changelog, https://github.com/grscheller/pythonic-fp-gadgets/blob/main/CHANGELOG.rst
|
|
20
|
-
Project-URL: Documentation, https://grscheller.github.io/pythonic-fp/gadgets/development/build/html/
|
|
21
|
-
Project-URL: Homepage, https://github.
|
|
20
|
+
Project-URL: Documentation, https://grscheller.github.io/pythonic-fp/gadgets/development/build/html/
|
|
21
|
+
Project-URL: Homepage, https://grscheller.github.io/pythonic-fp/homepage/build/html/
|
|
22
22
|
Project-URL: Source, https://github.com/grscheller/pythonic-fp-gadgets
|
|
23
23
|
Provides-Extra: test
|
|
24
24
|
|
|
@@ -29,22 +29,25 @@ PyPI project
|
|
|
29
29
|
`pythonic-fp-gadgets
|
|
30
30
|
<https://pypi.org/project/pythonic-fp>`_.
|
|
31
31
|
|
|
32
|
-
Library of simple, but useful,
|
|
32
|
+
Library of simple, but useful, classes and functions with no dependencies
|
|
33
|
+
outside the Python Standard Library.
|
|
33
34
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
35
|
+
- Gadgets
|
|
36
|
+
- single item box
|
|
37
|
+
- function returning iterator of its arguments
|
|
38
|
+
- find the latest common ancestor of two classes
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
`pythonic-fp
|
|
40
|
-
<https://github.
|
|
40
|
+
Part of the
|
|
41
|
+
`pythonic-fp
|
|
42
|
+
<https://grscheller.github.io/pythonic-fp>`_
|
|
43
|
+
PyPI projects.
|
|
41
44
|
|
|
42
45
|
Documentation
|
|
43
46
|
-------------
|
|
44
47
|
|
|
45
48
|
Documentation for this project is hosted on
|
|
46
49
|
`GitHub Pages
|
|
47
|
-
<https://grscheller.github.io/pythonic-fp/gadgets
|
|
50
|
+
<https://grscheller.github.io/pythonic-fp/gadgets>`_.
|
|
48
51
|
|
|
49
52
|
Copyright and License
|
|
50
53
|
---------------------
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Pythonic FP - Gadgets
|
|
2
|
+
=====================
|
|
3
|
+
|
|
4
|
+
PyPI project
|
|
5
|
+
`pythonic-fp-gadgets
|
|
6
|
+
<https://pypi.org/project/pythonic-fp>`_.
|
|
7
|
+
|
|
8
|
+
Library of simple, but useful, classes and functions with no dependencies
|
|
9
|
+
outside the Python Standard Library.
|
|
10
|
+
|
|
11
|
+
- Gadgets
|
|
12
|
+
- single item box
|
|
13
|
+
- function returning iterator of its arguments
|
|
14
|
+
- find the latest common ancestor of two classes
|
|
15
|
+
|
|
16
|
+
Part of the
|
|
17
|
+
`pythonic-fp
|
|
18
|
+
<https://grscheller.github.io/pythonic-fp>`_
|
|
19
|
+
PyPI projects.
|
|
20
|
+
|
|
21
|
+
Documentation
|
|
22
|
+
-------------
|
|
23
|
+
|
|
24
|
+
Documentation for this project is hosted on
|
|
25
|
+
`GitHub Pages
|
|
26
|
+
<https://grscheller.github.io/pythonic-fp/gadgets>`_.
|
|
27
|
+
|
|
28
|
+
Copyright and License
|
|
29
|
+
---------------------
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2025 Geoffrey R. Scheller. Licensed under the Apache
|
|
32
|
+
License, Version 2.0. See the LICENSE file for details.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pythonic-fp-gadgets"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.0.0"
|
|
4
4
|
readme = "README.rst"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -17,20 +17,19 @@ classifiers = [
|
|
|
17
17
|
"Programming Language :: Python :: 3.13",
|
|
18
18
|
"Typing :: Typed",
|
|
19
19
|
]
|
|
20
|
-
dependencies = [
|
|
21
|
-
"pythonic-fp-sentinels>=2.1.0",
|
|
22
|
-
]
|
|
20
|
+
dependencies = []
|
|
23
21
|
dynamic = ["description"]
|
|
24
22
|
|
|
25
23
|
[project.urls]
|
|
26
24
|
Changelog = "https://github.com/grscheller/pythonic-fp-gadgets/blob/main/CHANGELOG.rst"
|
|
27
|
-
Documentation = "https://grscheller.github.io/pythonic-fp/gadgets/development/build/html/
|
|
28
|
-
Homepage = "https://github.
|
|
25
|
+
Documentation = "https://grscheller.github.io/pythonic-fp/gadgets/development/build/html/"
|
|
26
|
+
Homepage = "https://grscheller.github.io/pythonic-fp/homepage/build/html/"
|
|
29
27
|
Source = "https://github.com/grscheller/pythonic-fp-gadgets"
|
|
30
28
|
|
|
31
29
|
[project.optional-dependencies]
|
|
32
30
|
test = [
|
|
33
31
|
"pytest>=8.4.1",
|
|
32
|
+
"pythonic-fp-circulararray>=6.0.0",
|
|
34
33
|
]
|
|
35
34
|
|
|
36
35
|
[build-system]
|
|
@@ -70,6 +69,7 @@ docstring-quotes = "double"
|
|
|
70
69
|
|
|
71
70
|
[tool.ruff.lint.per-file-ignores]
|
|
72
71
|
"tests/*" = ["C901"]
|
|
72
|
+
"tests/**/*.py" = ['E731']
|
|
73
73
|
"**/*.py" = ["E741"]
|
|
74
74
|
|
|
75
75
|
[tool.ruff.format]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright 2023-2025 Geoffrey R. Scheller
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
**Module level function names.**
|
|
17
|
+
|
|
18
|
+
========================== ================================================
|
|
19
|
+
Function Description
|
|
20
|
+
========================== ================================================
|
|
21
|
+
``first_common_ancestor`` Find least common base class in a type hierarchy
|
|
22
|
+
``iterate_over_arguments`` Return an iterator over the function's arguments
|
|
23
|
+
========================== ================================================
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from collections.abc import Iterator
|
|
28
|
+
from inspect import getmro
|
|
29
|
+
|
|
30
|
+
__all__ = ['iterate_over_arguments', 'first_common_ancestor']
|
|
31
|
+
|
|
32
|
+
__author__ = 'Geoffrey R. Scheller'
|
|
33
|
+
__copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
|
|
34
|
+
__license__ = 'Apache License 2.0'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def iterate_over_arguments[A](*args: A) -> Iterator[A]:
|
|
38
|
+
"""Function returning an iterator of its arguments.
|
|
39
|
+
|
|
40
|
+
.. note::
|
|
41
|
+
|
|
42
|
+
Does not create an object to iterate over.
|
|
43
|
+
|
|
44
|
+
- well, not in the Python world
|
|
45
|
+
- maybe in the C world
|
|
46
|
+
|
|
47
|
+
:param args: Objects to iterate over.
|
|
48
|
+
:returns: An iterator of the arguments.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
yield from args
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def first_common_ancestor(cls1: type, cls2: type) -> type:
|
|
55
|
+
"""Find the least upper bound in the inheritance graph
|
|
56
|
+
of two classes.
|
|
57
|
+
|
|
58
|
+
.. warning::
|
|
59
|
+
|
|
60
|
+
This function can fail with a TypeError. Some error messages
|
|
61
|
+
seen are
|
|
62
|
+
|
|
63
|
+
- multiple bases have instance lay-out conflict
|
|
64
|
+
- type 'bool' is not an acceptable base type
|
|
65
|
+
|
|
66
|
+
This happens frequently when the function is given
|
|
67
|
+
Python builtin types or in multiple inheritance situations.
|
|
68
|
+
|
|
69
|
+
:param cls1: A class in the inheritance hierarchy.
|
|
70
|
+
:param cls2: A class in the inheritance hierarchy.
|
|
71
|
+
:returns: First common ancestor based on getmro order.
|
|
72
|
+
:raises TypeError: Raised by ``inspect.getmro``.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
if issubclass(cls1, cls2):
|
|
76
|
+
return cls2
|
|
77
|
+
if issubclass(cls2, cls1):
|
|
78
|
+
return cls1
|
|
79
|
+
|
|
80
|
+
for common_ancestor in getmro(type('LcaDiamondClass', (cls1, cls2), {})):
|
|
81
|
+
if issubclass(cls1, common_ancestor) and issubclass(cls2, common_ancestor):
|
|
82
|
+
return common_ancestor
|
|
83
|
+
raise TypeError("latest_common_ancestor: no common ancestor found!!!")
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
"""Class for a stateful container that can hold at most one item."""
|
|
16
|
-
|
|
17
15
|
__all__ = ['Box']
|
|
18
16
|
|
|
19
17
|
from collections.abc import Callable, Iterator
|
|
20
|
-
from typing import
|
|
21
|
-
|
|
18
|
+
from typing import cast, Final, overload
|
|
19
|
+
|
|
20
|
+
type _Sentinel = object
|
|
21
|
+
_sentinel: Final[_Sentinel] = object()
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class Box[T]:
|
|
@@ -26,8 +26,8 @@ class Box[T]:
|
|
|
26
26
|
|
|
27
27
|
.. note::
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
- ``Box(item: T)``: contains at one item of type ``T``
|
|
30
|
+
- ``Box[T]()``: creates empty container
|
|
31
31
|
|
|
32
32
|
Where type ``T`` is some definite type, which
|
|
33
33
|
could be ``None`` or even ``Never``.
|
|
@@ -40,23 +40,19 @@ class Box[T]:
|
|
|
40
40
|
__slots__ = ('_item',)
|
|
41
41
|
__match_args__ = ('_item',)
|
|
42
42
|
|
|
43
|
-
_sentinel: Final[ClassVar[Sentinel[str]]] = Sentinel('_Box')
|
|
44
|
-
|
|
45
|
-
@overload
|
|
46
|
-
def __init__(self) -> None: ...
|
|
47
43
|
@overload
|
|
48
44
|
def __init__(self, item: T) -> None: ...
|
|
45
|
+
@overload
|
|
46
|
+
def __init__(self) -> None: ...
|
|
49
47
|
|
|
50
|
-
def __init__(self, item: T |
|
|
48
|
+
def __init__(self, item: T | _Sentinel = _sentinel) -> None:
|
|
51
49
|
"""
|
|
52
|
-
:param item:
|
|
53
|
-
:returns: New ``Box`` instance.
|
|
54
|
-
|
|
50
|
+
:param item: An "optional" initial contained ``item`` for the ``Box``.
|
|
55
51
|
"""
|
|
56
52
|
self._item = item
|
|
57
53
|
|
|
58
54
|
def __bool__(self) -> bool:
|
|
59
|
-
return self._item is not
|
|
55
|
+
return self._item is not _sentinel
|
|
60
56
|
|
|
61
57
|
def __iter__(self) -> Iterator[T]:
|
|
62
58
|
if self:
|
|
@@ -85,7 +81,7 @@ class Box[T]:
|
|
|
85
81
|
@overload
|
|
86
82
|
def get(self, alt: T) -> T: ...
|
|
87
83
|
|
|
88
|
-
def get(self, alt: T |
|
|
84
|
+
def get(self, alt: T | _Sentinel = _sentinel) -> T:
|
|
89
85
|
"""Return the contained item if it exists, otherwise an alternate item.
|
|
90
86
|
|
|
91
87
|
:param alt: an "optional" item of type ``T`` to return if ``Box`` is empty
|
|
@@ -93,9 +89,9 @@ class Box[T]:
|
|
|
93
89
|
:raises ValueError: when an ``alt`` item is not provided but needed
|
|
94
90
|
|
|
95
91
|
"""
|
|
96
|
-
if self._item is not
|
|
92
|
+
if self._item is not _sentinel:
|
|
97
93
|
return cast(T, self._item)
|
|
98
|
-
if alt is
|
|
94
|
+
if alt is _sentinel:
|
|
99
95
|
msg = 'Box: get from empty Box with no alternate return item provided'
|
|
100
96
|
raise ValueError(msg)
|
|
101
97
|
return cast(T, alt)
|
|
@@ -103,15 +99,15 @@ class Box[T]:
|
|
|
103
99
|
def pop(self) -> T:
|
|
104
100
|
"""Pop the contained item if ``Box`` is not empty.
|
|
105
101
|
|
|
106
|
-
:returns: item contained in the ``Box
|
|
107
|
-
:raises
|
|
102
|
+
:returns: The item contained in the ``Box``.
|
|
103
|
+
:raises ValueError: If Box is empty.
|
|
108
104
|
|
|
109
105
|
"""
|
|
110
|
-
if self._item is
|
|
106
|
+
if self._item is _sentinel:
|
|
111
107
|
msg = 'Box: Trying to pop an item from an empty Box'
|
|
112
108
|
raise ValueError(msg)
|
|
113
109
|
popped = cast(T, self._item)
|
|
114
|
-
self._item =
|
|
110
|
+
self._item = _sentinel
|
|
115
111
|
return popped
|
|
116
112
|
|
|
117
113
|
def push(self, item: T) -> None:
|
|
@@ -121,7 +117,7 @@ class Box[T]:
|
|
|
121
117
|
:raises ValueError: If ``Box`` is not empty.
|
|
122
118
|
|
|
123
119
|
"""
|
|
124
|
-
if self._item is
|
|
120
|
+
if self._item is _sentinel:
|
|
125
121
|
self._item = item
|
|
126
122
|
else:
|
|
127
123
|
msg = 'Box: Trying to push an item in a non-empty Box'
|
|
@@ -139,7 +135,7 @@ class Box[T]:
|
|
|
139
135
|
:returns: Original contents of the ``Box``.
|
|
140
136
|
:raises ValueError: If Box is empty.
|
|
141
137
|
"""
|
|
142
|
-
if self._item is
|
|
138
|
+
if self._item is _sentinel:
|
|
143
139
|
msg = 'Box: Trying to exchange items from an empty Box'
|
|
144
140
|
raise ValueError(msg)
|
|
145
141
|
popped = cast(T, self._item)
|
|
@@ -154,7 +150,7 @@ class Box[T]:
|
|
|
154
150
|
:returns: a new instance
|
|
155
151
|
|
|
156
152
|
"""
|
|
157
|
-
if self._item is
|
|
153
|
+
if self._item is _sentinel:
|
|
158
154
|
return Box()
|
|
159
155
|
return Box(f(cast(T, self._item)))
|
|
160
156
|
|
|
@@ -165,6 +161,6 @@ class Box[T]:
|
|
|
165
161
|
:returns: a new instance
|
|
166
162
|
|
|
167
163
|
"""
|
|
168
|
-
if self._item is
|
|
164
|
+
if self._item is _sentinel:
|
|
169
165
|
return Box()
|
|
170
166
|
return f(cast(T, self._item))
|
|
@@ -4,12 +4,13 @@ from typing import overload
|
|
|
4
4
|
|
|
5
5
|
__all__ = ['Box']
|
|
6
6
|
|
|
7
|
+
type _Sentinel = object
|
|
7
8
|
class Box[T]:
|
|
8
9
|
__match_args__: Incomplete
|
|
9
10
|
@overload
|
|
10
|
-
def __init__(self) -> None: ...
|
|
11
|
-
@overload
|
|
12
11
|
def __init__(self, item: T) -> None: ...
|
|
12
|
+
@overload
|
|
13
|
+
def __init__(self) -> None: ...
|
|
13
14
|
def __bool__(self) -> bool: ...
|
|
14
15
|
def __iter__(self) -> Iterator[T]: ...
|
|
15
16
|
def __len__(self) -> int: ...
|
|
@@ -12,25 +12,6 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
__all__ = ['ita']
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def ita[A](*args: A) -> Iterator[A]:
|
|
23
|
-
"""Function returning an iterator of its arguments.
|
|
24
|
-
|
|
25
|
-
.. note::
|
|
26
|
-
|
|
27
|
-
Does not create an object to iterate over.
|
|
28
|
-
|
|
29
|
-
- well, not in the Python world
|
|
30
|
-
- maybe in the C world
|
|
31
|
-
|
|
32
|
-
:param args: Objects to iterate over.
|
|
33
|
-
:returns: An iterator of the arguments.
|
|
34
|
-
|
|
35
|
-
"""
|
|
36
|
-
yield from args
|
|
15
|
+
__author__ = 'Geoffrey R. Scheller'
|
|
16
|
+
__copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
|
|
17
|
+
__license__ = 'Apache License 2.0'
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright 2023-2025 Geoffrey R. Scheller
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
**Sentinel values labeled by different (hashable) flavors.**
|
|
17
|
+
|
|
18
|
+
.. note::
|
|
19
|
+
|
|
20
|
+
Can be compared using ``==`` and ``!=``. A flavored sentinel
|
|
21
|
+
value always equals itself and never equals anything else,
|
|
22
|
+
especially other flavored sentinel values.
|
|
23
|
+
|
|
24
|
+
Useful for union types where ``Sentinel[H]`` is one of the
|
|
25
|
+
types making up the union.
|
|
26
|
+
|
|
27
|
+
To ensure that reference equality is used, put the known
|
|
28
|
+
sentinel value first in the comparison.
|
|
29
|
+
|
|
30
|
+
.. note::
|
|
31
|
+
|
|
32
|
+
Threadsafe.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import threading
|
|
37
|
+
from typing import ClassVar, final, Hashable
|
|
38
|
+
|
|
39
|
+
__all__ = ['Sentinel']
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@final
|
|
43
|
+
class Sentinel[H: Hashable]:
|
|
44
|
+
__slots__ = ('_flavor',)
|
|
45
|
+
|
|
46
|
+
_flavors: 'dict[H, Sentinel[H]]' = {}
|
|
47
|
+
_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
48
|
+
|
|
49
|
+
def __new__(cls, flavor: H) -> 'Sentinel[H]':
|
|
50
|
+
if flavor not in cls._flavors:
|
|
51
|
+
with cls._lock:
|
|
52
|
+
if flavor not in cls._flavors:
|
|
53
|
+
cls._flavors[flavor] = super().__new__(cls)
|
|
54
|
+
return cls._flavors[flavor]
|
|
55
|
+
|
|
56
|
+
def __init__(self, flavor: H) -> None:
|
|
57
|
+
"""
|
|
58
|
+
:param flavor: Some Hashable value of generic type ``H``.
|
|
59
|
+
:returns: The ``Sentinel`` singleton instance with flavor ``flavor``.
|
|
60
|
+
:rtype: ``Sentinel[H]`` where ``H`` is a subtype of Hashable.
|
|
61
|
+
"""
|
|
62
|
+
if not hasattr(self, '_flavor'):
|
|
63
|
+
self._flavor = flavor
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
return "Sentinel('" + repr(self._flavor) + "')"
|
|
67
|
+
|
|
68
|
+
def flavor(self) -> H:
|
|
69
|
+
"""
|
|
70
|
+
:returns: The sentinel's flavor. A ``Hashable`` value of type ``H``.
|
|
71
|
+
"""
|
|
72
|
+
return self._flavor
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Copyright 2023-2025 Geoffrey R. Scheller
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
**Singleton class representing an actually, not potentially, missing value.**
|
|
17
|
+
|
|
18
|
+
.. note:
|
|
19
|
+
|
|
20
|
+
``NoValue()`` is a singleton object representing a missing value.
|
|
21
|
+
|
|
22
|
+
While ``None`` and ``()`` are frequently used as sentinel values,
|
|
23
|
+
I prefer to think of them as
|
|
24
|
+
|
|
25
|
+
- ``None``: Returns, or returned, no values.
|
|
26
|
+
- ``()``: An empty, possibly typed, iterable collection.
|
|
27
|
+
|
|
28
|
+
.. important::
|
|
29
|
+
|
|
30
|
+
Given variables
|
|
31
|
+
|
|
32
|
+
.. code:: python
|
|
33
|
+
|
|
34
|
+
x: int | NoValue
|
|
35
|
+
y: int | NoValue
|
|
36
|
+
|
|
37
|
+
Equality between ``x`` and ``y`` means both values exist and compare
|
|
38
|
+
as equal. If one or both of theses values are missing, then what is
|
|
39
|
+
there to compare?
|
|
40
|
+
|
|
41
|
+
.. table:: ``x == y``
|
|
42
|
+
|
|
43
|
+
+-----------+-----------+--------+--------+
|
|
44
|
+
| x∖y | NoValue() | 42 | 57 |
|
|
45
|
+
+===========+===========+========+========+
|
|
46
|
+
| NoValue() | false | false | false |
|
|
47
|
+
+-----------+-----------+--------+--------+
|
|
48
|
+
| 42 | false | true | false |
|
|
49
|
+
+-----------+-----------+--------+--------+
|
|
50
|
+
| 57 | false | false | true |
|
|
51
|
+
+-----------+-----------+--------+--------+
|
|
52
|
+
|
|
53
|
+
Similarly for not equals.
|
|
54
|
+
|
|
55
|
+
.. table:: ``x != y``
|
|
56
|
+
|
|
57
|
+
+-----------+-----------+--------+--------+
|
|
58
|
+
| x∖y | NoValue() | 42 | 57 |
|
|
59
|
+
+===========+===========+========+========+
|
|
60
|
+
| NoValue() | false | false | false |
|
|
61
|
+
+-----------+-----------+--------+--------+
|
|
62
|
+
| 42 | false | false | true |
|
|
63
|
+
+-----------+-----------+--------+--------+
|
|
64
|
+
| 57 | false | true | false |
|
|
65
|
+
+-----------+-----------+--------+--------+
|
|
66
|
+
|
|
67
|
+
.. warning::
|
|
68
|
+
|
|
69
|
+
Only use ``==`` or ``!=`` in value comparisons. To directly
|
|
70
|
+
identity the ``NoValue`` singleton, use ``is`` and ``is not``
|
|
71
|
+
instead.
|
|
72
|
+
|
|
73
|
+
.. note::
|
|
74
|
+
|
|
75
|
+
Threadsafe.
|
|
76
|
+
|
|
77
|
+
.. tip::
|
|
78
|
+
|
|
79
|
+
Use as a hidden implementation detail when creating "optional"
|
|
80
|
+
arguments to functions and methods.
|
|
81
|
+
|
|
82
|
+
To help ensure the abstraction does not leak,
|
|
83
|
+
|
|
84
|
+
- Do not export the sentinel value.
|
|
85
|
+
- Use ``@overload`` to keep the NoValue type out of documentation and IDEs.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
import threading
|
|
89
|
+
from typing import ClassVar, final
|
|
90
|
+
|
|
91
|
+
__all__ = ['NoValue']
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@final
|
|
95
|
+
class NoValue():
|
|
96
|
+
__slots__ = ()
|
|
97
|
+
|
|
98
|
+
_instance: 'ClassVar[NoValue | None]' = None
|
|
99
|
+
_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
100
|
+
|
|
101
|
+
def __new__(cls) -> 'NoValue':
|
|
102
|
+
"""
|
|
103
|
+
:returns: The ``NoValue`` singleton instance.
|
|
104
|
+
"""
|
|
105
|
+
if cls._instance is None:
|
|
106
|
+
with cls._lock:
|
|
107
|
+
if cls._instance is None:
|
|
108
|
+
cls._instance = super().__new__(cls)
|
|
109
|
+
return cls._instance
|
|
110
|
+
|
|
111
|
+
def __repr__(self) -> str:
|
|
112
|
+
return 'NoValue()'
|
|
113
|
+
|
|
114
|
+
def __eq__(self, other: object) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
:returns: False
|
|
117
|
+
"""
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
def __ne__(self, other: object) -> bool:
|
|
121
|
+
"""
|
|
122
|
+
:returns: False
|
|
123
|
+
"""
|
|
124
|
+
return False
|