pythonic-fp-gadgets 4.0.2__py3-none-any.whl → 4.0.4__py3-none-any.whl

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.
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2025 Geoffrey R. Scheller
1
+ # Copyright 2023-2026 Geoffrey R. Scheller
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -14,14 +14,15 @@
14
14
 
15
15
  """
16
16
  Gadgets
17
- -------
17
+ =======
18
18
 
19
19
  .. admonition:: Collection of mostly self-contained functions and classes
20
20
 
21
21
  - Functions and classes which could go multiple places or have
22
22
  no good place to go.
23
23
  - Self-contained with minimal dependencies.
24
- - No pythonic_fp dependencies.
24
+
25
+ - No pythonic_fp dependencies at all.
25
26
 
26
27
  """
27
28
 
@@ -31,31 +32,33 @@ from inspect import getmro
31
32
  __all__ = ['first_common_ancestor', 'iterate_over_arguments']
32
33
 
33
34
  __author__ = 'Geoffrey R. Scheller'
34
- __copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
35
+ __copyright__ = 'Copyright (c) 2023-2026 Geoffrey R. Scheller'
35
36
  __license__ = 'Apache License 2.0'
36
37
 
37
38
 
38
39
  def first_common_ancestor(cls1: type, cls2: type) -> type:
39
- """Find the least upper bound in the inheritance graph
40
- of two classes.
40
+ """
41
+ .. admonition:: Least upper bound
41
42
 
42
- .. warning::
43
+ Find the least upper bound in the inheritance graph
44
+ of two classes.
43
45
 
44
- This function can fail with a TypeError. Some error messages
45
- seen are
46
+ :param cls1: A class in the inheritance hierarchy.
47
+ :param cls2: A class in the inheritance hierarchy.
48
+ :returns: First common ancestor based on ``getmro`` order.
49
+ :raises TypeError: Raised when no common ancestor exists, or when
50
+ not caught when raised by ``inspect.getmro``.
46
51
 
47
- - multiple bases have instance lay-out conflict
48
- - type 'bool' is not an acceptable base type
52
+ .. warning::
49
53
 
50
- This happens frequently when the function is given
51
- Python builtin types or in multiple inheritance situations.
54
+ This function can fail with a TypeError. Some error messages
55
+ seen are
52
56
 
53
- :param cls1: A class in the inheritance hierarchy.
54
- :param cls2: A class in the inheritance hierarchy.
55
- :returns: First common ancestor based on getmro order.
56
- :raises TypeError: Raised when no common ancestor or not
57
- caught when raised by ``inspect.getmro``.
57
+ - multiple bases have instance lay-out conflict
58
+ - type 'bool' is not an acceptable base type
58
59
 
60
+ This happens frequently when the function is given
61
+ Python builtin types or in multiple inheritance situations.
59
62
  """
60
63
  if issubclass(cls1, cls2):
61
64
  return cls2
@@ -69,17 +72,17 @@ def first_common_ancestor(cls1: type, cls2: type) -> type:
69
72
 
70
73
 
71
74
  def iterate_over_arguments[A](*args: A) -> Iterator[A]:
72
- """Function returning an iterator of its arguments.
75
+ """
76
+ .. admonition:: Iterate over arguments.
73
77
 
74
- .. note::
78
+ Function returning an iterator over its arguments.
75
79
 
76
- Does not create an object to iterate over.
80
+ :param args: Objects to iterate over.
81
+ :returns: An iterator of the function's arguments.
77
82
 
78
- - well, not in the Python world
79
- - maybe in the C world
83
+ .. note::
80
84
 
81
- :param args: Objects to iterate over.
82
- :returns: An iterator of the functions arguments.
85
+ Does not create a Python object to iterate over.
83
86
 
84
87
  """
85
88
  yield from args
@@ -1,6 +1,6 @@
1
1
  from collections.abc import Iterator
2
2
 
3
- __all__ = ['iterate_over_arguments', 'first_common_ancestor']
3
+ __all__ = ['first_common_ancestor', 'iterate_over_arguments']
4
4
 
5
- def iterate_over_arguments[A](*args: A) -> Iterator[A]: ...
6
5
  def first_common_ancestor(cls1: type, cls2: type) -> type: ...
6
+ def iterate_over_arguments[A](*args: A) -> Iterator[A]: ...
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2025 Geoffrey R. Scheller
1
+ # Copyright 2023-2026 Geoffrey R. Scheller
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -12,29 +12,30 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __all__ = ['Box']
16
-
17
15
  from collections.abc import Callable, Iterator
18
16
  from typing import cast, Final, overload
19
17
 
18
+ __all__ = ['Box']
19
+
20
20
  type _Sentinel = object
21
21
  _sentinel: Final[_Sentinel] = object()
22
22
 
23
23
 
24
24
  class Box[T]:
25
- """Container holding at most one item of a given type.
26
-
27
- .. note::
25
+ """
26
+ .. admonition:: Box
28
27
 
29
- - ``Box(item: T)``: contains at one item of type ``T``
30
- - ``Box[T]()``: creates empty container
28
+ Container holding at most one item of a given type.
31
29
 
