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.
Files changed (33) hide show
  1. {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/CHANGELOG.rst +29 -11
  2. {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/PKG-INFO +16 -13
  3. pythonic_fp_gadgets-4.0.0/README.rst +32 -0
  4. {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/pyproject.toml +6 -6
  5. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/__init__.py +83 -0
  6. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/__init__.pyi +6 -0
  7. {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/box.py +22 -26
  8. {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/box.pyi +3 -2
  9. 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
  10. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/__init__.pyi +0 -0
  11. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/flavored.py +72 -0
  12. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/flavored.pyi +8 -0
  13. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/novalue.py +124 -0
  14. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/sentinels/novalue.pyi +6 -0
  15. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/wrap.py +124 -0
  16. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/wrap.pyi +23 -0
  17. pythonic_fp_gadgets-4.0.0/tests/sentinels/test_novalue.py +83 -0
  18. pythonic_fp_gadgets-4.0.0/tests/sentinels/test_sentinel_hidden_implemetation_detail.py +70 -0
  19. pythonic_fp_gadgets-4.0.0/tests/sentinels/test_sentinel_with_functions.py +31 -0
  20. pythonic_fp_gadgets-4.0.0/tests/test_first_common_ancestor.py +115 -0
  21. pythonic_fp_gadgets-4.0.0/tests/test_hwrap.py +76 -0
  22. pythonic_fp_gadgets-3.0.1/tests/test_it.py → pythonic_fp_gadgets-4.0.0/tests/test_iterate_over_arguments.py +1 -1
  23. pythonic_fp_gadgets-4.0.0/tests/test_wrap.py +71 -0
  24. pythonic_fp_gadgets-3.0.1/README.rst +0 -29
  25. pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/__init__.py +0 -35
  26. pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/iterate_arguments.pyi +0 -5
  27. pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/latest_common_ancestor.py +0 -51
  28. pythonic_fp_gadgets-3.0.1/src/pythonic_fp/gadgets/latest_common_ancestor.pyi +0 -3
  29. pythonic_fp_gadgets-3.0.1/tests/test_lca.py +0 -114
  30. {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/.gitignore +0 -0
  31. {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/LICENSE +0 -0
  32. {pythonic_fp_gadgets-3.0.1 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/py.typed +0 -0
  33. {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 Python typing 3.12+ conventions.
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
- - only "happy path" return types (pythonic convention)
29
- - stubgen actually puts them back in .pyi files
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, pythonic-fp-gadgets.
38
- Replaced it with the empty Python module ``pythonic_fp.name_claim``.
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 packages
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 project pythonic-fp.
63
- The gadgets library is for simple, but useful, functions and data structures
64
- with minimal dependencies.
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: 3.0.1
4
- Summary: Simple Gadgets
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/releases.html
21
- Project-URL: Homepage, https://github.com/grscheller/pythonic-fp/blob/main/README.md
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, data structures with minimal dependencies.
32
+ Library of simple, but useful, classes and functions with no dependencies
33
+ outside the Python Standard Library.
33
34
 
34
- - single item box
35
- - function returning iterator of its arguments
36
- - find the last common ancestor of two classes
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
- This PyPI project is part of of the grscheller
39
- `pythonic-fp namespace projects
40
- <https://github.com/grscheller/pythonic-fp/blob/main/README.md>`_
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/development/build/html>`_.
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.0.1"
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/releases.html"
28
- Homepage = "https://github.com/grscheller/pythonic-fp/blob/main/README.md"
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!!!")
@@ -0,0 +1,6 @@
1
+ from collections.abc import Iterator
2
+
3
+ __all__ = ['iterate_over_arguments', 'first_common_ancestor']
4
+
5
+ def iterate_over_arguments[A](*args: A) -> Iterator[A]: ...
6
+ def first_common_ancestor(cls1: type, cls2: type) -> type: ...
@@ -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 ClassVar, cast, Final, overload
21
- from pythonic_fp.sentinels.flavored import Sentinel
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
- - ``Box(item: T)``: contains at one item of type ``T``
30
- - ``Box[T]()``: creates empty container
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 | Sentinel[str] = Sentinel('_Box')) -> None:
48
+ def __init__(self, item: T | _Sentinel = _sentinel) -> None:
51
49
  """
52
- :param item: an "optional" initial contained ``item`` for the ``Box``.
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 Sentinel('_Box')
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 | Sentinel[str] = Sentinel('_Box')) -> 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 self._sentinel:
92
+ if self._item is not _sentinel:
97
93
  return cast(T, self._item)
98
- if alt is self._sentinel:
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: ``ValueError`` if Box is empty
102
+ :returns: The item contained in the ``Box``.
103
+ :raises ValueError: If Box is empty.
108
104
 
109
105
  """
110
- if self._item is self._sentinel:
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 = self._sentinel
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 Sentinel('_Box'):
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 self._sentinel:
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 Sentinel('_Box'):
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 self._sentinel:
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
- """Function which returns an iterator of its arguments."""
16
-
17
- from collections.abc import Iterator
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'
@@ -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,8 @@
1
+ from typing import Hashable
2
+
3
+ __all__ = ['Sentinel']
4
+
5
+ class Sentinel[H: Hashable]:
6
+ def __new__(cls, flavor: H) -> Sentinel[H]: ...
7
+ def __init__(self, flavor: H) -> None: ...
8
+ def flavor(self) -> H: ...
@@ -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
@@ -0,0 +1,6 @@
1
+ __all__ = ['NoValue']
2
+
3
+ class NoValue:
4
+ def __new__(cls) -> NoValue: ...
5
+ def __eq__(self, other: object) -> bool: ...
6
+ def __ne__(self, other: object) -> bool: ...