pythonic-fp-gadgets 3.1.0__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 (32) hide show
  1. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/CHANGELOG.rst +9 -0
  2. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/PKG-INFO +6 -6
  3. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/README.rst +2 -2
  4. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/pyproject.toml +4 -3
  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.1.0 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/box.py +4 -6
  8. pythonic_fp_gadgets-4.0.0/src/pythonic_fp/gadgets/box.pyi +27 -0
  9. pythonic_fp_gadgets-3.1.0/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-3.1.0 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/sentinels/flavored.py +3 -17
  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/test_first_common_ancestor.py +115 -0
  18. pythonic_fp_gadgets-4.0.0/tests/test_hwrap.py +76 -0
  19. pythonic_fp_gadgets-3.1.0/tests/test_it.py → pythonic_fp_gadgets-4.0.0/tests/test_iterate_over_arguments.py +1 -1
  20. pythonic_fp_gadgets-4.0.0/tests/test_wrap.py +71 -0
  21. pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/__init__.py +0 -39
  22. pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/latest_common_ancestor.py +0 -51
  23. pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/sentinels/__init__.py +0 -31
  24. pythonic_fp_gadgets-3.1.0/src/pythonic_fp/gadgets/sentinels/novalue.py +0 -106
  25. pythonic_fp_gadgets-3.1.0/tests/test_lca.py +0 -114
  26. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/.gitignore +0 -0
  27. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/LICENSE +0 -0
  28. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/src/pythonic_fp/gadgets/py.typed +0 -0
  29. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/tests/sentinels/test_novalue.py +0 -0
  30. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/tests/sentinels/test_sentinel_hidden_implemetation_detail.py +0 -0
  31. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/tests/sentinels/test_sentinel_with_functions.py +0 -0
  32. {pythonic_fp_gadgets-3.1.0 → pythonic_fp_gadgets-4.0.0}/tests/test_box.py +0 -0
@@ -17,6 +17,15 @@ 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
+
20
29
  PyPI v3.0.1 - 2025-09-09
21
30
  ~~~~~~~~~~~~~~~~~~~~~~~~
22
31
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonic-fp-gadgets
3
- Version: 3.1.0
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
@@ -17,8 +17,8 @@ License-File: LICENSE
17
17
  Requires-Dist: pytest>=8.4.1 ; extra == "test"
18
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
 
@@ -39,7 +39,7 @@ outside the Python Standard Library.
39
39
 
40
40
  Part of the
41
41
  `pythonic-fp
42
- <https://grscheller.github.io/pythonic-fp/homepage/build/html/index.html>`_
42
+ <https://grscheller.github.io/pythonic-fp>`_
43
43
  PyPI projects.
44
44
 
45
45
  Documentation
@@ -47,7 +47,7 @@ Documentation
47
47
 
48
48
  Documentation for this project is hosted on
49
49
  `GitHub Pages
50
- <https://grscheller.github.io/pythonic-fp/gadgets/development/build/html>`_.
50
+ <https://grscheller.github.io/pythonic-fp/gadgets>`_.
51
51
 
52
52
  Copyright and License
53
53
  ---------------------
@@ -15,7 +15,7 @@ outside the Python Standard Library.
15
15
 
16
16
  Part of the
17
17
  `pythonic-fp
18
- <https://grscheller.github.io/pythonic-fp/homepage/build/html/index.html>`_
18
+ <https://grscheller.github.io/pythonic-fp>`_
19
19
  PyPI projects.
20
20
 
21
21
  Documentation
@@ -23,7 +23,7 @@ Documentation
23
23
 
24
24
  Documentation for this project is hosted on
25
25
  `GitHub Pages
26
- <https://grscheller.github.io/pythonic-fp/gadgets/development/build/html>`_.
26
+ <https://grscheller.github.io/pythonic-fp/gadgets>`_.
27
27
 
28
28
  Copyright and License
29
29
  ---------------------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pythonic-fp-gadgets"
3
- version = "3.1.0"
3
+ version = "4.0.0"
4
4
  readme = "README.rst"
5
5
  requires-python = ">=3.13"
6
6
  license = { file = "LICENSE" }
@@ -22,8 +22,8 @@ dynamic = ["description"]
22
22
 
23
23
  [project.urls]
24
24
  Changelog = "https://github.com/grscheller/pythonic-fp-gadgets/blob/main/CHANGELOG.rst"
