pythonic-fp-gadgets 3.0.1__tar.gz → 3.1.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-3.1.0}/CHANGELOG.rst +20 -11
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/PKG-INFO +12 -9
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/README.rst +12 -9
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/pyproject.toml +3 -4
- pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/__init__.py +39 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/box.py +18 -20
- pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/sentinels/__init__.py +31 -0
- pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/sentinels/flavored.py +86 -0
- pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/sentinels/novalue.py +106 -0
- pythonic_fp_gadgets-3.1.0/tests/sentinels/test_novalue.py +83 -0
- pythonic_fp_gadgets-3.1.0/tests/sentinels/test_sentinel_hidden_implemetation_detail.py +70 -0
- pythonic_fp_gadgets-3.1.0/tests/sentinels/test_sentinel_with_functions.py +31 -0
- pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/__init__.py +0 -35
- pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/box.pyi +0 -26
- 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.pyi +0 -3
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/.gitignore +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/LICENSE +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/iterate_arguments.py +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/latest_common_ancestor.py +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/py.typed +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/tests/test_box.py +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/tests/test_it.py +0 -0
- {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/tests/test_lca.py +0 -0
|
@@ -17,16 +17,23 @@ See `Semantic Versioning 2.0.0 <https://semver.org>`_.
|
|
|
17
17
|
Releases and Important Milestones
|
|
18
18
|
---------------------------------
|
|
19
19
|
|
|
20
|
+
PyPI v3.0.1 - 2025-09-09
|
|
21
|
+
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
22
|
+
|
|
23
|
+
Corrected incorrect dependencies.
|
|
24
|
+
|
|
20
25
|
PyPI v3.0.0 - 2025-08-31
|
|
21
26
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
22
27
|
|
|
23
|
-
Discovered I was not quite compliant with
|
|
28
|
+
Discovered I was not quite compliant with
|
|
29
|
+
Python typing 3.12+ conventions.
|
|
24
30
|
|
|
25
31
|
- No longer explicitly using TypeVar directly.
|
|
26
|
-
- Removed Unions with Never from .py files
|
|
27
32
|
|
|
28
|
-
-
|
|
29
|
-
|
|
33
|
+
- Removed Unions with Never from .py files
|
|
34
|
+
|
|
35
|
+
- the "happy path" returns just types (pythonic convention)
|
|
36
|
+
- stubgen actually puts them back in .pyi files
|
|
30
37
|
|
|
31
38
|
- renamed it.it -> iterate_arguments.ita
|
|
32
39
|
- renamed lca.latest_common_ancestor -> latest_common_ancestor.lca
|
|
@@ -34,11 +41,12 @@ Discovered I was not quite compliant with Python typing 3.12+ conventions.
|
|
|
34
41
|
PyPI v2.2.0 - 2025-08-30
|
|
35
42
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
36
43
|
|
|
37
|
-
Moved for pythonic_fp.gadgets package to a new GitHub repo,
|
|
38
|
-
Replaced it with the empty Python
|
|
44
|
+
Moved for pythonic_fp.gadgets package to a new GitHub repo,
|
|
45
|
+
pythonic-fp-gadgets. Replaced it with the empty Python
|
|
46
|
+
module pythonic_fp.name_claim in pythonic-fp.
|
|
39
47
|
|
|
40
|
-
The gadgets package being different from the other namespace
|
|
41
|
-
was throwing off my workflow.
|
|
48
|
+
The gadgets package being different from the other namespace
|
|
49
|
+
packages was throwing off my workflow.
|
|
42
50
|
|
|
43
51
|
- added function it.it
|
|
44
52
|
- added function lca.latest_common_ancestor
|
|
@@ -59,6 +67,7 @@ Released pythonic-fp v1.1.0 which contains pythonic_fp.gadgets package.
|
|
|
59
67
|
Update - 2025-08-01
|
|
60
68
|
~~~~~~~~~~~~~~~~~~~
|
|
61
69
|
|
|
62
|
-
Added package pythonic_fp.gadgets to the "name-claim" PyPI
|
|
63
|
-
The gadgets library is for simple,
|
|
64
|
-
with minimal
|
|
70
|
+
Added package pythonic_fp.gadgets to the "name-claim" PyPI
|
|
71
|
+
project pythonic-fp. The gadgets library is for simple,
|
|
72
|
+
but useful, functions and data structures with minimal
|
|
73
|
+
dependencies.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonic-fp-gadgets
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Simple Gadgets
|
|
5
5
|
Keywords: gadgets
|
|
6
6
|
Author-email: "Geoffrey R. Scheller" <geoffrey@scheller.com>
|
|
@@ -14,8 +14,8 @@ 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
20
|
Project-URL: Documentation, https://grscheller.github.io/pythonic-fp/gadgets/development/build/html/releases.html
|
|
21
21
|
Project-URL: Homepage, https://github.com/grscheller/pythonic-fp/blob/main/README.md
|
|
@@ -29,15 +29,18 @@ 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/homepage/build/html/index.html>`_
|
|
43
|
+
PyPI projects.
|
|
41
44
|
|
|
42
45
|
Documentation
|
|
43
46
|
-------------
|
|
@@ -5,15 +5,18 @@ PyPI project
|
|
|
5
5
|
`pythonic-fp-gadgets
|
|
6
6
|
<https://pypi.org/project/pythonic-fp>`_.
|
|
7
7
|
|
|
8
|
-
Library of simple, but useful,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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/homepage/build/html/index.html>`_
|
|
19
|
+
PyPI projects.
|
|
17
20
|
|
|
18
21
|
Documentation
|
|
19
22
|
-------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pythonic-fp-gadgets"
|
|
3
|
-
version = "3.0
|
|
3
|
+
version = "3.1.0"
|
|
4
4
|
readme = "README.rst"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -17,9 +17,7 @@ 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]
|
|
@@ -31,6 +29,7 @@ Source = "https://github.com/grscheller/pythonic-fp-gadgets"
|
|
|
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]
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
Simple Gadgets
|
|
17
|
+
==============
|
|
18
|
+
|
|
19
|
+
Library of simple, but useful, functions and classes with no external
|
|
20
|
+
dependencies besides the those from the Python standard Library. This
|
|
21
|
+
includes other Pythonic Functional Programming dependencies.
|
|
22
|
+
|
|
23
|
+
+------------------------------+----------------------+------------------------------------------------+
|
|
24
|
+
| Description | Gadget | Module |
|
|
25
|
+
+==============================+======================+================================================+
|
|
26
|
+
| Single item box | class ``Box`` | ``pythonic_fp.gadgets.box`` |
|
|
27
|
+
+------------------------------+----------------------+------------------------------------------------+
|
|
28
|
+
| Return Iterator of arguments | function ``ita`` | ``pythonic_fp.gadgets.iterate_arguments`` |
|
|
29
|
+
+------------------------------+----------------------+------------------------------------------------+
|
|
30
|
+
| Find least common base class | function ``lca`` | ``pythonic_fp.gadgets.latest_common_ancestor`` |
|
|
31
|
+
+------------------------------+----------------------+------------------------------------------------+
|
|
32
|
+
| sentinels values with extras | module ``sentinals`` | ``pythonic_fp.gadgets.sentinels`` |
|
|
33
|
+
+------------------------------+----------------------+------------------------------------------------+
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
__author__ = 'Geoffrey R. Scheller'
|
|
38
|
+
__copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
|
|
39
|
+
__license__ = 'Apache License 2.0'
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
__all__ = ['Box']
|
|
18
18
|
|
|
19
19
|
from collections.abc import Callable, Iterator
|
|
20
|
-
from typing import
|
|
21
|
-
|
|
20
|
+
from typing import cast, Final, overload
|
|
21
|
+
|
|
22
|
+
type _Sentinel = object
|
|
23
|
+
_sentinel: Final[_Sentinel] = object()
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class Box[T]:
|
|
@@ -40,23 +42,19 @@ class Box[T]:
|
|
|
40
42
|
__slots__ = ('_item',)
|
|
41
43
|
__match_args__ = ('_item',)
|
|
42
44
|
|
|
43
|
-
_sentinel: Final[ClassVar[Sentinel[str]]] = Sentinel('_Box')
|
|
44
|
-
|
|
45
45
|
@overload
|
|
46
46
|
def __init__(self) -> None: ...
|
|
47
47
|
@overload
|
|
48
48
|
def __init__(self, item: T) -> None: ...
|
|
49
49
|
|
|
50
|
-
def __init__(self, item: T |
|
|
50
|
+
def __init__(self, item: T | _Sentinel = _sentinel) -> None:
|
|
51
51
|
"""
|
|
52
|
-
:param item:
|
|
53
|
-
:returns: New ``Box`` instance.
|
|
54
|
-
|
|
52
|
+
:param item: An "optional" initial contained ``item`` for the ``Box``.
|
|
55
53
|
"""
|
|
56
54
|
self._item = item
|
|
57
55
|
|
|
58
56
|
def __bool__(self) -> bool:
|
|
59
|
-
return self._item is not
|
|
57
|
+
return self._item is not _sentinel
|
|
60
58
|
|
|
61
59
|
def __iter__(self) -> Iterator[T]:
|
|
62
60
|
if self:
|
|
@@ -85,7 +83,7 @@ class Box[T]:
|
|
|
85
83
|
@overload
|
|
86
84
|
def get(self, alt: T) -> T: ...
|
|
87
85
|
|
|
88
|
-
def get(self, alt: T |
|
|
86
|
+
def get(self, alt: T | _Sentinel = _sentinel) -> T:
|
|
89
87
|
"""Return the contained item if it exists, otherwise an alternate item.
|
|
90
88
|
|
|
91
89
|
:param alt: an "optional" item of type ``T`` to return if ``Box`` is empty
|
|
@@ -93,9 +91,9 @@ class Box[T]:
|
|
|
93
91
|
:raises ValueError: when an ``alt`` item is not provided but needed
|
|
94
92
|
|
|
95
93
|
"""
|
|
96
|
-
if self._item is not
|
|
94
|
+
if self._item is not _sentinel:
|
|
97
95
|
return cast(T, self._item)
|
|
98
|
-
if alt is
|
|
96
|
+
if alt is _sentinel:
|
|
99
97
|
msg = 'Box: get from empty Box with no alternate return item provided'
|
|
100
98
|
raise ValueError(msg)
|
|
101
99
|
return cast(T, alt)
|
|
@@ -103,15 +101,15 @@ class Box[T]:
|
|
|
103
101
|
def pop(self) -> T:
|
|
104
102
|
"""Pop the contained item if ``Box`` is not empty.
|
|
105
103
|
|
|
106
|
-
:returns: item contained in the ``Box
|
|
107
|
-
:raises
|
|
104
|
+
:returns: The item contained in the ``Box``.
|
|
105
|
+
:raises ValueError: If Box is empty.
|
|
108
106
|
|
|
109
107
|
"""
|
|
110
|
-
if self._item is
|
|
108
|
+
if self._item is _sentinel:
|
|
111
109
|
msg = 'Box: Trying to pop an item from an empty Box'
|
|
112
110
|
raise ValueError(msg)
|
|
113
111
|
popped = cast(T, self._item)
|
|
114
|
-
self._item =
|
|
112
|
+
self._item = _sentinel
|
|
115
113
|
return popped
|
|
116
114
|
|
|
117
115
|
def push(self, item: T) -> None:
|
|
@@ -121,7 +119,7 @@ class Box[T]:
|
|
|
121
119
|
:raises ValueError: If ``Box`` is not empty.
|
|
122
120
|
|
|
123
121
|
"""
|
|
124
|
-
if self._item is
|
|
122
|
+
if self._item is _sentinel:
|
|
125
123
|
self._item = item
|
|
126
124
|
else:
|
|
127
125
|
msg = 'Box: Trying to push an item in a non-empty Box'
|
|
@@ -139,7 +137,7 @@ class Box[T]:
|
|
|
139
137
|
:returns: Original contents of the ``Box``.
|
|
140
138
|
:raises ValueError: If Box is empty.
|
|
141
139
|
"""
|
|
142
|
-
if self._item is
|
|
140
|
+
if self._item is _sentinel:
|
|
143
141
|
msg = 'Box: Trying to exchange items from an empty Box'
|
|
144
142
|
raise ValueError(msg)
|
|
145
143
|
popped = cast(T, self._item)
|
|
@@ -154,7 +152,7 @@ class Box[T]:
|
|
|
154
152
|
:returns: a new instance
|
|
155
153
|
|
|
156
154
|
"""
|
|
157
|
-
if self._item is
|
|
155
|
+
if self._item is _sentinel:
|
|
158
156
|
return Box()
|
|
159
157
|
return Box(f(cast(T, self._item)))
|
|
160
158
|
|
|
@@ -165,6 +163,6 @@ class Box[T]:
|
|
|
165
163
|
:returns: a new instance
|
|
166
164
|
|
|
167
165
|
"""
|
|
168
|
-
if self._item is
|
|
166
|
+
if self._item is _sentinel:
|
|
169
167
|
return Box()
|
|
170
168
|
return f(cast(T, self._item))
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
Sentinels with Extras
|
|
17
|
+
=====================
|
|
18
|
+
|
|
19
|
+
+-----------------------+---------------------------+-----------------------------------------------+
|
|
20
|
+
| Module | Class | Represents |
|
|
21
|
+
+=======================+===========================+===============================================+
|
|
22
|
+
| ``sentinel.novalue`` | ``NoValue`` | an actually , not potentially, missing value |
|
|
23
|
+
+-----------------------+---------------------------+-----------------------------------------------+
|
|
24
|
+
| ``sentinel.flavored`` | ``Sentinel[H: Hashable]`` | distinct sentinels labeled by hashable values |
|
|
25
|
+
+-----------------------+---------------------------+-----------------------------------------------+
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__author__ = 'Geoffrey R. Scheller'
|
|
30
|
+
__copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
|
|
31
|
+
__license__ = 'Apache License 2.0'
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
Flavored Sentinels
|
|
17
|
+
==================
|
|
18
|
+
|
|
19
|
+
Sentinel values labeled by different (hashable) flavors. Can be used
|
|
20
|
+
with functions or classes.
|
|
21
|
+
|
|
22
|
+
.. note::
|
|
23
|
+
|
|
24
|
+
Threadsafe.
|
|
25
|
+
|
|
26
|
+
.. note::
|
|
27
|
+
|
|
28
|
+
Can be compared using ``==`` and ``!=``. A flavored sentinel
|
|
29
|
+
value always equals itself and never equals anything else,
|
|
30
|
+
especially other flavored sentinel values.
|
|
31
|
+
|
|
32
|
+
Useful for union types where ``Sentinel[H]`` is one of the
|
|
33
|
+
types making up the union.
|
|
34
|
+
|
|
35
|
+
To ensure that reference equality is used, put the known
|
|
36
|
+
sentinel value first in the comparison.
|
|
37
|
+
|
|
38
|
+
.. tip::
|
|
39
|
+
|
|
40
|
+
Use as a hidden implementation detail when creating "optional"
|
|
41
|
+
arguments to functions and methods. Do not export the sentinel
|
|
42
|
+
value.
|
|
43
|
+
|
|
44
|
+
- does not clash with end user code
|
|
45
|
+
|
|
46
|
+
- which may be using either ``None`` or ``()`` as a "sentinel" values
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
import threading
|
|
51
|
+
from typing import ClassVar, final, Hashable
|
|
52
|
+
|
|
53
|
+
__all__ = ['Sentinel']
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@final
|
|
57
|
+
class Sentinel[H: Hashable]:
|
|
58
|
+
__slots__ = ('_flavor',)
|
|
59
|
+
|
|
60
|
+
_flavors: 'dict[H, Sentinel[H]]' = {}
|
|
61
|
+
_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
62
|
+
|
|
63
|
+
def __new__(cls, flavor: H) -> 'Sentinel[H]':
|
|
64
|
+
if flavor not in cls._flavors:
|
|
65
|
+
with cls._lock:
|
|
66
|
+
if flavor not in cls._flavors:
|
|
67
|
+
cls._flavors[flavor] = super().__new__(cls)
|
|
68
|
+
return cls._flavors[flavor]
|
|
69
|
+
|
|
70
|
+
def __init__(self, flavor: H) -> None:
|
|
71
|
+
"""
|
|
72
|
+
:param flavor: Some Hashable value of generic type ``H``.
|
|
73
|
+
:returns: The ``Sentinel`` singleton instance with flavor ``flavor``.
|
|
74
|
+
:rtype: ``Sentinel[H]`` where ``H`` is a subtype of Hashable.
|
|
75
|
+
"""
|
|
76
|
+
if not hasattr(self, '_flavor'):
|
|
77
|
+
self._flavor = flavor
|
|
78
|
+
|
|
79
|
+
def __repr__(self) -> str:
|
|
80
|
+
return "Sentinel('" + repr(self._flavor) + "')"
|
|
81
|
+
|
|
82
|
+
def flavor(self) -> H:
|
|
83
|
+
"""
|
|
84
|
+
:returns: The sentinel's flavor. A ``Hashable`` value of type ``H``.
|
|
85
|
+
"""
|
|
86
|
+
return self._flavor
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
Missing Value Sentinel
|
|
17
|
+
======================
|
|
18
|
+
|
|
19
|
+
Singleton class representing a missing value.
|
|
20
|
+
|
|
21
|
+
In untyped Python, both ``None`` and ``()`` are often used by end users
|
|
22
|
+
and libraries as sentinel values. I prefer to think of them as
|
|
23
|
+
|
|
24
|
+
- ``None``: returned (or returns) no values
|
|
25
|
+
- ``()``: an empty, possibly typed, but still iterable collection
|
|
26
|
+
|
|
27
|
+
While ``NoValue()`` is a singleton object representing a missing value.
|
|
28
|
+
|
|
29
|
+
Given variables
|
|
30
|
+
|
|
31
|
+
.. code:: python
|
|
32
|
+
|
|
33
|
+
x: int | NoValue
|
|
34
|
+
y: int | NoValue
|
|
35
|
+
|
|
36
|
+
Equality between ``x`` and ``y`` means both values exist and compare as equal.
|
|
37
|
+
If one or both of theses values are missing, then what is there to compare?
|
|
38
|
+
|
|
39
|
+
.. table:: ``x == y``
|
|
40
|
+
|
|
41
|
+
+-----------+-----------+--------+--------+
|
|
42
|
+
| x∖y | NoValue() | 42 | 57 |
|
|
43
|
+
+===========+===========+========+========+
|
|
44
|
+
| NoValue() | false | false | false |
|
|
45
|
+
+-----------+-----------+--------+--------+
|
|
46
|
+
| 42 | false | true | false |
|
|
47
|
+
+-----------+-----------+--------+--------+
|
|
48
|
+
| 57 | false | false | true |
|
|
49
|
+
+-----------+-----------+--------+--------+
|
|
50
|
+
|
|
51
|
+
Similarly for not equals.
|
|
52
|
+
|
|
53
|
+
.. table:: ``x != y``
|
|
54
|
+
|
|
55
|
+
+-----------+-----------+--------+--------+
|
|
56
|
+
| x∖y | NoValue() | 42 | 57 |
|
|
57
|
+
+===========+===========+========+========+
|
|
58
|
+
| NoValue() | false | false | false |
|
|
59
|
+
+-----------+-----------+--------+--------+
|
|
60
|
+
| 42 | false | false | true |
|
|
61
|
+
+-----------+-----------+--------+--------+
|
|
62
|
+
| 57 | false | true | false |
|
|
63
|
+
+-----------+-----------+--------+--------+
|
|
64
|
+
|
|
65
|
+
.. note::
|
|
66
|
+
|
|
67
|
+
Threadsafe.
|
|
68
|
+
|
|
69
|
+
.. warning::
|
|
70
|
+
|
|
71
|
+
Do not use ``==`` or ``!=`` to identify ``NoValue()``, compare
|
|
72
|
+
directly by identity using ``is`` and ``is not``.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
import threading
|
|
77
|
+
from typing import ClassVar, final
|
|
78
|
+
|
|
79
|
+
__all__ = ['NoValue']
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@final
|
|
83
|
+
class NoValue():
|
|
84
|
+
__slots__ = ()
|
|
85
|
+
|
|
86
|
+
_instance: 'ClassVar[NoValue | None]' = None
|
|
87
|
+
_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
88
|
+
|
|
89
|
+
def __new__(cls) -> 'NoValue':
|
|
90
|
+
"""
|
|
91
|
+
:returns: The ``NoValue`` singleton instance.
|
|
92
|
+
"""
|
|
93
|
+
if cls._instance is None:
|
|
94
|
+
with cls._lock:
|
|
95
|
+
if cls._instance is None:
|
|
96
|
+
cls._instance = super().__new__(cls)
|
|
97
|
+
return cls._instance
|
|
98
|
+
|
|
99
|
+
def __repr__(self) -> str:
|
|
100
|
+
return 'NoValue()'
|
|
101
|
+
|
|
102
|
+
def __eq__(self, other: object) -> bool:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def __ne__(self, other: object) -> bool:
|
|
106
|
+
return False
|
|
@@ -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
|
+
from typing import cast, Final, overload
|
|
16
|
+
from pythonic_fp.gadgets.sentinels.novalue import NoValue
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestStrRepr:
|
|
20
|
+
def test_noValue_str(self) -> None:
|
|
21
|
+
noValue: Final[NoValue] = NoValue()
|
|
22
|
+
assert str(noValue) == 'NoValue()'
|
|
23
|
+
assert repr(noValue) == 'NoValue()'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Foo:
|
|
27
|
+
@overload
|
|
28
|
+
def __init__(self, /) -> None: ...
|
|
29
|
+
@overload
|
|
30
|
+
def __init__(self, repeat: None, /) -> None: ...
|
|
31
|
+
|
|
32
|
+
def __init__(self, repeat: int | None | NoValue = NoValue(), /) -> None:
|
|
33
|
+
if repeat is None:
|
|
34
|
+
self._repeat = 0
|
|
35
|
+
else:
|
|
36
|
+
self._repeat = 1
|
|
37
|
+
|
|
38
|
+
def repeat(self) -> str:
|
|
39
|
+
return 'foo' * self._repeat
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RepeatFoo(Foo):
|
|
43
|
+
@overload
|
|
44
|
+
def __init__(self, /) -> None: ...
|
|
45
|
+
@overload
|
|
46
|
+
def __init__(self, repeat: None, /) -> None: ...
|
|
47
|
+
@overload
|
|
48
|
+
def __init__(self, repeat: int, /) -> None: ...
|
|
49
|
+
|
|
50
|
+
def __init__(self, repeat: int | None | NoValue = NoValue(), /) -> None:
|
|
51
|
+
if repeat is None:
|
|
52
|
+
self._repeat = 1
|
|
53
|
+
elif repeat is NoValue():
|
|
54
|
+
self._repeat = 2
|
|
55
|
+
else:
|
|
56
|
+
ii: int = cast(int, repeat)
|
|
57
|
+
self._repeat = ii if ii >=0 else -ii
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestNoValue:
|
|
61
|
+
def test_foo(self) -> None:
|
|
62
|
+
foo: Foo = Foo()
|
|
63
|
+
foo_none: Foo = Foo(None)
|
|
64
|
+
|
|
65
|
+
assert foo.repeat() == 'foo'
|
|
66
|
+
assert foo_none.repeat() == ''
|
|
67
|
+
|
|
68
|
+
def test_repeat_foo(self) -> None:
|
|
69
|
+
foo: Foo = RepeatFoo()
|
|
70
|
+
foo_none: Foo = RepeatFoo(None)
|
|
71
|
+
foo_0: Foo = RepeatFoo(0)
|
|
72
|
+
foo_1: Foo = RepeatFoo(1)
|
|
73
|
+
foo_2: Foo = RepeatFoo(2)
|
|
74
|
+
foo_3: Foo = RepeatFoo(3)
|
|
75
|
+
foo_neg_4: Foo = RepeatFoo(-4)
|
|
76
|
+
|
|
77
|
+
assert foo.repeat() == 'foofoo'
|
|
78
|
+
assert foo_none.repeat() == 'foo'
|
|
79
|
+
assert foo_0.repeat() == ''
|
|
80
|
+
assert foo_1.repeat() == 'foo'
|
|
81
|
+
assert foo_2.repeat() == 'foofoo'
|
|
82
|
+
assert foo_3.repeat() == 'foofoofoo'
|
|
83
|
+
assert foo_neg_4.repeat() == 'foofoofoofoo'
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
from typing import cast, ClassVar, Final, overload
|
|
16
|
+
from pythonic_fp.gadgets.sentinels.flavored import Sentinel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MyClass:
|
|
20
|
+
_sentinel: Final[ClassVar[Sentinel[str]]] = Sentinel('_secret_str')
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
def __init__(self) -> None: ...
|
|
24
|
+
@overload
|
|
25
|
+
def __init__(self, value: float) -> None: ...
|
|
26
|
+
@overload
|
|
27
|
+
def __init__(self, value: None) -> None: ...
|
|
28
|
+
@overload
|
|
29
|
+
def __init__(self, value: float | None) -> None: ...
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self, value: float | None | Sentinel[str] = Sentinel('_secret_str')
|
|
33
|
+
) -> None:
|
|
34
|
+
if value is self._sentinel:
|
|
35
|
+
self.value: float | None = 42.0
|
|
36
|
+
else:
|
|
37
|
+
self.value = cast(float | None, value)
|
|
38
|
+
|
|
39
|
+
def get_value(self) -> float | None:
|
|
40
|
+
return self.value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestHiddenImplementation:
|
|
44
|
+
def test_hidden_inplemetation(self) -> None:
|
|
45
|
+
my_0 = MyClass(0.0)
|
|
46
|
+
my_1 = MyClass(1.0)
|
|
47
|
+
my_42 = MyClass()
|
|
48
|
+
my_none = MyClass(None)
|
|
49
|
+
|
|
50
|
+
value: float | None
|
|
51
|
+
|
|
52
|
+
if (value := my_0.get_value()) is None:
|
|
53
|
+
assert False
|
|
54
|
+
else:
|
|
55
|
+
assert value == 0.0
|
|
56
|
+
|
|
57
|
+
if (value := my_1.get_value()) is None:
|
|
58
|
+
assert False
|
|
59
|
+
else:
|
|
60
|
+
assert value == 1.0
|
|
61
|
+
|
|
62
|
+
if (value := my_42.get_value()) is None:
|
|
63
|
+
assert False
|
|
64
|
+
else:
|
|
65
|
+
assert value == 42.0
|
|
66
|
+
|
|
67
|
+
if (value := my_none.get_value()) is None:
|
|
68
|
+
assert True
|
|
69
|
+
else:
|
|
70
|
+
assert value == 0.0
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
from pythonic_fp.gadgets.sentinels.flavored import Sentinel
|
|
16
|
+
|
|
17
|
+
def calculate_something(n: int, x: float) -> tuple[Sentinel[int], float]:
|
|
18
|
+
if n <= 0:
|
|
19
|
+
return (Sentinel(0), x)
|
|
20
|
+
return (Sentinel(n), x/n)
|
|
21
|
+
|
|
22
|
+
def process_result(pair: tuple[Sentinel[int], float]) -> float:
|
|
23
|
+
if pair[0] is Sentinel(0):
|
|
24
|
+
return 0.0
|
|
25
|
+
return pair[1]
|
|
26
|
+
|
|
27
|
+
class TestWithFunctions:
|
|
28
|
+
|
|
29
|
+
def test_int_flavored_sentinels(self) -> None:
|
|
30
|
+
assert process_result(calculate_something(-42, 15234.541)) == 0.0
|
|
31
|
+
assert process_result(calculate_something(200, 80000.0)) == 400.0
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
Simple Gadgets
|
|
17
|
-
==============
|
|
18
|
-
|
|
19
|
-
Library of simple, but useful, functions and classes with minimal dependencies.
|
|
20
|
-
|
|
21
|
-
+------------------------------+------------------+------------------------------------------------+
|
|
22
|
-
| Description | Gadget | Module |
|
|
23
|
-
+==============================+==================+================================================+
|
|
24
|
-
| Single item box | class ``Box`` | ``pythonic_fp.gadgets.box`` |
|
|
25
|
-
+------------------------------+------------------+------------------------------------------------+
|
|
26
|
-
| Return Iterator of arguments | function ``ita`` | ``pythonic_fp.gadgets.iterate_arguments`` |
|
|
27
|
-
+------------------------------+------------------+------------------------------------------------+
|
|
28
|
-
| Find least common base class | function ``lca`` | ``pythonic_fp.gadgets.latest_common_ancestor`` |
|
|
29
|
-
+------------------------------+------------------+------------------------------------------------+
|
|
30
|
-
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
__author__ = 'Geoffrey R. Scheller'
|
|
34
|
-
__copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
|
|
35
|
-
__license__ = 'Apache License 2.0'
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from _typeshed import Incomplete
|
|
2
|
-
from collections.abc import Callable, Iterator
|
|
3
|
-
from typing import overload
|
|
4
|
-
|
|
5
|
-
__all__ = ['Box']
|
|
6
|
-
|
|
7
|
-
class Box[T]:
|
|
8
|
-
__match_args__: Incomplete
|
|
9
|
-
@overload
|
|
10
|
-
def __init__(self) -> None: ...
|
|
11
|
-
@overload
|
|
12
|
-
def __init__(self, item: T) -> None: ...
|
|
13
|
-
def __bool__(self) -> bool: ...
|
|
14
|
-
def __iter__(self) -> Iterator[T]: ...
|
|
15
|
-
def __len__(self) -> int: ...
|
|
16
|
-
def __eq__(self, other: object) -> bool: ...
|
|
17
|
-
@overload
|
|
18
|
-
def get(self) -> T: ...
|
|
19
|
-
@overload
|
|
20
|
-
def get(self, alt: T) -> T: ...
|
|
21
|
-
def pop(self) -> T: ...
|
|
22
|
-
def push(self, item: T) -> None: ...
|
|
23
|
-
def put(self, item: T) -> None: ...
|
|
24
|
-
def exchange(self, new_item: T) -> T: ...
|
|
25
|
-
def map[U](self, f: Callable[[T], U]) -> Box[U]: ...
|
|
26
|
-
def bind[U](self, f: Callable[[T], 'Box[U]']) -> Box[U]: ...
|
|
File without changes
|
|
File without changes
|
{pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/iterate_arguments.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|