32
- Where type ``T`` is some definite type, which
33
- could be ``None`` or even ``Never``.
30
+ .. tip::
34
31
 
35
- .. tip ::
32
+ Objects of the ``Box`` type
36
33
 
37
- ``Box`` objects can be used in Python match statements.
34
+ - Are truthy if not empty.
35
+ - Can be combined with other iterators before being filled.
36
+ - Can be used like a promise.
37
+ - Can be used in Python match statements.
38
+ - Threadsafe
38
39
 
39
40
  """
40
41
  __slots__ = ('_item',)
@@ -47,34 +48,57 @@ class Box[T]:
47
48
 
48
49
  def __init__(self, item: T | _Sentinel = _sentinel) -> None:
49
50
  """
50
- :param item: An optional initial contained ``item``
51
- for the ``Box``.
51
+ .. admonition:: Initializer
52
+
53
+ Initialize ``Box`` with 0 or 1 items.
54
+
55
+ :param item: An optional initial ``item`` for the ``Box``.
56
+
52
57
  """
53
58
  self._item = item
54
59
 
55
60
  def __bool__(self) -> bool:
61
+ """
62
+ .. admonition:: Bool
63
+
64
+ Truthy if not empty.
65
+
66
+ """
56
67
  return self._item is not _sentinel
57
68
 
58
69
  def __iter__(self) -> Iterator[T]:
59
- if self:
60
- yield cast(T, self._item)
70
+ """
71
+ .. admonition:: Iterability
61
72
 
62
- def __repr__(self) -> str:
73
+ Iterates boxed item.
74
+
75
+ """
63
76
  if self:
64
- return 'Box(' + repr(self._item) + ')'
65
- return 'Box()'
77
+ yield cast(T, self._item)
66
78
 
67
79
  def __len__(self) -> int:
80
+ """
81
+ .. admonition:: Length
82
+
83
+ - 1 if ``Box`` contains an item
84
+ - 0 if ``Box`` is empty
85
+
86
+ :returns: The number of items currently in the ``Box``.
87
+
88
+ """
68
89
  return 1 if self else 0
69
90
 
70
91
  def __eq__(self, other: object) -> bool:
71
92
  """
72
- Efficiently compare to another object.
93
+ .. admonition:: Equality comparison
94
+
95
+ Efficiently compare ``Box`` to another object.
73
96
 
74
- :param other: The object to be compared with,
75
- :returns: ``True`` if ``other`` is of type Box and contains
97
+ :param other: The object to be compared.
98
+ :returns: ``True`` if ``other`` is another ``Box`` and contains
76
99
  an object which compares as equal to the object
77
100
  contained in the ``Box``, otherwise ``False``.
101
+
78
102
  """
79
103
  if not isinstance(other, type(self)):
80
104
  return False
@@ -85,16 +109,34 @@ class Box[T]:
85
109
  return True
86
110
  return False
87
111
 
112
+ def __repr__(self) -> str:
113
+ """
114
+ .. admonition:: Representation string
115
+
116
+ Construct string 'Box()' if empty, otherwise 'Box(item_repr)'
117
+ where ``item_repr = repr(item)`` for the currently contained
118
+ item.
119
+
120
+ :returns: A string to reproduce the current state of the ``Box``.
121
+
122
+ """
123
+ if self:
124
+ return 'Box(' + repr(self._item) + ')'
125
+ return 'Box()'
126
+
88
127
  @overload
89
128
  def get(self) -> T: ...
90
129
  @overload
91
130
  def get(self, alt: T) -> T: ...
92
131
 
93
132
  def get(self, alt: T | _Sentinel = _sentinel) -> T:
94
- """Return the contained item, if it exists, otherwise
95
- an alternate item, if given.
133
+ """
134
+ .. admonition:: Get
135
+
136
+ Return the boxed item, if it exists, otherwise
137
+ an alternate item, if given.
96
138
 
97
- :param alt: An optional item of type ``T`` to return
139
+ :param alt: An optional item of type ``T`` to return
98
140
  if the ``Box`` is empty.
99
141
  :returns: Contents of ``Box`` or an alternate item, if given,
100
142
  when the ``Box`` is empty.
@@ -110,7 +152,10 @@ class Box[T]:
110
152
  return cast(T, alt)
111
153
 
112
154
  def pop(self) -> T:
113
- """Pop the contained item if ``Box`` is not empty.
155
+ """
156
+ .. admonition:: Pop
157
+
158
+ Pop item from ``Box`` if not empty.
114
159
 
115
160
  :returns: The item contained in the ``Box``.
116
161
  :raises ValueError: If Box is empty.
@@ -124,7 +169,10 @@ class Box[T]:
124
169
  return popped
125
170
 
126
171
  def push(self, item: T) -> None:
127
- """Push an item into an empty ``Box``.
172
+ """
173
+ .. admonition:: Push
174
+
175
+ Push an item into ``Box`` if empty.
128
176
 
129
177
  :param item: Item to push into the empty ``Box``.
130
178
  :raises ValueError: If ``Box`` is not empty.
@@ -138,15 +186,24 @@ class Box[T]:
138
186
  return None
139
187
 
140
188
  def put(self, item: T) -> None:
141
- """Put an item in the Box. Discard any previous contents."""
189
+ """
190
+ .. admonition:: Put
191
+
192
+ Put an item in the Box. Discard any previous contents.
193
+
194
+ """
142
195
  self._item = item
143
196
 
144
197
  def exchange(self, new_item: T) -> T:
145
- """Exchange an item with what is in the Box.
198
+ """
199
+ .. admonition:: Exchange
200
+
201
+ Exchange an item with what is in the Box.
146
202
 
147
203
  :param ``new_item``: New item to exchange for current item.
148
204
  :returns: Original contents of the ``Box``.
149
205
  :raises ValueError: If Box is empty.
206
+
150
207
  """