25
- Documentation = "https://grscheller.github.io/pythonic-fp/gadgets/development/build/html/releases.html"
26
- 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/"
27
27
  Source = "https://github.com/grscheller/pythonic-fp-gadgets"
28
28
 
29
29
  [project.optional-dependencies]
@@ -69,6 +69,7 @@ docstring-quotes = "double"
69
69
 
70
70
  [tool.ruff.lint.per-file-ignores]
71
71
  "tests/*" = ["C901"]
72
+ "tests/**/*.py" = ['E731']
72
73
  "**/*.py" = ["E741"]
73
74
 
74
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,8 +12,6 @@
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
@@ -28,8 +26,8 @@ class Box[T]:
28
26
 
29
27
  .. note::
30
28
 
31
- - ``Box(item: T)``: contains at one item of type ``T``
32
- - ``Box[T]()``: creates empty container
29
+ - ``Box(item: T)``: contains at one item of type ``T``
30
+ - ``Box[T]()``: creates empty container
33
31
 
34
32
  Where type ``T`` is some definite type, which
35
33
  could be ``None`` or even ``Never``.
@@ -42,10 +40,10 @@ class Box[T]:
42
40
  __slots__ = ('_item',)
43
41
  __match_args__ = ('_item',)
44
42
 
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
48
  def __init__(self, item: T | _Sentinel = _sentinel) -> None:
51
49
  """
@@ -0,0 +1,27 @@
1
+ from _typeshed import Incomplete
2
+ from collections.abc import Callable, Iterator
3
+ from typing import overload
4
+
5
+ __all__ = ['Box']
6
+
7
+ type _Sentinel = object
8
+ class Box[T]:
9
+ __match_args__: Incomplete
10
+ @overload
11
+ def __init__(self, item: T) -> None: ...
12
+ @overload
13
+ def __init__(self) -> None: ...
14
+ def __bool__(self) -> bool: ...
15
+ def __iter__(self) -> Iterator[T]: ...
16
+ def __len__(self) -> int: ...
17
+ def __eq__(self, other: object) -> bool: ...
18
+ @overload
19
+ def get(self) -> T: ...
20
+ @overload
21
+ def get(self, alt: T) -> T: ...
22
+ def pop(self) -> T: ...
23
+ def push(self, item: T) -> None: ...
24
+ def put(self, item: T) -> None: ...
25
+ def exchange(self, new_item: T) -> T: ...
26
+ def map[U](self, f: Callable[[T], U]) -> Box[U]: ...
27
+ def bind[U](self, f: Callable[[T], 'Box[U]']) -> Box[U]: ...
@@ -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'
@@ -13,15 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
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.
16
+ **Sentinel values labeled by different (hashable) flavors.**
25
17
 
26
18
  .. note::
27
19
 
@@ -35,15 +27,9 @@ with functions or classes.
35
27
  To ensure that reference equality is used, put the known
36
28
  sentinel value first in the comparison.
37
29
 
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
30
+ .. note::
45
31
 
46
- - which may be using either ``None`` or ``()`` as a "sentinel" values
32
+ Threadsafe.
47
33
 
48
34
  """
49
35
 
