pythonic-fp-gadgets 3.0.0__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.
Files changed (24) hide show
  1. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/CHANGELOG.rst +20 -11
  2. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/PKG-INFO +12 -9
  3. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/README.rst +12 -9
  4. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/pyproject.toml +3 -4
  5. pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/__init__.py +39 -0
  6. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/box.py +18 -20
  7. pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/sentinels/__init__.py +31 -0
  8. pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/sentinels/flavored.py +86 -0
  9. pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/sentinels/novalue.py +106 -0
  10. pythonic_fp_gadgets-3.1.0/tests/sentinels/test_novalue.py +83 -0
  11. pythonic_fp_gadgets-3.1.0/tests/sentinels/test_sentinel_hidden_implemetation_detail.py +70 -0
  12. pythonic_fp_gadgets-3.1.0/tests/sentinels/test_sentinel_with_functions.py +31 -0
  13. pythonic_fp_gadgets-3.0.0/src/pythonic_fp/gadgets/__init__.py +0 -35
  14. pythonic_fp_gadgets-3.0.0/src/pythonic_fp/gadgets/box.pyi +0 -26
  15. pythonic_fp_gadgets-3.0.0/src/pythonic_fp/gadgets/iterate_arguments.pyi +0 -5
  16. pythonic_fp_gadgets-3.0.0/src/pythonic_fp/gadgets/latest_common_ancestor.pyi +0 -3
  17. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/.gitignore +0 -0
  18. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/LICENSE +0 -0
  19. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/iterate_arguments.py +0 -0
  20. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/latest_common_ancestor.py +0 -0
  21. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/src/pythonic_fp/gadgets/py.typed +0 -0
  22. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/tests/test_box.py +0 -0
  23. {pythonic_fp_gadgets-3.0.0 → pythonic_fp_gadgets-3.1.0}/tests/test_it.py +0 -0
  24. {pythonic_fp_gadgets-3.0.0 → 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 Python typing 3.12+ conventions.
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
- - only "happy path" return types (pythonic convention)
29
- - stubgen actually puts them back in .pyi files
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, pythonic-fp-gadgets.
38
- Replaced it with the empty Python module ``pythonic_fp.name_claim``.
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 packages
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 project pythonic-fp.
63
- The gadgets library is for simple, but useful, functions and data structures
64
- with minimal dependencies.
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.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, 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/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, data structures with minimal dependencies.
9
-
10
- - single item box
11
- - function returning iterator of its arguments
12
- - find the last common ancestor of two classes
13
-
14
- This PyPI project is part of of the grscheller
15
- `pythonic-fp namespace projects
16
- <https://github.com/grscheller/pythonic-fp/blob/main/README.md>`_
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.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 ClassVar, cast, Final, overload
21
- from pythonic_fp.sentinels.flavored import Sentinel
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 | Sentinel[str] = Sentinel('_Box')) -> None:
50
+ def __init__(self, item: T | _Sentinel = _sentinel) -> None:
51
51
  """
52
- :param item: an "optional" initial contained ``item`` for the ``Box``.
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 Sentinel('_Box')
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 | Sentinel[str] = Sentinel('_Box')) -> 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 self._sentinel:
94
+ if self._item is not _sentinel:
97
95
  return cast(T, self._item)
98
- if alt is self._sentinel:
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: ``ValueError`` if Box is empty
104
+ :returns: The item contained in the ``Box``.
105
+ :raises ValueError: If Box is empty.
108
106
 
109
107
  """
110
- if self._item is self._sentinel:
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 = self._sentinel
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 Sentinel('_Box'):
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 self._sentinel:
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 Sentinel('_Box'):
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 self._sentinel:
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]: ...
@@ -1,5 +0,0 @@
1
- from collections.abc import Iterator
2
-
3
- __all__ = ['ita']
4
-
5
- def ita[A](*args: A) -> Iterator[A]: ...
@@ -1,3 +0,0 @@
1
- __all__ = ['lca']
2
-
3
- def lca(cls1: type, cls2: type) -> type: ...