151
208
  if self._item is _sentinel:
152
209
  msg = 'Box: Trying to exchange items from an empty Box'
@@ -156,11 +213,14 @@ class Box[T]:
156
213
  return popped
157
214
 
158
215
  def map[U](self, f: Callable[[T], U]) -> 'Box[U]':
159
- """Map function ``f`` over contents. We need to return a new
160
- instance since the type of Box can change.
216
+ """
217
+ .. admonition:: Map
218
+
219
+ Map function ``f`` over contents. We need to return a new
220
+ instance since the type of Box can change.
161
221
 
162
222
  :param f: Mapping function.
163
- :returns: A new instance.
223
+ :returns: New instance.
164
224
 
165
225
  """
166
226
  if self._item is _sentinel:
@@ -168,10 +228,13 @@ class Box[T]:
168
228
  return Box(f(cast(T, self._item)))
169
229
 
170
230
  def bind[U](self, f: Callable[[T], 'Box[U]']) -> 'Box[U]':
171
- """Flatmap ``Box`` with function ``f``.
231
+ """
232
+ .. admonition:: Bind
233
+
234
+ Flatmap ``Box`` with function ``f``.
172
235
 
173
236
  :param f: Binding function.
174
- :returns: A new instance.
237
+ :returns: New instance.
175
238
 
176
239
  """
177
240
  if self._item is _sentinel:
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2025 Geoffrey R. Scheller
1
+ # Copyright 2023-2026 Geoffrey R. Scheller
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -12,6 +12,17 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __author__ = 'Geoffrey R. Scheller'
16
- __copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
17
- __license__ = 'Apache License 2.0'
15
+ """
16
+ sentinels
17
+ ---------
18
+
19
+ .. admonition:: Sentinel values
20
+
21
+ - Singletons
22
+ - Threadsafe
23
+ - Useful for both
24
+
25
+ - referenced objects
26
+ - hidden implementation details
27
+
28
+ """
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2025 Geoffrey R. Scheller
1
+ # Copyright 2023-2026 Geoffrey R. Scheller
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -12,29 +12,6 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- """
16
- .. admonition:: Sentinel values labeled by different (hashable) flavors.
17
-
18
- When different flavors of the truth are needed.
19
-
20
- .. note::
21
-
22
- Can be compared using ``==`` and ``!=``. A flavored sentinel
23
- value always equals itself and never equals anything else,
24
- especially other flavored sentinel values.
25
-
26
- Useful for union types where ``Sentinel[H]`` is one of the
27
- types making up the union.
28
-
29
- To ensure that reference equality is used, put the known
30
- sentinel value first in the comparison.
31
-
32
- .. note::
33
-
34
- Threadsafe.
35
-
36
- """
37
-
38
15
  import threading
39
16
  from typing import ClassVar, final, Hashable
40
17
 
@@ -43,12 +20,34 @@ __all__ = ['Sentinel']
43
20
 
44
21
  @final
45
22
  class Sentinel[H: Hashable]:
23
+ """
24
+ .. admonition:: Sentinel
25
+
26
+ Sentinel values labeled by different (hashable) flavors.
27
+
28
+ .. note::
29
+
30
+ - Useful for union types.
31
+ - A flavored ``Sentinel`` value always equals itself
32
+ and never equals anything else, especially other
33
+ flavored sentinel values.
34
+
35
+ """
36
+
46
37
  __slots__ = ('_flavor',)
47
38
 
48
39
  _flavors: 'dict[H, Sentinel[H]]' = {}
49
40
  _lock: ClassVar[threading.Lock] = threading.Lock()
50
41
 
51
42
  def __new__(cls, flavor: H) -> 'Sentinel[H]':
43
+ """
44
+ .. admonition:: new
45
+
46
+ :param flavor: Hashable value determining which
47
+ flavored ``Sentinel`` to return.
48
+ :returns: The ``Sentinel(flavor)`` singleton instance.
49
+
50
+ """
52
51
  if flavor not in cls._flavors:
53
52
  with cls._lock:
54
53
  if flavor not in cls._flavors:
@@ -57,18 +56,44 @@ class Sentinel[H: Hashable]:
57
56
 
58
57
  def __init__(self, flavor: H) -> None:
