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.
Files changed (23) hide show
  1. py_maybetype-0.6.0/.github/workflows/python-lint-test.yml → py_maybetype-0.7.0/.github/workflows/lint-test.yml +9 -2
  2. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.github/workflows/publish-to-pypi.yml +2 -2
  3. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/CHANGELOG.md +26 -0
  4. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/PKG-INFO +39 -16
  5. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/README.md +34 -14
  6. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/maybetype/__init__.py +51 -20
  7. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/pyproject.toml +5 -2
  8. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/tests/test_maybe.py +53 -10
  9. py_maybetype-0.7.0/uv.lock +994 -0
  10. py_maybetype-0.6.0/uv.lock +0 -108
  11. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.gitignore +0 -0
  12. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.markdownlint.json +0 -0
  13. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.python-version +0 -0
  14. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/.readthedocs.yaml +0 -0
  15. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/LICENSE +0 -0
  16. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/changelog.rst +0 -0
  17. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/conf.py +0 -0
  18. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/index.rst +0 -0
  19. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/readme.rst +0 -0
  20. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/reference/index.rst +0 -0
  21. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/docs/requirements.txt +0 -0
  22. {py_maybetype-0.6.0 → py_maybetype-0.7.0}/maybetype/const.py +0 -0
  23. {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 3.12
22
+ - name: Set up Python
20
23
  uses: actions/setup-python@v3
21
24
  with:
22
- python-version: "3.12"
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.6.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.14.8; extra == 'dev'
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`'s built-in `int()` class method:
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.int('5')
84
- num2: Maybe[int] = Maybe.int('five')
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 `None`:
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
- date_str = '2025-09-06T030000'
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
- date_str = None
132
- date = maybe(date_str).then(datetime.fromisoformat)
133
- # date == None
140
+ assert maybe(None).then(datetime.fromisoformat) is None
134
141
 
135
- date_str = ''
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`'s built-in `int()` class method:
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.int('5')
55
- num2: Maybe[int] = Maybe.int('five')
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 `None`:
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
- date_str = '2025-09-06T030000'
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
- date_str = None
103
- date = maybe(date_str).then(datetime.fromisoformat)
104
- # date == None
108
+ assert maybe(None).then(datetime.fromisoformat) is None
105
109
 
106
- date_str = ''
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() is not recommended as of v0.5.0, use the maybe() function instead',
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.__hash__()
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 int(val: Any) -> 'Maybe[int]': # noqa: ANN401
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
- @staticmethod
61
- def map[A, B](fn: 'Callable[[A], Maybe[B]]', vals: Iterable[A]) -> list[B]:
62
- """Maps ``fn`` onto ``vals``, taking the unwrapped values of ``Some``s and discarding ``Nothing``s."""
63
- return [i.unwrap() for i in map(fn, vals) if i]
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[V](self, name: str, typ: type[V] | None = None, *, err: bool = False) -> 'Maybe[V]':
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[V](self, name: str, default: V) -> V:
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[V](self,
111
+ def get[U](self,
88
112
  accessor: Any, # noqa: ANN401
89
- _typ: type[V] | None = None,
113
+ _typ: type[U] | None = None,
90
114
  *,
91
115
  err: bool = False,
92
- default: V | None = None,
93
- ) -> 'Maybe[V]':
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 then[R](self, func: Callable[[T], R]) -> R | None:
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
- Calls ``func`` with the wrapped value as the argument and returns its value, or returns ``None`` if the wrapped
118
- value is ``None``.
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.6.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.14.8",
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
- def test_maybe_this_or() -> None:
28
- assert Maybe.int('10').this_or(0).unwrap() == 10 # noqa: PLR2004
29
- assert Maybe.int('ten').this_or(0).unwrap() == 0
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.int('ten').unwrap()
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.int, ALPHANUMERIC)) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
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.int, ALPHANUMERIC) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
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 test_maybe_with_predicate[T](value: T, predicate: Callable[[T], bool], expected_bool: bool) -> None:
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