@@ -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: ...
@@ -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
+ __all__ = ['Wrap', 'HWrap']
16
+
17
+ from collections.abc import Callable, Iterator, Hashable
18
+
19
+
20
+ class Wrap[T]():
21
+ """Immutablely wrap exactly one value of a given type.
22
+
23
+ .. tip::
24
+
25
+ ``Wrap`` objects can be used in Python match statements.
26
+
27
+ """
28
+ __slots__ = ('_item',)
29
+ __match_args__ = ('_item',)
30
+
31
+ def __init__(self, item: T) -> None:
32
+ self._item = item
33
+
34
+ def __bool__(self) -> bool:
35
+ return bool(self._item)
36
+
37
+ def __iter__(self) -> Iterator[T]:
38
+ if self:
39
+ yield self._item
40
+
41
+ def __repr__(self) -> str:
42
+ return 'Wrap(' + repr(self._item) + ')'
43
+
44
+ def __eq__(self, other: object) -> bool:
45
+ if not isinstance(other, type(self)):
46
+ return False
47
+
48
+ if self._item is other._item:
49
+ return True
50
+ return self._item == other._item
51
+
52
+ def map[U](self, f: Callable[[T], U]) -> 'Wrap[U]':
53
+ """Map function ``f`` over contents.
54
+
55
+ :param f: Mapping function.
56
+ :returns: A new instance.
57
+
58
+ """
59
+ return Wrap(f(self._item))
60
+
61
+ def bind[U](self, f: Callable[[T], 'Wrap[U]']) -> 'Wrap[U]':
62
+ """Flatmap the ``Wrap`` with function ``f``.
63
+
64
+ :param f: Binding function.
65
+ :returns: A new instance.
66
+
67
+ """
68
+ return f(self._item)
69
+
70
+
71
+ class HWrap[T: Hashable](Hashable):
72
+ """Immutablely wrap exactly one hashable value of a given type.
73
+
74
+ .. tip::
75
+
76
+ ``Wrap`` objects can be used in Python match statements.
77
+
78
+ """
79
+ __slots__ = ('_item', '_hash')
80
+ __match_args__ = ('_item',)
81
+
82
+ def __init__(self, item: T) -> None:
83
+ self._item, self._hash = item, hash(item)
84
+
85
+ def __hash__(self) -> int:
86
+ return self._hash
87
+
88
+ def __bool__(self) -> bool:
89
+ return bool(self._item)
90
+
91
+ def __iter__(self) -> Iterator[T]:
92
+ if self:
93
+ yield self._item
94
+
95
+ def __repr__(self) -> str:
96
+ return 'Wrap(' + repr(self._item) + ')'
97
+
98
+ def __eq__(self, other: object) -> bool:
99
+ if not isinstance(other, type(self)):
100
+ return False
101
+
102
+ if self._hash != other._hash:
103
+ return False
104
+ if self._item is other._item:
105
+ return True
106
+ return self._item == other._item
107
+
108
+ def map[U](self, f: Callable[[T], U]) -> 'HWrap[U]':
109
+ """Map function ``f`` over contents.
110
+
111
+ :param f: Mapping function.
112
+ :returns: A new instance.
113
+
114
+ """
115
+ return HWrap(f(self._item))
116
+
117
+ def bind[U](self, f: Callable[[T], 'HWrap[U]']) -> 'HWrap[U]':
118
+ """Flatmap the ``Wrap`` with function ``f``.
119
+
120
+ :param f: Binding function.
121
+ :returns: A new instance.
122
+
123
+ """
124
+ return f(self._item)
@@ -0,0 +1,23 @@
1
+ from _typeshed import Incomplete
2
+ from collections.abc import Callable, Hashable, Iterator
3
+
4
+ __all__ = ['Wrap', 'HWrap']
5
+
6
+ class Wrap[T]:
7
+ __match_args__: Incomplete
8
+ def __init__(self, item: T) -> None: ...
9
+ def __bool__(self) -> bool: ...
10
+ def __iter__(self) -> Iterator[T]: ...
11
+ def __eq__(self, other: object) -> bool: ...
12
+ def map[U](self, f: Callable[[T], U]) -> Wrap[U]: ...
13
+ def bind[U](self, f: Callable[[T], 'Wrap[U]']) -> Wrap[U]: ...
14
+
15
+ class HWrap[T: Hashable](Hashable):
16
+ __match_args__: Incomplete
17
+ def __init__(self, item: T) -> None: ...
18
+ def __hash__(self) -> int: ...
19
+ def __bool__(self) -> bool: ...
20
+ def __iter__(self) -> Iterator[T]: ...
21
+ def __eq__(self, other: object) -> bool: ...
22
+ def map[U](self, f: Callable[[T], U]) -> HWrap[U]: ...
23
+ def bind[U](self, f: Callable[[T], 'HWrap[U]']) -> HWrap[U]: ...
@@ -0,0 +1,115 @@
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 import first_common_ancestor as fca
16
+
17
+
18
+ class A1():
19
+ pass
20
+
21
+ class A1B1(A1):
22
+ pass
23
+
24
+ class A1B1C1(A1B1):
25
+ pass
26
+
27
+ class A1B1C2(A1B1):
28
+ pass
29
+
30
+ class A1B2(A1):
31
+ pass
32
+
33
+ class A1B2C1(A1B2):
34
+ pass
35
+
36
+ class A1B2C2(A1B2):
37
+ pass
38
+
39
+ class A2():
40
+ pass
41
+
42
+ class A2B1(A2):
43
+ pass
44
+
45
+ class A2B1C1(A2B1):
46
+ pass
47
+
48
+ class A2B1C2(A2B1):
49
+ pass
50
+
51
+ class A2B2(A2):
52
+ pass
53
+
54
+ class A2B2C1(A2B2):
55
+ pass
56
+
57
+ class A2B2C1D1(A2B2C1):
58
+ pass
59
+
60
+ class A2B2C1D2(A2B2C1):
61
+ pass
62
+
63
+ class A2B2C2(A2B2):
64
+ pass
65
+
66
+ class A2B2C2D1(A2B2C2):
67
+ pass
68
+
69
+ class A2B2C2D2(A2B2C2):
70
+ pass
71
+
72
+ class TestLatestCommonAncestor:
73
+ """Functionality testing"""
74
+
75
+ def test_self(self) -> None:
76
+ assert fca(A1, A1) is A1
77
+ assert fca(A1B1, A1B1) is A1B1
78
+ assert fca(A1B1C1, A1B1C1) is A1B1C1
79
+ assert fca(int, int) is int
80
+ assert fca(bool, bool) is bool
81
+
82
+ def test_linear_descent(self) -> None:
83
+ assert fca(A1, A1B1) is A1
84
+ assert fca(A1B1, A1) is A1
85
+ assert fca(A1B1C1, A1) is A1
86
+ assert fca(A1, A1B1C1) is A1
87
+ assert fca(A1B1C1, A1B1) is A1B1
88
+ assert fca(A1B1, A1B1C1) is A1B1
89
+ assert fca(A1B1C1, A1B1) is A1B1
90
+ assert fca(A1B1, A1B1C1) is A1B1
91
+ assert fca(A1B1C1, A1) is A1
92
+ assert fca(A1, A1B1C1) is A1
93
+ assert fca(A2B2, A2B2C2D2) is A2B2
94
+ assert fca(A2B2C2D2, A2B2) is A2B2
95
+ assert fca(int, object) is object
96
+ assert fca(object, int) is object
97
+ assert fca(int, bool) is int
98
+ assert fca(bool, int) is int
99
+
100
+ def test_nonliear_descent(self) -> None:
101
+ assert fca(A1B1, A1B2) is A1
102
+ assert fca(A1B2, A1B1) is A1
103
+ assert fca(A1B2C1, A1B2C2) is A1B2
104
+ assert fca(A1B2C2, A1B2C1) is A1B2
105
+ assert fca(A1B1C1, A1B2C2) is A1
106
+ assert fca(A1B2C2, A1B1C1) is A1
107
+ assert fca(A1B1C1, A1B2C1) is A1
108
+ assert fca(A1B2C1, A1B1C1) is A1
109
+ assert fca(A2B2C1, A2B2C2D2) is A2B2
110
+ assert fca(A2B2C2D2, A2B2C1) is A2B2
111
+ assert fca(A2B1C1, A2B2C2D2) is A2
112
+ assert fca(A2B2C2D2, A2B1C1) is A2
113
+ assert fca(A1, A2) is object
114
+ assert fca(A2B2C2D2, A1B2C2) is object
115
+ assert fca(A1B2C1, A2B1C1) is object
@@ -0,0 +1,76 @@
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.wrap import HWrap
16
+
17
+
18
+ class TestGadgetHWrapBuiltins:
19
+ def test_hwrap_simple(self) -> None:
20
+ w0 = HWrap(0)
21
+ w1 = HWrap(1)
22
+ x1 = HWrap(1)
23
+
24
+ assert w0 != w1
25
+ assert w1 == x1
26
+ assert w1 is w1
27
+ assert w1 is not x1
28
+
29
+ assert hash(w0) == hash(w0)
30
+ assert hash(w1) == hash(w1)
31
+ assert hash(w1) == hash(x1)
32
+ assert hash(w1) != hash(w0)
33
+
34
+ def test_map(self) -> None:
35
+ def cat(s: str) -> str:
36
+ return s + s
37
+
38
+ wa = HWrap('a')
39
+ waa = HWrap('aa')
40
+ waaaa = HWrap('aaaa')
41
+
42
+ xaa = wa.map(cat)
43
+ xaaaa = xaa.map(cat)
44
+
45
+ assert xaa == waa
46
+ assert xaa != waaaa
47
+ assert xaaaa == waaaa
48
+ assert waaaa == xaaaa
49
+
50
+ def test_bind(self) -> None:
51
+ type TS = tuple[str, ...]
52
+ type WLS = HWrap[TS]
53
+ type TI = tuple[int, ...]
54
+ type WTI = HWrap[TI]
55
+
56
+ def cnt(ts: TS) -> WTI:
57
+ ti: TI = ()
58
+ for s in ts:
59
+ ti += (len(s),)
60
+ return HWrap(ti)
61
+
62
+ hw: TS = ('hello', 'world', '!')
63
+ fb: TS = ('foobar',)
64
+ nil: TS = ()
65
+
66
+ wt_hw: WLS = HWrap(hw)
67
+ wt_fb: WLS = HWrap(fb)
68
+ wt_nil: WLS = HWrap(nil)
69
+
70
+ wi_hw: WTI = HWrap((5, 5, 1))
71
+ wi_fb: WTI = HWrap((6,))
72
+ wi_nil: WTI = HWrap(())
73
+
74
+ assert wi_hw == wt_hw.bind(cnt)
75
+ assert wi_fb == wt_fb.bind(cnt)
76
+ assert wi_nil == wt_nil.bind(cnt)
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from pythonic_fp.circulararray.auto import ca, CA
16
- from pythonic_fp.gadgets.iterate_arguments import ita
16
+ from pythonic_fp.gadgets import iterate_over_arguments as ita
17
17
 