59
58
  """
60
- :param flavor: Some Hashable value of generic type ``H``.
61
- :returns: The ``Sentinel`` singleton instance with flavor ``flavor``.
62
- :rtype: ``Sentinel[H]`` where ``H`` is a subtype of Hashable.
59
+ .. admonition:: init
60
+
61
+ :param flavor: Hashable value to initially cache the flavor
62
+ :type flavor: ``H: Hashable``
63
+
63
64
  """
64
65
  if not hasattr(self, '_flavor'):
65
66
  self._flavor = flavor
66
67
 
67
68
  def __repr__(self) -> str:
69
+ """
70
+ .. admonition:: repr string
71
+
72
+ Construct string 'Sentinel(flavor)' where the flavor
73
+ is displayed with ``repr()``.
74
+
75
+ :returns: A string to reproduce the flavored sentinel.
76
+
77
+ """
68
78
  return "Sentinel('" + repr(self._flavor) + "')"
69
79
 
80
+ def __str__(self) -> str:
81
+ """
82
+ .. admonition:: user string
83
+
84
+ Construct string 'Sentinel(flavor)' where the flavor
85
+ is displayed with ``str()``.
86
+
87
+ :returns: A string meaningful to an end user.
88
+
89
+ """
90
+ return "Sentinel('" + str(self._flavor) + "')"
91
+
70
92
  def flavor(self) -> H:
71
93
  """
72
- :returns: The sentinel's flavor. A ``Hashable`` value of type ``H``.
94
+ .. admonition:: get flavor
95
+
96
+ :returns: The sentinel's flavor.
97
+
73
98
  """
74
99
  return self._flavor
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2025 Geoffrey R. Scheller
1
+ # Copyright 2023-2026 Geoffrey R. Scheller
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -12,112 +12,176 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- """
16
- .. admonition:: Singleton class representing an actually,
17
- not potentially, missing value.
15
+ import threading
16
+ from typing import ClassVar, final
18
17
 
19
- ``NoValue()`` is a singleton object representing a missing value.
18
+ __all__ = ['NoValue']
20
19
 
21
- While ``None`` and ``()`` are frequently used as sentinel values,
22
- I prefer to think of them as
23
20
 
24
- - ``None``: Returns, or returned, no values.
25
- - ``()``: An empty, possibly typed, iterable collection.
21
+ @final
22
+ class NoValue:
23
+ """
24
+ .. admonition:: missing value
26
25
 
27
- .. important::
26
+ Singleton class representing an actual, not
27
+ potential, missing value.
28
28
 
29
- Given variables
29
+ While ``None`` and ``()`` are frequently used as sentinel values,
30
+ I prefer to think of them as
30
31
 
31
- .. code:: python
32
+ - ``None`` as returns, or returned, no values.
33
+ - ``()`` as an empty, possibly typed, iterable collection.
32
34
 
33
- x: int | NoValue
34
- y: int | NoValue
35
+ .. important::
35
36
 
36
- Equality between ``x`` and ``y`` means both values exist and compare
37
- as equal. If one or both of theses values are missing, then what is
38
- there to compare?
37
+ Given variables
39
38
 
40
- .. table:: ``x == y``
39
+ .. code:: python
41
40
 
42
- +-----------+-----------+--------+--------+
43
- | x∖y | NoValue() | 42 | 57 |
44
- +===========+===========+========+========+
45
- | NoValue() | false | false | false |
46
- +-----------+-----------+--------+--------+
47
- | 42 | false | true | false |
48
- +-----------+-----------+--------+--------+
49
- | 57 | false | false | true |
50
- +-----------+-----------+--------+--------+
41
+ x: int | NoValue
42
+ y: int | NoValue
51
43
 
52
- Similarly for not equals.
44
+ Equality between ``x`` and ``y`` means both values exist
45
+ and compare as equal.
53
46
 
54
- .. table:: ``x != y``:wq
47
+ .. table:: ``x == y``
55
48
 
56
- +-----------+-----------+--------+--------+
57
- | x∖y | NoValue() | 42 | 57 |
58
- +===========+===========+========+========+
59
- | NoValue() | false | false | false |
60
- +-----------+-----------+--------+--------+
61
- | 42 | false | false | true |
62
- +-----------+-----------+--------+--------+
63
- | 57 | false | true | false |
64
- +-----------+-----------+--------+--------+
49
+ +-------------------+-------------------+------------+------------+
50
+ | | | | |
51
+ +===================+===================+============+============+
52
+ | | ``y = NoValue()`` | ``y = 42`` | ``y = 57`` |
53
+ +-------------------+-------------------+------------+------------+
54
+ | ``x = NoValue()`` | ``False`` | ``False`` | ``False`` |
55
+ +-------------------+-------------------+------------+------------+
56
+ | ``x = 42`` | ``False`` | ``True`` | ``False`` |
57
+ +-------------------+-------------------+------------+------------+
58
+ | ``x = 57`` | ``False`` | ``False`` | ``True`` |
59
+ +-------------------+-------------------+------------+------------+
65
60
 
66
- .. warning::
61
+ .. table:: ``x != y``
67
62
 
