py-maybetype 0.6.0__tar.gz → 0.7.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.
- py_maybetype-0.6.0/.github/workflows/python-lint-test.yml → py_maybetype-0.7.0/.github/workflows/lint-test.yml +9 -2
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.github/workflows/publish-to-pypi.yml +2 -2
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/CHANGELOG.md +26 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/PKG-INFO +39 -16
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/README.md +34 -14
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/maybetype/__init__.py +51 -20
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/pyproject.toml +5 -2
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/tests/test_maybe.py +53 -10
- py_maybetype-0.7.0/uv.lock +994 -0
- py_maybetype-0.6.0/uv.lock +0 -108
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.gitignore +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.markdownlint.json +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.python-version +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.readthedocs.yaml +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/LICENSE +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/changelog.rst +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/conf.py +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/index.rst +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/readme.rst +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/reference/index.rst +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/requirements.txt +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/maybetype/const.py +0 -0
- {py_maybetype-0.6.0 → py_maybetype-0.7.0}/ruff.toml +0 -0
|
@@ -13,17 +13,24 @@ jobs:
|
|
|
13
13
|
build:
|
|
14
14
|
name: Lint and test
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: [ '3.12', '3.13', '3.14' ]
|
|
16
19
|
|
|
17
20
|
steps:
|
|
18
21
|
- uses: actions/checkout@v4
|
|
19
|
-
- name: Set up Python
|
|
22
|
+
- name: Set up Python
|
|
20
23
|
uses: actions/setup-python@v3
|
|
21
24
|
with:
|
|
22
|
-
python-version:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
architecture: x64
|
|
23
27
|
- name: Install project + dependencies
|
|
24
28
|
run: |
|
|
25
29
|
python -m pip install --upgrade pip
|
|
26
30
|
pip install .[dev]
|
|
31
|
+
- name: Check with ty
|
|
32
|
+
run: |
|
|
33
|
+
ty check .
|
|
27
34
|
- name: Lint with ruff
|
|
28
35
|
run: |
|
|
29
36
|
ruff check .
|
|
@@ -40,7 +40,7 @@ jobs:
|
|
|
40
40
|
runs-on: ubuntu-latest
|
|
41
41
|
environment:
|
|
42
42
|
name: pypi
|
|
43
|
-
url: https://pypi.org/p/maybetype
|
|
43
|
+
url: https://pypi.org/p/py-maybetype
|
|
44
44
|
permissions:
|
|
45
45
|
id-token: write
|
|
46
46
|
steps:
|
|
@@ -59,7 +59,7 @@ jobs:
|
|
|
59
59
|
runs-on: ubuntu-latest
|
|
60
60
|
environment:
|
|
61
61
|
name: testpypi
|
|
62
|
-
url: https://test.pypi.org/p/maybetype
|
|
62
|
+
url: https://test.pypi.org/p/py-maybetype
|
|
63
63
|
permissions:
|
|
64
64
|
id-token: write
|
|
65
65
|
steps:
|
|
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
Generally, more significant breaking changes will be put near the top of each category.
|
|
9
|
+
|
|
10
|
+
## [0.7.0]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added static method `Maybe.sequence()`
|
|
15
|
+
- Added methods to `Maybe`:
|
|
16
|
+
- `and_then()`: Similar to `Maybe.then()`, but returns a `Maybe` instance
|
|
17
|
+
- `test()`: Returns `Some` if the instance is `Some` and the test function returns `True` when
|
|
18
|
+
called with the wrapped value, otherwise returns `Nothing`
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Renamed static method `Maybe.int()` to `Maybe.try_int()`
|
|
23
|
+
- `Maybe.__hash__()` now returns `hash()` called with the wrapped value instead of accessing its
|
|
24
|
+
`__hash__()` method
|
|
25
|
+
- Most type signatures now use the more standard type variable naming of `T`, `U`, `V`...
|
|
26
|
+
- The only exception currently is the static method `Maybe.map()`, which uses `A` and `B` since
|
|
27
|
+
`T` already belongs the `Maybe` class' scope and the method operates on unrelated types
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- Removed method `Maybe.this_or()`
|
|
32
|
+
- Redundant, `maybe(x).this_or(y)` is exactly the same as `maybe(x) or Some(y)`
|
|
33
|
+
|
|
8
34
|
## [0.6.0] - 2026-01-21
|
|
9
35
|
|
|
10
36
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-maybetype
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: A basic implementation of a maybe/option type in Python, largely inspired by Rust's Option.
|
|
5
5
|
Project-URL: Homepage, https://github.com/svioletg/py-maybetype
|
|
6
6
|
Project-URL: Repository, https://github.com/svioletg/py-maybetype
|
|
@@ -16,10 +16,13 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
20
|
Requires-Python: >=3.12
|
|
20
21
|
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: ipython>=9.10.0; extra == 'dev'
|
|
21
23
|
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
22
|
-
Requires-Dist: ruff>=0.
|
|
24
|
+
Requires-Dist: ruff>=0.15.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ty>=0.0.15; extra == 'dev'
|
|
23
26
|
Provides-Extra: docs
|
|
24
27
|
Requires-Dist: furo>=2025.12.19; extra == 'docs'
|
|
25
28
|
Requires-Dist: myst-parser>=4.0.1; extra == 'docs'
|
|
@@ -41,6 +44,10 @@ its own package as I wanted to use it elsewhere and its scope grew. This is not
|
|
|
41
44
|
replication or replacement for Rust's `Option` or Haskell's `Maybe`, but rather just an
|
|
42
45
|
interperetation of the idea that I feel works for Python.
|
|
43
46
|
|
|
47
|
+
> [!WARNING]
|
|
48
|
+
> Breaking changes are likely each update in the 0.x phase. Please check the changelog for these
|
|
49
|
+
> changes before updating to a new version.
|
|
50
|
+
|
|
44
51
|
## Usage
|
|
45
52
|
|
|
46
53
|
Install with `pip`:
|
|
@@ -77,17 +84,20 @@ assert bool(num1) is True
|
|
|
77
84
|
assert bool(num2) is False
|
|
78
85
|
```
|
|
79
86
|
|
|
80
|
-
This example in particular can also be done with `Maybe
|
|
87
|
+
This example in particular can also be done with the `Maybe.try_int()` class method:
|
|
81
88
|
|
|
82
89
|
```python
|
|
83
|
-
num1: Maybe[int] = Maybe.
|
|
84
|
-
num2: Maybe[int] = Maybe.
|
|
90
|
+
num1: Maybe[int] = Maybe.try_int('5')
|
|
91
|
+
num2: Maybe[int] = Maybe.try_int('five')
|
|
85
92
|
```
|
|
86
93
|
|
|
87
94
|
The `maybe` constructor can be given an optional predicate argument to specify a custom condition
|
|
88
95
|
for which `Some(value)` is returned. This argument must be a `Callable` that returns `bool`,
|
|
89
96
|
where returning `False` causes the constructor to return `Nothing`.
|
|
90
97
|
|
|
98
|
+
> [!NOTE]
|
|
99
|
+
> `maybe(None)` will always returning `Nothing`, even if `predicate(None)` would return `True`
|
|
100
|
+
|
|
91
101
|
```python
|
|
92
102
|
import re
|
|
93
103
|
import uuid
|
|
@@ -112,29 +122,42 @@ from maybetype import maybe, Some
|
|
|
112
122
|
match maybe(1):
|
|
113
123
|
case Some(val):
|
|
114
124
|
print('Value: ', val)
|
|
115
|
-
case _:
|
|
125
|
+
case _: # "case Nothing:" also works, but just matching else in this case will be identical
|
|
116
126
|
print('No value')
|
|
117
127
|
```
|
|
118
128
|
|
|
119
129
|
## Other examples
|
|
120
130
|
|
|
121
|
-
Converting a `str | None` timestamp into a `datetime` object if not `None`, otherwise returning
|
|
131
|
+
Converting a `str | None` timestamp into a `datetime` object if not `None`, otherwise returning
|
|
132
|
+
`None`:
|
|
122
133
|
|
|
123
134
|
```python
|
|
124
135
|
from datetime import datetime
|
|
125
136
|
from maybetype import maybe
|
|
126
137
|
|
|
127
|
-
|
|
128
|
-
date = maybe('2025-09-06T030000').then(datetime.fromisoformat)
|
|
129
|
-
# date == datetime.datetime(2025, 9, 6, 3, 0)
|
|
138
|
+
assert maybe('2025-09-06T030000').then(datetime.fromisoformat) == datetime(2025, 9, 6, 3, 0)
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
date = maybe(date_str).then(datetime.fromisoformat)
|
|
133
|
-
# date == None
|
|
140
|
+
assert maybe(None).then(datetime.fromisoformat) is None
|
|
134
141
|
|
|
135
|
-
|
|
136
|
-
date = maybe(date_str or None).then(datetime.fromisoformat)
|
|
137
|
-
# date == None
|
|
142
|
+
assert maybe('' or None).then(datetime.fromisoformat) is None
|
|
138
143
|
# Maybe does not treat falsy values as None, only strictly x-is-None values
|
|
139
144
|
# Without `or None` here, datetime.fromisoformat would have raised a ValueError
|
|
140
145
|
```
|
|
146
|
+
|
|
147
|
+
Converting a `str | None` timestamp into a `datetime` object if not `None`, then ensuring that date
|
|
148
|
+
meets certain criteria:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from datetime import datetime
|
|
152
|
+
from maybetype import maybe
|
|
153
|
+
|
|
154
|
+
assert maybe('2025-09-06T030000').and_then(datetime.fromisoformat).test(lambda dt: dt.year > 2024)
|
|
155
|
+
|
|
156
|
+
assert not maybe('2024-09-06T030000').and_then(datetime.fromisoformat).test(lambda dt: dt.year > 2024)
|
|
157
|
+
|
|
158
|
+
match maybe('2025-09-06T030000').and_then(datetime.fromisoformat).test(lambda dt: dt.year > 2024):
|
|
159
|
+
case Some(date):
|
|
160
|
+
... # Do something with the date
|
|
161
|
+
case _:
|
|
162
|
+
... # Do something else
|
|
163
|
+
```
|
|
@@ -12,6 +12,10 @@ its own package as I wanted to use it elsewhere and its scope grew. This is not
|
|
|
12
12
|
replication or replacement for Rust's `Option` or Haskell's `Maybe`, but rather just an
|
|
13
13
|
interperetation of the idea that I feel works for Python.
|
|
14
14
|
|
|
15
|
+
> [!WARNING]
|
|
16
|
+
> Breaking changes are likely each update in the 0.x phase. Please check the changelog for these
|
|
17
|
+
> changes before updating to a new version.
|
|
18
|
+
|
|
15
19
|
## Usage
|
|
16
20
|
|
|
17
21
|
Install with `pip`:
|
|
@@ -48,17 +52,20 @@ assert bool(num1) is True
|
|
|
48
52
|
assert bool(num2) is False
|
|
49
53
|
```
|
|
50
54
|
|
|
51
|
-
This example in particular can also be done with `Maybe
|
|
55
|
+
This example in particular can also be done with the `Maybe.try_int()` class method:
|
|
52
56
|
|
|
53
57
|
```python
|
|
54
|
-
num1: Maybe[int] = Maybe.
|
|
55
|
-
num2: Maybe[int] = Maybe.
|
|
58
|
+
num1: Maybe[int] = Maybe.try_int('5')
|
|
59
|
+
num2: Maybe[int] = Maybe.try_int('five')
|
|
56
60
|
```
|
|
57
61
|
|
|
58
62
|
The `maybe` constructor can be given an optional predicate argument to specify a custom condition
|
|
59
63
|
for which `Some(value)` is returned. This argument must be a `Callable` that returns `bool`,
|
|
60
64
|
where returning `False` causes the constructor to return `Nothing`.
|
|
61
65
|
|
|
66
|
+
> [!NOTE]
|
|
67
|
+
> `maybe(None)` will always returning `Nothing`, even if `predicate(None)` would return `True`
|
|
68
|
+
|
|
62
69
|
```python
|
|
63
70
|
import re
|
|
64
71
|
import uuid
|
|
@@ -83,29 +90,42 @@ from maybetype import maybe, Some
|
|
|
83
90
|
match maybe(1):
|
|
84
91
|
case Some(val):
|
|
85
92
|
print('Value: ', val)
|
|
86
|
-
case _:
|
|
93
|
+
case _: # "case Nothing:" also works, but just matching else in this case will be identical
|
|
87
94
|
print('No value')
|
|
88
95
|
```
|
|
89
96
|
|
|
90
97
|
## Other examples
|
|
91
98
|
|
|
92
|
-
Converting a `str | None` timestamp into a `datetime` object if not `None`, otherwise returning
|
|
99
|
+
Converting a `str | None` timestamp into a `datetime` object if not `None`, otherwise returning
|
|
100
|
+
`None`:
|
|
93
101
|
|
|
94
102
|
```python
|
|
95
103
|
from datetime import datetime
|
|
96
104
|
from maybetype import maybe
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
date = maybe('2025-09-06T030000').then(datetime.fromisoformat)
|
|
100
|
-
# date == datetime.datetime(2025, 9, 6, 3, 0)
|
|
106
|
+
assert maybe('2025-09-06T030000').then(datetime.fromisoformat) == datetime(2025, 9, 6, 3, 0)
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
date = maybe(date_str).then(datetime.fromisoformat)
|
|
104
|
-
# date == None
|
|
108
|
+
assert maybe(None).then(datetime.fromisoformat) is None
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
date = maybe(date_str or None).then(datetime.fromisoformat)
|
|
108
|
-
# date == None
|
|
110
|
+
assert maybe('' or None).then(datetime.fromisoformat) is None
|
|
109
111
|
# Maybe does not treat falsy values as None, only strictly x-is-None values
|
|
110
112
|
# Without `or None` here, datetime.fromisoformat would have raised a ValueError
|
|
111
113
|
```
|
|
114
|
+
|
|
115
|
+
Converting a `str | None` timestamp into a `datetime` object if not `None`, then ensuring that date
|
|
116
|
+
meets certain criteria:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from datetime import datetime
|
|
120
|
+
from maybetype import maybe
|
|
121
|
+
|
|
122
|
+
assert maybe('2025-09-06T030000').and_then(datetime.fromisoformat).test(lambda dt: dt.year > 2024)
|
|
123
|
+
|
|
124
|
+
assert not maybe('2024-09-06T030000').and_then(datetime.fromisoformat).test(lambda dt: dt.year > 2024)
|
|
125
|
+
|
|
126
|
+
match maybe('2025-09-06T030000').and_then(datetime.fromisoformat).test(lambda dt: dt.year > 2024):
|
|
127
|
+
case Some(date):
|
|
128
|
+
... # Do something with the date
|
|
129
|
+
case _:
|
|
130
|
+
... # Do something else
|
|
131
|
+
```
|
|
@@ -12,7 +12,8 @@ class Maybe[T]:
|
|
|
12
12
|
|
|
13
13
|
def __init__(self, val: T | None) -> None:
|
|
14
14
|
warnings.warn(
|
|
15
|
-
'Direct instancing of Maybe()
|
|
15
|
+
'Direct instancing of Maybe() not intended and may cause unexpected behavior,'
|
|
16
|
+
+ ' use the maybe() function instead',
|
|
16
17
|
stacklevel=2,
|
|
17
18
|
)
|
|
18
19
|
self.val = val
|
|
@@ -32,7 +33,7 @@ class Maybe[T]:
|
|
|
32
33
|
return self.val == other.val
|
|
33
34
|
|
|
34
35
|
def __hash__(self) -> int:
|
|
35
|
-
return self.val
|
|
36
|
+
return hash(self.val)
|
|
36
37
|
|
|
37
38
|
@staticmethod
|
|
38
39
|
def cat(vals: 'Iterable[Maybe[T]]') -> list[T]:
|
|
@@ -46,7 +47,28 @@ class Maybe[T]:
|
|
|
46
47
|
return [i.unwrap() for i in vals if i]
|
|
47
48
|
|
|
48
49
|
@staticmethod
|
|
49
|
-
def
|
|
50
|
+
def map[A, B](fn: 'Callable[[A], Maybe[B]]', vals: Iterable[A]) -> list[B]:
|
|
51
|
+
"""Maps ``fn`` onto ``vals``, taking the unwrapped values of ``Some``s and discarding ``Nothing``s."""
|
|
52
|
+
return [i.unwrap() for i in map(fn, vals) if i]
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def sequence(vals: 'Iterable[Maybe[T]]') -> 'Maybe[list[T]]':
|
|
56
|
+
"""
|
|
57
|
+
Returns ``Nothing`` if any of ``vals`` is ``Nothing``, otherwise returns a ``Some`` of a list of unwrapped
|
|
58
|
+
items of ``vals``.
|
|
59
|
+
|
|
60
|
+
>>> assert Maybe.sequence([Some(1), Some(2), Some(3)]) == Some([1, 2, 3])
|
|
61
|
+
>>> assert Maybe.sequence([Some(1), Nothing, Some(3)]) is Nothing
|
|
62
|
+
"""
|
|
63
|
+
unwrapped: list[T] = []
|
|
64
|
+
for i in vals:
|
|
65
|
+
if i is Nothing:
|
|
66
|
+
return Nothing
|
|
67
|
+
unwrapped.append(i.unwrap())
|
|
68
|
+
return Some(unwrapped)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def try_int(val: Any) -> 'Maybe[int]': # noqa: ANN401
|
|
50
72
|
"""
|
|
51
73
|
Attempts to convert ``val`` to an ``int``, returning a ``Some``-wrapped ``int`` if successful, or
|
|
52
74
|
``Nothing`` on failure.
|
|
@@ -57,12 +79,14 @@ class Maybe[T]:
|
|
|
57
79
|
except ValueError:
|
|
58
80
|
return Nothing
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
def and_then[R](self, func: Callable[[T], R]) -> 'Maybe[R]':
|
|
83
|
+
"""
|
|
84
|
+
Like :py:meth:`~maybetype.Maybe.then`, but returns a ``Maybe`` instance instead—``Nothing`` if this instance
|
|
85
|
+
is a ``Nothing``, ``Some(R)`` if the instance is ``Some``, where ``R`` is the returned value of ``func``.
|
|
86
|
+
"""
|
|
87
|
+
return Some(func(self.val)) if self.val is not None else Nothing
|
|
64
88
|
|
|
65
|
-
def attr[
|
|
89
|
+
def attr[U](self, name: str, typ: type[U] | None = None, *, err: bool = False) -> 'Maybe[U]':
|
|
66
90
|
"""
|
|
67
91
|
Attempts to access an attribute ``name`` on the wrapped object, returning a ``Some`` instance wrapping the
|
|
68
92
|
the value if it exists, or ``Nothing`` otherwise.
|
|
@@ -74,7 +98,7 @@ class Maybe[T]:
|
|
|
74
98
|
"""
|
|
75
99
|
return Some(getattr(self.val, name)) if err else maybe(getattr(self.val, name, None))
|
|
76
100
|
|
|
77
|
-
def attr_or[
|
|
101
|
+
def attr_or[U](self, name: str, default: U) -> U:
|
|
78
102
|
"""
|
|
79
103
|
Similar to the ``attr`` method, but unwraps the result if the attribute exists or returns the required default
|
|
80
104
|
value otherwise.
|
|
@@ -84,13 +108,13 @@ class Maybe[T]:
|
|
|
84
108
|
except AttributeError:
|
|
85
109
|
return default
|
|
86
110
|
|
|
87
|
-
def get[
|
|
111
|
+
def get[U](self,
|
|
88
112
|
accessor: Any, # noqa: ANN401
|
|
89
|
-
_typ: type[
|
|
113
|
+
_typ: type[U] | None = None,
|
|
90
114
|
*,
|
|
91
115
|
err: bool = False,
|
|
92
|
-
default:
|
|
93
|
-
) -> 'Maybe[
|
|
116
|
+
default: U | None = None,
|
|
117
|
+
) -> 'Maybe[U]':
|
|
94
118
|
"""
|
|
95
119
|
Attempts to access an item by ``accessor`` on the wrapped object, assuming the wrapped value implements
|
|
96
120
|
``__getitem__``. If it does not, or if the value does not exist (list index out of range, key does not exist on
|
|
@@ -112,20 +136,27 @@ class Maybe[T]:
|
|
|
112
136
|
raise
|
|
113
137
|
return maybe(default)
|
|
114
138
|
|
|
115
|
-
def
|
|
139
|
+
def test(self, predicate: Callable[[T], bool]) -> 'Maybe[T]':
|
|
140
|
+
"""
|
|
141
|
+
Returns ``Nothing`` if the wrapped value does not return ``True`` when passed to ``predicate``, otherwise
|
|
142
|
+
returns the instance the method was called from. When called from a ``Nothing`` instance, ``Nothing`` is always
|
|
143
|
+
returned.
|
|
116
144
|
"""
|
|
117
|
-
|
|
118
|
-
|
|
145
|
+
match self:
|
|
146
|
+
case Some(val):
|
|
147
|
+
return self if predicate(val) else Nothing
|
|
148
|
+
case _:
|
|
149
|
+
return Nothing
|
|
150
|
+
|
|
151
|
+
def then[U](self, func: Callable[[T], U]) -> U | None:
|
|
152
|
+
"""
|
|
153
|
+
Returns ``func`` called with this instance's wrapped value if ``Some``, otherwise returns ``None``.
|
|
119
154
|
|
|
120
155
|
:param func: A ``Callable`` which takes a type of the possible wrapped value (``T``) and can return any type
|
|
121
156
|
(``R``).
|
|
122
157
|
"""
|
|
123
158
|
return func(self.val) if self.val is not None else None
|
|
124
159
|
|
|
125
|
-
def this_or(self, other: T) -> 'Maybe[T]':
|
|
126
|
-
"""Returns the original wrapped value if not ``None``, otherwise returns a ``Some``-wrapped ``other``."""
|
|
127
|
-
return self if self else Some(other)
|
|
128
|
-
|
|
129
160
|
def unwrap(self,
|
|
130
161
|
exc: Exception | Callable[..., Never] | None = None,
|
|
131
162
|
*exc_args: object,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "py-maybetype"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "A basic implementation of a maybe/option type in Python, largely inspired by Rust's Option."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Seth 'Violet' Gibbs" }
|
|
@@ -17,6 +17,7 @@ classifiers = [
|
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
19
19
|
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
[project.urls]
|
|
@@ -28,8 +29,10 @@ Issues = "https://github.com/svioletg/py-maybetype/issues"
|
|
|
28
29
|
|
|
29
30
|
[project.optional-dependencies]
|
|
30
31
|
dev = [
|
|
32
|
+
"ipython>=9.10.0",
|
|
31
33
|
"pytest>=9.0.2",
|
|
32
|
-
"ruff>=0.
|
|
34
|
+
"ruff>=0.15.0",
|
|
35
|
+
"ty>=0.0.15",
|
|
33
36
|
]
|
|
34
37
|
docs = [
|
|
35
38
|
"furo>=2025.12.19",
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Callable, Iterable
|
|
3
3
|
from copy import deepcopy
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from string import ascii_lowercase
|
|
6
6
|
from types import EllipsisType
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
import pytest
|
|
9
|
+
import pytest # ty:ignore[unresolved-import, unused-ignore-comment]; seems to only show up in workflow runs?
|
|
10
10
|
|
|
11
|
-
from maybetype import Maybe, Nothing, Some, maybe
|
|
11
|
+
from maybetype import Maybe, Nothing, Some, _Nothing, maybe
|
|
12
12
|
|
|
13
13
|
ALPHANUMERIC: str = ascii_lowercase + '0123456789'
|
|
14
14
|
MAYBE_UNWRAP_NONE_REGEX: re.Pattern[str] = re.compile(r"Maybe\[.*\] unwrapped into None")
|
|
15
15
|
|
|
16
|
+
def square(n: int) -> int:
|
|
17
|
+
return n * n
|
|
18
|
+
|
|
16
19
|
def test_maybe_none_unwrap_error() -> None:
|
|
17
20
|
m_none: Maybe[Any] = Nothing
|
|
18
21
|
assert bool(m_none) is False
|
|
@@ -24,11 +27,27 @@ def test_maybe_none_unwrap_error() -> None:
|
|
|
24
27
|
def test_maybe_none_is_nothing() -> None:
|
|
25
28
|
assert maybe(None) is Nothing
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
@pytest.mark.parametrize(('val'),
|
|
31
|
+
[
|
|
32
|
+
None,
|
|
33
|
+
0,
|
|
34
|
+
1,
|
|
35
|
+
'',
|
|
36
|
+
'string',
|
|
37
|
+
[],
|
|
38
|
+
[1, 2, 3],
|
|
39
|
+
True,
|
|
40
|
+
False,
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
def test_nothing_instance_always_wraps_none(val: object) -> None:
|
|
44
|
+
assert _Nothing(val).val is None # type: ignore
|
|
45
|
+
|
|
46
|
+
def test_maybe_or() -> None:
|
|
47
|
+
assert Maybe.try_int('10') or Some(0).unwrap() == 10 # noqa: PLR2004
|
|
48
|
+
assert Maybe.try_int('ten') or Some(0).unwrap() == 0
|
|
30
49
|
with pytest.raises(ValueError, match=MAYBE_UNWRAP_NONE_REGEX):
|
|
31
|
-
Maybe.
|
|
50
|
+
Maybe.try_int('ten').unwrap()
|
|
32
51
|
|
|
33
52
|
@pytest.mark.parametrize(('val', 'default'),
|
|
34
53
|
[
|
|
@@ -122,14 +141,14 @@ def test_maybe_get(val: object, accessor: object, result: object) -> None:
|
|
|
122
141
|
assert m.get(accessor) == result
|
|
123
142
|
|
|
124
143
|
def test_maybe_cat() -> None:
|
|
125
|
-
assert Maybe.cat(map(Maybe.
|
|
144
|
+
assert Maybe.cat(map(Maybe.try_int, ALPHANUMERIC)) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
126
145
|
|
|
127
146
|
def test_maybe_cat_failure() -> None:
|
|
128
147
|
with pytest.raises(AttributeError, match='has no attribute \'unwrap\''):
|
|
129
148
|
Maybe.cat([1, 2, 3]) # type: ignore
|
|
130
149
|
|
|
131
150
|
def test_maybe_map() -> None:
|
|
132
|
-
assert Maybe.map(Maybe.
|
|
151
|
+
assert Maybe.map(Maybe.try_int, ALPHANUMERIC) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
133
152
|
|
|
134
153
|
def is_valid_uuid(s: str) -> bool:
|
|
135
154
|
return re.match(r"[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}|[0-9a-f]{32}", s) is not None
|
|
@@ -147,10 +166,13 @@ def is_valid_uuid(s: str) -> bool:
|
|
|
147
166
|
('nf0cmmdq-l0gt-rq5a-upry-706trht3ocv9', is_valid_uuid, False),
|
|
148
167
|
],
|
|
149
168
|
)
|
|
150
|
-
def
|
|
169
|
+
def test_maybe_with_predicate_and_test[T](value: T, predicate: Callable[[T], bool], expected_bool: bool) -> None:
|
|
151
170
|
assert bool(maybe(value, predicate)) is expected_bool
|
|
152
171
|
if expected_bool is False:
|
|
153
172
|
assert maybe(value, predicate) is Nothing
|
|
173
|
+
assert maybe(value).test(predicate) is Nothing
|
|
174
|
+
else:
|
|
175
|
+
assert maybe(value).test(predicate)
|
|
154
176
|
|
|
155
177
|
@pytest.mark.parametrize(('value', 'predicate', 'expected'),
|
|
156
178
|
[
|
|
@@ -176,3 +198,24 @@ def test_maybe_pattern_matching[T](value: T, predicate: Callable[[T], bool], exp
|
|
|
176
198
|
result = None
|
|
177
199
|
|
|
178
200
|
assert result == expected
|
|
201
|
+
|
|
202
|
+
@pytest.mark.parametrize(('value', 'fn', 'expected'),
|
|
203
|
+
[
|
|
204
|
+
(None, square, Nothing),
|
|
205
|
+
(0, square, Some(0)),
|
|
206
|
+
(5, square, Some(25)),
|
|
207
|
+
('image.png', lambda s: s.split('.')[0], Some('image')),
|
|
208
|
+
],
|
|
209
|
+
)
|
|
210
|
+
def test_maybe_and_then[T, U](value: T, fn: Callable[[T], U], expected: Maybe[T]) -> None:
|
|
211
|
+
assert maybe(value).and_then(fn) == expected
|
|
212
|
+
|
|
213
|
+
@pytest.mark.parametrize(('vals', 'expected'),
|
|
214
|
+
[
|
|
215
|
+
([Some(1), Some(2), Some(3)], Some([1, 2, 3])),
|
|
216
|
+
([Some(1), Nothing, Some(3)], Nothing),
|
|
217
|
+
([Nothing, Nothing, Nothing], Nothing),
|
|
218
|
+
],
|
|
219
|
+
)
|
|
220
|
+
def test_maybe_sequence[T](vals: Iterable[Maybe[T]], expected: Maybe[list[T]]) -> None:
|
|
221
|
+
assert Maybe.sequence(vals) == expected
|