py-maybetype 0.5.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 (24) hide show
  1. py_maybetype-0.5.0/.github/workflows/python-lint-test.yml → py_maybetype-0.7.0/.github/workflows/lint-test.yml +9 -2
  2. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/.github/workflows/publish-to-pypi.yml +3 -3
  3. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/CHANGELOG.md +33 -0
  4. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/PKG-INFO +45 -18
  5. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/README.md +40 -16
  6. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/maybetype/__init__.py +51 -20
  7. py_maybetype-0.7.0/maybetype/const.py +3 -0
  8. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/pyproject.toml +5 -2
  9. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/tests/test_maybe.py +53 -10
  10. py_maybetype-0.7.0/uv.lock +994 -0
  11. py_maybetype-0.5.0/maybetype/const.py +0 -3
  12. py_maybetype-0.5.0/uv.lock +0 -108
  13. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/.gitignore +0 -0
  14. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/.markdownlint.json +0 -0
  15. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/.python-version +0 -0
  16. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/.readthedocs.yaml +0 -0
  17. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/LICENSE +0 -0
  18. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/docs/changelog.rst +0 -0
  19. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/docs/conf.py +0 -0
  20. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/docs/index.rst +0 -0
  21. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/docs/readme.rst +0 -0
  22. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/docs/reference/index.rst +0 -0
  23. {py_maybetype-0.5.0 → py_maybetype-0.7.0}/docs/requirements.txt +0 -0
  24. {py_maybetype-0.5.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,12 +40,12 @@ 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:
47
47
  - name: Download all the dists
48
- uses: actions/downlad-artifact@v6
48
+ uses: actions/download-artifact@v6
49
49
  with:
50
50
  name: python-package-distributions
51
51
  path: dist/
@@ -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,39 @@ 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
+
34
+ ## [0.6.0] - 2026-01-21
35
+
36
+ ### Changed
37
+
38
+ - Project string has been changed from `"maybetype"` to `"py-maybetype"` for getting metadata in
39
+ `const`
40
+
8
41
  ## [0.5.0] - 2026-01-18
9
42
 
10
43
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-maybetype
3
- Version: 0.5.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'
@@ -29,20 +32,28 @@ Description-Content-Type: text/markdown
29
32
 
30
33
  # py-maybetype
31
34
 
35
+ ![Linting & testing](https://github.com/svioletg/py-maybetype/actions/workflows/python-lint-test.yml/badge.svg)
36
+
32
37
  Documentation: <https://py-maybetype.readthedocs.io/en/latest/>
33
38
 
39
+ PyPI: <https://pypi.org/project/py-maybetype/>
40
+
34
41
  A basic implementation of a maybe/option type in Python, largely inspired by Rust's `Option`.
35
42
  This was created as part of a separate project I had been working on, but I decided to make it into
36
43
  its own package as I wanted to use it elsewhere and its scope grew. This is not meant to be a 1:1
37
44
  replication or replacement for Rust's `Option` or Haskell's `Maybe`, but rather just an
38
45
  interperetation of the idea that I feel works for Python.
39
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
+
40
51
  ## Usage
41
52
 
42
- Install the package with `pip` using the repository link:
53
+ Install with `pip`:
43
54
 
44
55
  ```bash
45
- pip install git+https://github.com/svioletg/py-maybetype
56
+ pip install py-maybetype
46
57
  ```
47
58
 
48
59
  Call the `maybe()` function with a `T | None` value to return a `Maybe[T]`—either a `Some` instance
@@ -73,17 +84,20 @@ assert bool(num1) is True
73
84
  assert bool(num2) is False
74
85
  ```
75
86
 
76
- 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:
77
88
 
78
89
  ```python
79
- num1: Maybe[int] = Maybe.int('5')
80
- num2: Maybe[int] = Maybe.int('five')
90
+ num1: Maybe[int] = Maybe.try_int('5')
91
+ num2: Maybe[int] = Maybe.try_int('five')
81
92
  ```
82
93
 
83
94
  The `maybe` constructor can be given an optional predicate argument to specify a custom condition
84
95
  for which `Some(value)` is returned. This argument must be a `Callable` that returns `bool`,
85
96
  where returning `False` causes the constructor to return `Nothing`.
86
97
 
98
+ > [!NOTE]
99
+ > `maybe(None)` will always returning `Nothing`, even if `predicate(None)` would return `True`
100
+
87
101
  ```python
88
102
  import re
89
103
  import uuid
@@ -108,29 +122,42 @@ from maybetype import maybe, Some
108
122
  match maybe(1):
109
123
  case Some(val):
110
124
  print('Value: ', val)
111
- case _:
125
+ case _: # "case Nothing:" also works, but just matching else in this case will be identical
112
126
  print('No value')
113
127
  ```
114
128
 
115
129
  ## Other examples
116
130
 
117
- 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`:
118
133
 
119
134
  ```python
120
135
  from datetime import datetime
121
136
  from maybetype import maybe
122
137
 
123
- date_str = '2025-09-06T030000'
124
- date = maybe('2025-09-06T030000').then(datetime.fromisoformat)
125
- # date == datetime.datetime(2025, 9, 6, 3, 0)
138
+ assert maybe('2025-09-06T030000').then(datetime.fromisoformat) == datetime(2025, 9, 6, 3, 0)
126
139
 
127
- date_str = None
128
- date = maybe(date_str).then(datetime.fromisoformat)
129
- # date == None
140
+ assert maybe(None).then(datetime.fromisoformat) is None
130
141
 
131
- date_str = ''
132
- date = maybe(date_str or None).then(datetime.fromisoformat)
133
- # date == None
142
+ assert maybe('' or None).then(datetime.fromisoformat) is None
134
143
  # Maybe does not treat falsy values as None, only strictly x-is-None values
135
144
  # Without `or None` here, datetime.fromisoformat would have raised a ValueError
136
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
+ ```
@@ -1,19 +1,27 @@
1
1
  # py-maybetype
2
2
 
3
+ ![Linting & testing](https://github.com/svioletg/py-maybetype/actions/workflows/python-lint-test.yml/badge.svg)
4
+
3
5
  Documentation: <https://py-maybetype.readthedocs.io/en/latest/>
4
6
 
7
+ PyPI: <https://pypi.org/project/py-maybetype/>
8
+
5
9
  A basic implementation of a maybe/option type in Python, largely inspired by Rust's `Option`.
6
10
  This was created as part of a separate project I had been working on, but I decided to make it into
7
11
  its own package as I wanted to use it elsewhere and its scope grew. This is not meant to be a 1:1
8
12
  replication or replacement for Rust's `Option` or Haskell's `Maybe`, but rather just an
9
13
  interperetation of the idea that I feel works for Python.
10
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
+
11
19
  ## Usage
12
20
 
13
- Install the package with `pip` using the repository link:
21
+ Install with `pip`:
14
22
 
15
23
  ```bash
16
- pip install git+https://github.com/svioletg/py-maybetype
24
+ pip install py-maybetype
17
25
  ```
18
26
 
19
27
  Call the `maybe()` function with a `T | None` value to return a `Maybe[T]`—either a `Some` instance
@@ -44,17 +52,20 @@ assert bool(num1) is True
44
52
  assert bool(num2) is False
45
53
  ```
46
54
 
47
- 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:
48
56
 
49
57
  ```python
50
- num1: Maybe[int] = Maybe.int('5')
51
- num2: Maybe[int] = Maybe.int('five')
58
+ num1: Maybe[int] = Maybe.try_int('5')
59
+ num2: Maybe[int] = Maybe.try_int('five')
52
60
  ```
53
61
 
54
62
  The `maybe` constructor can be given an optional predicate argument to specify a custom condition
55
63
  for which `Some(value)` is returned. This argument must be a `Callable` that returns `bool`,
56
64
  where returning `False` causes the constructor to return `Nothing`.
57
65
 
66
+ > [!NOTE]
67
+ > `maybe(None)` will always returning `Nothing`, even if `predicate(None)` would return `True`
68
+
58
69
  ```python
59
70
  import re
60
71
  import uuid
@@ -79,29 +90,42 @@ from maybetype import maybe, Some
79
90
  match maybe(1):
80
91
  case Some(val):
81
92
  print('Value: ', val)
82
- case _:
93
+ case _: # "case Nothing:" also works, but just matching else in this case will be identical
83
94
  print('No value')
84
95
  ```
85
96
 
86
97
  ## Other examples
87
98
 
88
- 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`:
89
101
 
90
102
  ```python
91
103
  from datetime import datetime
92
104
  from maybetype import maybe
93
105
 
94
- date_str = '2025-09-06T030000'
95
- date = maybe('2025-09-06T030000').then(datetime.fromisoformat)
96
- # date == datetime.datetime(2025, 9, 6, 3, 0)
106
+ assert maybe('2025-09-06T030000').then(datetime.fromisoformat) == datetime(2025, 9, 6, 3, 0)
97
107
 
98
- date_str = None
99
- date = maybe(date_str).then(datetime.fromisoformat)
100
- # date == None
108
+ assert maybe(None).then(datetime.fromisoformat) is None
101
109
 
102
- date_str = ''
103
- date = maybe(date_str or None).then(datetime.fromisoformat)
104
- # date == None
110
+ assert maybe('' or None).then(datetime.fromisoformat) is None
105
111
  # Maybe does not treat falsy values as None, only strictly x-is-None values
106
112
  # Without `or None` here, datetime.fromisoformat would have raised a ValueError
107
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,
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ PROJECT_VERSION: str = importlib.metadata.version('py-maybetype')
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "py-maybetype"
3
- version = "0.5.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