68
- Only use ``==`` or ``!=`` in value comparisons. To directly
69
- identity the ``NoValue`` singleton, use ``is`` and ``is not``
70
- instead.
63
+ +-------------------+-------------------+------------+------------+
64
+ | | | | |
65
+ +===================+===================+============+============+
66
+ | | ``y = NoValue()`` | ``y = 42`` | ``y = 57`` |
67
+ +-------------------+-------------------+------------+------------+
68
+ | ``x = NoValue()`` | ``False`` | ``False`` | ``False`` |
69
+ +-------------------+-------------------+------------+------------+
70
+ | ``x = 42`` | ``False`` | ``False`` | ``True`` |
71
+ +-------------------+-------------------+------------+------------+
72
+ | ``x = 57`` | ``False`` | ``True`` | ``False`` |
73
+ +-------------------+-------------------+------------+------------+
71
74
 
72
- .. note::
75
+ .. warning::
73
76
 
74
- Threadsafe.
77
+ - use ``==`` or ``!=`` only in value comparisons
78
+ - use ``is`` and ``is not`` to identity the ``NoValue()``
79
+ singleton itself
75
80
 
76
- .. tip::
81
+ .. tip::
77
82
 
78
- Use as a hidden implementation detail when creating "optional"
79
- arguments to functions and methods.
83
+ Use in a union type when creating "optional" arguments
84
+ to functions and methods.
80
85
 
81
- To help ensure the abstraction does not leak,
86
+ To help ensure the abstraction stays a hidden implementation
87
+ detail and does not leak out into user code,
82
88
 
83
- - Do not export the sentinel value.
84
- - Use ``@overload`` to keep the NoValue type out of documentation and IDEs.
85
-
86
- """
87
- import threading
88
- from typing import ClassVar, final
89
+ - Do not export the sentinel value.
89
90
 
90
- __all__ = ['NoValue']
91
+ - A new reference can always be generated via ``NoValue()``.
91
92
 
93
+ - Use ``@overload`` to keep the ``NoValue`` type out of
94
+ documentation and IDEs.
95
+
96
+ """
92
97
 
93
- @final
94
- class NoValue():
95
98
  __slots__ = ()
96
99
 
97
100
  _instance: 'ClassVar[NoValue | None]' = None
98
101
  _lock: ClassVar[threading.Lock] = threading.Lock()
102
+ _hash: ClassVar[int] = 0
99
103
 
100
104
  def __new__(cls) -> 'NoValue':
101
105
  """
102
- :returns: The ``NoValue`` singleton instance.
106
+ .. admonition:: new
107
+
108
+ :returns: The ``NoValue()`` singleton instance.
109
+
103
110
  """
104
111
  if cls._instance is None:
105
112
  with cls._lock:
106
113
  if cls._instance is None:
114
+ cls._hash = id(cls)
107
115
  cls._instance = super().__new__(cls)
108
116
  return cls._instance
109
117
 
118
+ def __hash__(self) -> int:
119
+ """
120
+ .. admonition:: hash
121
+
122
+ :returns: The singleton's unique integer hash value.
123
+ """
124
+ return type(self)._hash
125
+
110
126
  def __repr__(self) -> str:
127
+ """
128
+ .. admonition:: repr string
129
+
130
+ :returns: The string 'NoValue()'.
131
+
132
+ """
111
133
  return 'NoValue()'
112
134
 
135
+ def __bool__(self) -> bool:
136
+ """
137
+ .. admonition:: bool
138
+
139
+ Always falsy.
140
+
141
+ :returns: False
142
+
143
+ .. tip
144
+
145
+ Can be used to provide a fallback value when used
146
+ with Python shortcut logic.
147
+
148
+ .. code:: python
149
+
150
+ result: str | NoValue = NoValue()
151
+ if predicate(x):
152
+ result = 'some non-empty string'
153
+ value = result or 'fallback string'
154
+
155
+ """
156
+ return False
157
+
113
158
  def __eq__(self, other: object) -> bool:
114
159
  """
115
- :returns: ``False``
160
+ .. admonition:: Equality comparison
161
+
162
+ :param other: The object to be compared.
163
+ :returns: ``False`` even if compared to itself.
164
+
165
+ .. warning::
166
+
167
+ - non-standard comparison semantics
168
+ - always returns ``False``
169
+ - if one or both values are missing,
170
+ then what is there to compare?
171
+
116
172
  """
117
173
  return False
118
174
 
119
175
  def __ne__(self, other: object) -> bool:
120
176
  """
121
- :returns: ``False``
177
+ .. admonition:: not equal
178
+
179
+ :returns: ``False``
180
+
181
+ .. warning::
182
+
183
+ - non-standard comparison semantics
184
+ - always returns ``False``
185
+
122
186
  """
123
187
  return False
@@ -2,5 +2,7 @@ __all__ = ['NoValue']
2
2
 
3
3
  class NoValue:
4
4
  def __new__(cls) -> NoValue: ...
5
+ def __hash__(self) -> int: ...
6
+ def __bool__(self) -> bool: ...
5
7
  def __eq__(self, other: object) -> bool: ...