18
18
  class TestGadgetIt:
19
19
  """Functionality testing"""
@@ -0,0 +1,71 @@
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.wrap import Wrap
16
+
17
+
18
+ class TestGadgetWrapBuiltins:
19
+ def test_wrap_simple(self) -> None:
20
+ w0 = Wrap(0)
21
+ w1 = Wrap(1)
22
+ x1 = Wrap(1)
23
+
24
+ assert w0 != w1
25
+ assert w1 == x1
26
+ assert w1 is w1
27
+ assert w1 is not x1
28
+
29
+ def test_map(self) -> None:
30
+ def cat(s: str) -> str:
31
+ return s + s
32
+
33
+ wa = Wrap('a')
34
+ waa = Wrap('aa')
35
+ waaaa = Wrap('aaaa')
36
+
37
+ xaa = wa.map(cat)
38
+ xaaaa = xaa.map(cat)
39
+
40
+ assert xaa == waa
41
+ assert xaa != waaaa
42
+ assert xaaaa == waaaa
43
+ assert waaaa == xaaaa
44
+
45
+ def test_bind(self) -> None:
46
+ type LS = list[str]
47
+ type WLS = Wrap[LS]
48
+ type LI = list[int]
49
+ type WLI = Wrap[LI]
50
+
51
+ def cnt(ls: LS) -> WLI:
52
+ li: LI = []
53
+ for s in ls:
54
+ li.append(len(s))
55
+ return Wrap(li)
56
+
57
+ hw: LS = ['hello', 'world', '!']
58
+ fb: LS = ['foobar',]
59
+ nil: LS = []
60
+
61
+ wt_hw: WLS = Wrap(hw)
62
+ wt_fb: WLS = Wrap(fb)
63
+ wt_nil: WLS = Wrap(nil)
64
+
65
+ wi_hw: WLI = Wrap([5, 5, 1])
66
+ wi_fb: WLI = Wrap([6,])
67
+ wi_nil: WLI = Wrap([])
68
+
69
+ assert wi_hw == wt_hw.bind(cnt)
70
+ assert wi_fb == wt_fb.bind(cnt)
71
+ assert wi_nil == wt_nil.bind(cnt)
@@ -1,39 +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 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'
@@ -1,51 +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
- """Last Common Ancestor of Two Classes."""
16
-
17
- from inspect import getmro
18
-
19
- __all__ = ['lca']
20
-
21
-
22
- def lca(cls1: type, cls2: type) -> type:
23
- """Find the least upper bound in the inheritance graph
24
- of two classes.
25
-
26
- .. warning::
27
-
28
- This function can fail with a TypeError. Some error messages
29
- seen are
30
-
31
- - multiple bases have instance lay-out conflict
32
- - type 'bool' is not an acceptable base type
33
-
34
- This happens frequently when the function is given
35
- Python builtin types.
36
-
37
- :param cls1: first class
38
- :param cls2: second class
39
- :returns: Least common ancestor base class of ``cls1`` and ``cls2``.
40
- :raises TypeError: theoretically only from ``inspect.getmto``
41
-
42
- """
43
- if issubclass(cls1, cls2):
44
- return cls2
45
- if issubclass(cls2, cls1):
46
- return cls1
47
-
48
- for common_ancestor in getmro(type('LcaDiamondClass', (cls1, cls2), {})):
49
- if issubclass(cls1, common_ancestor) and issubclass(cls2, common_ancestor):
50
- return common_ancestor
51
- raise TypeError("latest_common_ancestor: no common ancestor found!!!")
@@ -1,31 +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
- 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'
@@ -1,106 +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
- 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
@@ -1,114 +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
- from pythonic_fp.gadgets.latest_common_ancestor import lca
16
-
17
- class A1():
18
- pass
19
-
20
- class A1B1(A1):
21
- pass
22
-
23
- class A1B1C1(A1B1):
24
- pass
25
-
26
- class A1B1C2(A1B1):
27
- pass
28
-
29
- class A1B2(A1):
30
- pass
31
-
32
- class A1B2C1(A1B2):
33
- pass
34
-
35
- class A1B2C2(A1B2):
36
- pass
37
-
38
- class A2():
39
- pass
40
-
41
- class A2B1(A2):
42
- pass
43
-
44
- class A2B1C1(A2B1):
45
- pass
46
-
47
- class A2B1C2(A2B1):
48
- pass
49
-
50
- class A2B2(A2):
51
- pass
52
-
53
- class A2B2C1(A2B2):
54
- pass
55
-
56
- class A2B2C1D1(A2B2C1):
57
- pass
58
-
59
- class A2B2C1D2(A2B2C1):
60
- pass
61
-
62
- class A2B2C2(A2B2):
63
- pass
64
-
65
- class A2B2C2D1(A2B2C2):
66
- pass
67
-
68
- class A2B2C2D2(A2B2C2):
69
- pass
70
-
71
- class TestLatestCommonAncestor:
72
- """Functionality testing"""
73
-
74
- def test_self(self) -> None:
75
- assert lca(A1, A1) is A1
76
- assert lca(A1B1, A1B1) is A1B1
77
- assert lca(A1B1C1, A1B1C1) is A1B1C1
78
- assert lca(int, int) is int
79
- assert lca(bool, bool) is bool
80
-
81
- def test_linear_descent(self) -> None:
82
- assert lca(A1, A1B1) is A1
83
- assert lca(A1B1, A1) is A1
84
- assert lca(A1B1C1, A1) is A1
85
- assert lca(A1, A1B1C1) is A1
86
- assert lca(A1B1C1, A1B1) is A1B1
87
- assert lca(A1B1, A1B1C1) is A1B1
88
- assert lca(A1B1C1, A1B1) is A1B1
89
- assert lca(A1B1, A1B1C1) is A1B1
90
- assert lca(A1B1C1, A1) is A1
91
- assert lca(A1, A1B1C1) is A1
92
- assert lca(A2B2, A2B2C2D2) is A2B2
93
- assert lca(A2B2C2D2, A2B2) is A2B2
94
- assert lca(int, object) is object
95
- assert lca(object, int) is object
96
- assert lca(int, bool) is int
97
- assert lca(bool, int) is int
98
-
99
- def test_nonliear_descent(self) -> None:
100
- assert lca(A1B1, A1B2) is A1
101
- assert lca(A1B2, A1B1) is A1
102
- assert lca(A1B2C1, A1B2C2) is A1B2
103
- assert lca(A1B2C2, A1B2C1) is A1B2
104
- assert lca(A1B1C1, A1B2C2) is A1
105
- assert lca(A1B2C2, A1B1C1) is A1
106
- assert lca(A1B1C1, A1B2C1) is A1
107
- assert lca(A1B2C1, A1B1C1) is A1
108
- assert lca(A2B2C1, A2B2C2D2) is A2B2
109
- assert lca(A2B2C2D2, A2B2C1) is A2B2
110
- assert lca(A2B1C1, A2B2C2D2) is A2
111
- assert lca(A2B2C2D2, A2B1C1) is A2
112
- assert lca(A1, A2) is object
113
- assert lca(A2B2C2D2, A1B2C2) is object
114
- assert lca(A1B2C1, A2B1C1) is object