6
8
  def __ne__(self, other: object) -> bool: ...
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2025 Geoffrey R. Scheller
1
+ # Copyright 2023-2026 Geoffrey R. Scheller
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -11,6 +11,15 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ """
15
+ .. admonition:: Module wrap
16
+
17
+ Wrap objects in ways to make them "immutable."
18
+
19
+ - Class ``Wrap``: wrap an object
20
+ - Class ``HWrap``: wrap a hashable object
21
+
22
+ """
14
23
 
15
24
  __all__ = ['Wrap', 'HWrap']
16
25
 
@@ -18,37 +27,71 @@ from collections.abc import Callable, Iterator, Hashable
18
27
 
19
28
 
20
29
  class Wrap[T]():
21
- """Immutablely wrap exactly one value of a given type.
22
-
23
- .. tip::
30
+ """
31
+ .. admonition:: Wrap object
24
32
 
25
- ``Wrap`` objects can be used in Python match statements.
33
+ Immutablely wrap exactly one value of a
34
+ given type. ``Wrap`` objects can be used
35
+ in Python match statements.
26
36
 
27
37
  """
28
38
  __slots__ = ('_item',)
29
39
  __match_args__ = ('_item',)
30
40
 
31
41
  def __init__(self, item: T) -> None:
42
+ """
43
+ .. admonition:: Initializer
44
+
45
+ Initialize ``Wrap`` with 1 required item.
46
+
47
+ :param item: Item to be wrapped.
48
+
49
+ """
32
50
  self._item = item
33
51
 
34
52
  def __bool__(self) -> bool:
53
+ """
54
+ .. admonition:: Bool
55
+
56
+ Truthiness same as wrapped object.
57
+
58
+ """
35
59
  return bool(self._item)
36
60
 
37
61
  def __iter__(self) -> Iterator[T]:
62
+ """
63
+ .. admonition:: Iter
64
+
65
+ Iterable, iterates wrapped item.
66
+
67
+ """
38
68
  if self:
39
69
  yield self._item
40
70
 
41
- def __repr__(self) -> str:
42
- return 'Wrap(' + repr(self._item) + ')'
71
+ def __str__(self) -> str:
72
+ """
73
+ .. admonition:: User string
74
+
75
+ Construct string 'Box(item_str)'
76
+ where ``item_str = str(item)`` for the currently contained
77
+ item.
78
+
79
+ :returns: A string to reproduce the current state of the ``Box``.
80
+
81
+ """
82
+ return 'Wrap(' + str(self._item) + ')'
43
83
 
44
84
  def __eq__(self, other: object) -> bool:
45
85
  """
46
- Efficiently compare to another object.
86
+ .. admonition:: Equality comparison
87
+
88
+ Efficiently compare ``Wrap`` to another object.
47
89
 
48
90
  :param other: The object to be compared with,
49
91
  :returns: ``True`` if ``other`` is of type Wrap and wraps
50
92
  an object which compares as equal to the wrapped
51
93
  object, otherwise ``False``.
94
+
52
95
  """
53
96
  if not isinstance(other, type(self)):
54
97
  return False
@@ -58,36 +101,46 @@ class Wrap[T]():
58
101
  return self._item == other._item
59
102
 
60
103
  def map[U](self, f: Callable[[T], U]) -> 'Wrap[U]':
61
- """Map function ``f`` over contents.
104
+ """
105
+ .. admonition:: Map
106
+
107
+ Map function ``f`` over contents.
108
+
109
+ Map function ``f`` over contents.
62
110
 
63
111
  :param f: Mapping function.
64
- :returns: A new instance.
112
+ :returns: New instance.
65
113
 
66
114
  """
67
115
  return Wrap(f(self._item))
68
116
 
69
117
  def bind[U](self, f: Callable[[T], 'Wrap[U]']) -> 'Wrap[U]':
70
- """Flatmap the ``Wrap`` with function ``f``.
118
+ """
119
+ .. admonition:: Bind
120
+
121
+ Flatmap wrapped object with function ``f``.
71
122
 
72
123
  :param f: Binding function.
73
- :returns: A new instance.
124
+ :returns: New instance.
74
125
 
75
126
  """
76
127
  return f(self._item)
77
128
 
78
129
 
79
130
  class HWrap[T: Hashable](Hashable):
80
- """Immutablely wrap exactly one hashable value of a given type.
81
-
82
- .. tip::
131
+ """
132
+ .. admonition:: Wrap hashable object
83
133
 
84
- ``HWrap`` objects can be used in Python match statements.
134
+ Immutablely wrap exactly one value of a
135
+ given hashable type. ``HWrap`` objects can
136
+ be used in Python match statements.
85
137
 
86
- .. tip::
138
+ .. tip::
87
139
 
88
- ``HWrap`` objects are hashable..
140
+ ``HWrap`` objects are hashable.
89
141
 
90
142
  """
143
+
91
144
  __slots__ = ('_item', '_hash')
92
145
  __match_args__ = ('_item',)
93
146
 
@@ -98,9 +151,21 @@ class HWrap[T: Hashable](Hashable):
98
151
  return self._hash
99
152
 
100
153
  def __bool__(self) -> bool:
154
+ """
155
+ .. admonition:: Bool
156
+
157
+ Truthiness same as wrapped object.
158
+
159
+ """
101
160
  return bool(self._item)
102
161
 
103
162
  def __iter__(self) -> Iterator[T]:
163
+ """
164
+ .. admonition:: Iter
165
+
166
+ Iterable, iterates wrapped item.
167
+
168
+ """
104
169
  if self:
105
170
  yield self._item
106
171
 
@@ -109,12 +174,15 @@ class HWrap[T: Hashable](Hashable):
109
174
 
110
175
  def __eq__(self, other: object) -> bool:
111
176
  """
112
- Efficiently compare to another object.
177
+ .. admonition:: Equality comparison
113
178
 
114
- :param other: The object to be compared with,
179
+ Efficiently compare to another object.
180
+
181
+ :param other: Object to be compared
115
182
  :returns: ``True`` if ``other`` is of type HWrap and wraps
116
183
  an object which compares as equal to the wrapped
117
184
  object, otherwise ``False``.
185
+
118
186
  """
119
187
  if not isinstance(other, type(self)):
120
188
  return False
@@ -126,19 +194,26 @@ class HWrap[T: Hashable](Hashable):
126
194
  return self._item == other._item
127
195
 
128
196
  def map[U](self, f: Callable[[T], U]) -> 'HWrap[U]':
129
- """Map function ``f`` over contents.
197
+ """
198
+ .. admonition:: Map
199
+
200
+ Map function ``f`` over wrapped the wrapped object
201
+ returning a new ``HWrap`` instance.
130
202
 
131
203
  :param f: Mapping function.
132
- :returns: A new instance.
204
+ :returns: New instance.
133
205
 
134
206
  """
135
207
  return HWrap(f(self._item))
136
208
 
137
209
  def bind[U](self, f: Callable[[T], 'HWrap[U]']) -> 'HWrap[U]':
138
- """Flatmap the ``Wrap`` with function ``f``.
210
+ """
211
+ .. admonition:: Bind
212
+
213
+ Flatmap ``Box`` with function ``f``.
139
214
 
140
215
  :param f: Binding function.
141
- :returns: A new instance.
216
+ :returns: New instance.
142
217
 
143
218
  """
144
219
  return f(self._item)
@@ -1,24 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonic-fp-gadgets
3
- Version: 4.0.2
3
+ Version: 4.0.4
4
4
  Summary: Gadgets
5
5
  Keywords: gadgets
6
6
  Author-email: "Geoffrey R. Scheller" <geoffrey@scheller.com>
7
7
  Requires-Python: >=3.13
8
8
  Description-Content-Type: text/x-rst
9
- Classifier: Development Status :: 4 - Beta
9
+ Classifier: Development Status :: 3 - Alpha
10
10
  Classifier: Framework :: Pytest
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
13
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Typing :: Typed
16
16
  License-File: LICENSE
17
17
  Requires-Dist: pytest>=8.4.1 ; extra == "test"
18
- Requires-Dist: pythonic-fp-circulararray>=6.0.1 ; extra == "test"
18
+ Requires-Dist: pythonic-fp-circulararray>=6.0.4 ; 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://grscheller.github.io/pythonic-fp/homepage/build/html/
20
+ Project-URL: Documentation, https://grscheller.github.io/pythonic-fp/projects/gadgets.html
21
+ Project-URL: Homepage, https://grscheller.github.io/pythonic-fp/
22
22
  Project-URL: Source, https://github.com/grscheller/pythonic-fp-gadgets
23
23
  Provides-Extra: test
24
24
 
@@ -27,7 +27,7 @@ Pythonic FP - Gadgets
27
27
 
28
28
  PyPI project
29
29
  `pythonic-fp-gadgets
30
- <https://pypi.org/project/pythonic-fp>`_.
30
+ <https://pypi.org/project/pythonic-fp-gadgets>`_.
31
31
 
32
32
  Library of simple, but useful, classes and functions with no dependencies
33
33
  outside the Python Standard Library.
@@ -35,24 +35,26 @@ outside the Python Standard Library.
35
35
  - Gadgets
36
36
 
37
37
  - single item box
38
+ - immutable wrapped (hashable) references
38
39
  - function returning iterator of its arguments
39
40
  - function to find the latest common ancestor of two classes
41
+ - sentinel values
40
42
 
41
43
  Part of the
42
44
  `pythonic-fp
43
- <https://grscheller.github.io/pythonic-fp>`_
45
+ <https://grscheller.github.io/pythonic-fp/>`_
44
46
  PyPI projects.
45
47
 
46
48
  Documentation
47
49
  -------------
48
50
 
49
- Documentation for this project is hosted on
51
+ Documentation and other links for this project are hosted on
50
52
  `GitHub Pages
51
- <https://grscheller.github.io/pythonic-fp/gadgets>`_.
53
+ <https://grscheller.github.io/pythonic-fp/projects/gadgets.html>`_.
52
54
 
53
55
  Copyright and License
54
56
  ---------------------
55
57
 
56
- Copyright (c) 2025 Geoffrey R. Scheller. Licensed under the Apache
58
+ Copyright (c) 2025-2026 Geoffrey R. Scheller. Licensed under the Apache
57
59
  License, Version 2.0. See the LICENSE file for details.
58
60
 
@@ -0,0 +1,17 @@
1
+ pythonic_fp/gadgets/__init__.py,sha256=-pgzuWq-DTORXvy8JDQpnNPhQou85oiySH2-gArNeWc,2806
2
+ pythonic_fp/gadgets/__init__.pyi,sha256=-O921ioE28UPnDm_EL928vl6dDxhrfXHBQenusBo1_o,224
3
+ pythonic_fp/gadgets/box.py,sha256=Dx5wF3x9LyUs5MHjvHP6yTM05f4ojHPqqzfjuXJOo8I,6580
4
+ pythonic_fp/gadgets/box.pyi,sha256=UkJBY8Xiv4oe70Q3bhJL1jGHwYmFWwkTIdxvf2zR3v4,837
5
+ pythonic_fp/gadgets/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ pythonic_fp/gadgets/wrap.py,sha256=tAnG7P3EYl_1-CH2cs7IfY-0h4LET1LEIjmMo26OzCg,5388
7
+ pythonic_fp/gadgets/wrap.pyi,sha256=TRbBph_517VNCKchwU-pdtPCn73c09gq8EchDFM7Uyo,868
8
+ pythonic_fp/gadgets/sentinels/__init__.py,sha256=Xh63-GTwiqqcBqs5OEYBSiLXuWtq_CVHfUz0mKO_2H0,775
9
+ pythonic_fp/gadgets/sentinels/__init__.pyi,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ pythonic_fp/gadgets/sentinels/flavored.py,sha256=LPmA-XOadcN5gJgxxo2prSnwRvq8bJ7o0Ae2_Y70Au0,2774
11
+ pythonic_fp/gadgets/sentinels/flavored.pyi,sha256=K3YSyXLyylPII2ahRP0olhzLJruOeR73TFJDC6rCUwk,212
12
+ pythonic_fp/gadgets/sentinels/novalue.py,sha256=gaQNm1TRtphbmWgDV8O0sblH7eU2M-Zymu_B67mwsJ8,6097
13
+ pythonic_fp/gadgets/sentinels/novalue.pyi,sha256=1_iUI6FEwYma8P4XXVCb6CKhSYmfq53S8dLmiwNtJw4,244
14
+ pythonic_fp_gadgets-4.0.4.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
15
+ pythonic_fp_gadgets-4.0.4.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
16
+ pythonic_fp_gadgets-4.0.4.dist-info/METADATA,sha256=SbxLXeCZA7LOtg3Q_mpBPlux1FR6-shkI9S2tutdVgk,1895
17
+ pythonic_fp_gadgets-4.0.4.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- pythonic_fp/gadgets/__init__.py,sha256=iEm1j8DIchgQvH4Tom6IyMeYz8i3LSAXBBBtz_gPykE,2673
2
- pythonic_fp/gadgets/__init__.pyi,sha256=cAf10WX2ZHMs-Tf93mImmDdZw7lNEmwjorkiVF04s_E,224
3
- pythonic_fp/gadgets/box.py,sha256=cmHe2kZhEMNtH51xlpdyd68SV-bBXxC6gRH8N0W6TqE,5443
4
- pythonic_fp/gadgets/box.pyi,sha256=UkJBY8Xiv4oe70Q3bhJL1jGHwYmFWwkTIdxvf2zR3v4,837
5
- pythonic_fp/gadgets/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- pythonic_fp/gadgets/wrap.py,sha256=x3mvawiRh9G3vMq3ub5Va6qBJgKcvkCKvWeR6pWrJBw,3929
7
- pythonic_fp/gadgets/wrap.pyi,sha256=TRbBph_517VNCKchwU-pdtPCn73c09gq8EchDFM7Uyo,868
8
- pythonic_fp/gadgets/sentinels/__init__.py,sha256=81T-HGQ9BgXj57PO_pduo8MpSQpRgZwT8LIXwODhHkI,724
9
- pythonic_fp/gadgets/sentinels/__init__.pyi,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- pythonic_fp/gadgets/sentinels/flavored.py,sha256=sXgjhBPHEulH9C3Yc09PwAPDscfRH2vKj5JldgS6Sak,2260
11
- pythonic_fp/gadgets/sentinels/flavored.pyi,sha256=K3YSyXLyylPII2ahRP0olhzLJruOeR73TFJDC6rCUwk,212
12
- pythonic_fp/gadgets/sentinels/novalue.py,sha256=UbyQ4AVTdM2flLmF2WE0sh7_dErPQiTeM8U6GdWaHN4,3602
13
- pythonic_fp/gadgets/sentinels/novalue.pyi,sha256=zhZdePpWoQ3ZXOq7xQWGR0A4W_XzasdBQKcoywXZEZI,173
14
- pythonic_fp_gadgets-4.0.2.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
15
- pythonic_fp_gadgets-4.0.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
16
- pythonic_fp_gadgets-4.0.2.dist-info/METADATA,sha256=GsyqIcxiofODEI34GKxzySI-FBv-pkwxaEhnTfZzcOs,1815
17
- pythonic_fp_gadgets-4.0.2.dist-info/RECORD,,