py-maybetype 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
maybetype/__init__.py ADDED
@@ -0,0 +1,185 @@
1
+ import warnings
2
+ from collections.abc import Callable, Iterable
3
+ from typing import Any, Never
4
+
5
+
6
+ class Maybe[T]:
7
+ """
8
+ Wraps a value that may be ``T`` or ``None``, providing methods for conditionally using that value or
9
+ short-circuiting to ``None`` without longer checks.
10
+ """
11
+ __match_args__ = ('val',)
12
+
13
+ def __init__(self, val: T | None) -> None:
14
+ warnings.warn(
15
+ 'Direct instancing of Maybe() is not recommended as of v0.5.0, use the maybe() function instead',
16
+ stacklevel=2,
17
+ )
18
+ self.val = val
19
+
20
+ def __repr__(self) -> str:
21
+ return f'{self.__class__.__name__}({self.val!r})'
22
+
23
+ def __bool__(self) -> bool:
24
+ return self.val is not None
25
+
26
+ def __eq__(self, other: object) -> bool:
27
+ """
28
+ Returns ``False`` if the compared object is not a ``Maybe`` instance, otherwise compares their wrapped values.
29
+ """
30
+ if not isinstance(other, Maybe):
31
+ return False
32
+ return self.val == other.val
33
+
34
+ def __hash__(self) -> int:
35
+ return self.val.__hash__()
36
+
37
+ @staticmethod
38
+ def cat(vals: 'Iterable[Maybe[T]]') -> list[T]:
39
+ """
40
+ Takes an iterable of ``Maybe`` and returns a list of the unwrapped values of all ``Some`` objects.
41
+
42
+ >>> vals = [maybe(5), maybe(None), maybe(10), maybe(None)]
43
+ >>> assert vals == [Some(5), Nothing, Some(10), Nothing]
44
+ >>> assert Maybe.cat(vals) == [5, 10]
45
+ """
46
+ return [i.unwrap() for i in vals if i]
47
+
48
+ @staticmethod
49
+ def int(val: Any) -> 'Maybe[int]': # noqa: ANN401
50
+ """
51
+ Attempts to convert ``val`` to an ``int``, returning a ``Some``-wrapped ``int`` if successful, or
52
+ ``Nothing`` on failure.
53
+ """
54
+ try:
55
+ i: int = int(val)
56
+ return Some(i)
57
+ except ValueError:
58
+ return Nothing
59
+
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]
64
+
65
+ def attr[V](self, name: str, typ: type[V] | None = None, *, err: bool = False) -> 'Maybe[V]':
66
+ """
67
+ Attempts to access an attribute ``name`` on the wrapped object, returning a ``Some`` instance wrapping the
68
+ the value if it exists, or ``Nothing`` otherwise.
69
+
70
+ :param typ: Specifies the generic type of the resulting ``Maybe``. Note that the potential value returned by
71
+ this method will not be coerced to the given type at runtime; this argument is only for typing purposes.
72
+ :param err: If ``True``, ``AttributeError`` is raised instead of returning ``Nothing`` if ``name`` does not
73
+ exist.
74
+ """
75
+ return Some(getattr(self.val, name)) if err else maybe(getattr(self.val, name, None))
76
+
77
+ def attr_or[V](self, name: str, default: V) -> V:
78
+ """
79
+ Similar to the ``attr`` method, but unwraps the result if the attribute exists or returns the required default
80
+ value otherwise.
81
+ """
82
+ try:
83
+ return self.attr(name, err=True).unwrap()
84
+ except AttributeError:
85
+ return default
86
+
87
+ def get[V](self,
88
+ accessor: Any, # noqa: ANN401
89
+ _typ: type[V] | None = None,
90
+ *,
91
+ err: bool = False,
92
+ default: V | None = None,
93
+ ) -> 'Maybe[V]':
94
+ """
95
+ Attempts to access an item by ``accessor`` on the wrapped object, assuming the wrapped value implements
96
+ ``__getitem__``. If it does not, or if the value does not exist (list index out of range, key does not exist on
97
+ a dictionary, etc.), ``Nothing`` is returned.
98
+
99
+ :param typ: Specifies the generic type of the resulting ``Maybe``. Note that the potential value returned by
100
+ this method will not be coerced to the given type at runtime; this argument is only for typing purposes.
101
+ :param err: By default, ``IndexError`` and ``KeyError`` are not raised when ``__getitem__`` is called on the
102
+ wrapped value, and ``Nothing`` is returned instead. Setting ``err`` to ``True`` allows these errors to
103
+ be raised as they normally would. Note that if ``__getitem__`` did not exist on the wrapped value in the
104
+ first placed (such as with ``Nothing``), no error is raised, and ``Nothing`` is returned regardless.
105
+ :param default: Specifies an alternate value to return a ``Some`` of instead of returning ``Nothing``.
106
+ """
107
+ if hasattr(self.val, '__getitem__'):
108
+ try:
109
+ return Some(self.val.__getitem__(accessor)) # type: ignore
110
+ except (IndexError, KeyError):
111
+ if err:
112
+ raise
113
+ return maybe(default)
114
+
115
+ def then[R](self, func: Callable[[T], R]) -> R | None:
116
+ """
117
+ Calls ``func`` with the wrapped value as the argument and returns its value, or returns ``None`` if the wrapped
118
+ value is ``None``.
119
+
120
+ :param func: A ``Callable`` which takes a type of the possible wrapped value (``T``) and can return any type
121
+ (``R``).
122
+ """
123
+ return func(self.val) if self.val is not None else None
124
+
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
+ def unwrap(self,
130
+ exc: Exception | Callable[..., Never] | None = None,
131
+ *exc_args: object,
132
+ ) -> T:
133
+ """
134
+ Returns the wrapped value if it is not ``None``, otherwise raises ``ValueError`` by default.
135
+
136
+ :param exc: The exception to raise if the wrapped value is ``None``. Can be either an ``Exception`` object, or a
137
+ ``Callable`` which takes any arguments and does not return. If given ``None``, the default behavior is to
138
+ raise a ``ValueError`` with the message ``Maybe[<type>] unwrapped into None``.
139
+ :param exc_args: Arguments to call ``exc`` with, if ``exc`` is a ``Callable``. Otherwise, this argument is not
140
+ used.
141
+ """
142
+ if self.val is None:
143
+ if isinstance(exc, Exception):
144
+ raise exc
145
+ if isinstance(exc, Callable):
146
+ exc(*exc_args)
147
+ raise ValueError(f'Maybe[{T.__name__}] unwrapped into None')
148
+ return self.val
149
+
150
+ def unwrap_or(self, other: T) -> T:
151
+ """Returns the wrapped value if it is not ``None``, otherwise returns ``other``."""
152
+ return self.val if self.val is not None else other
153
+
154
+ class Some[T](Maybe[T]):
155
+ def __init__(self, val: T) -> None:
156
+ self.val = val
157
+
158
+ def __bool__(self) -> bool:
159
+ return True
160
+
161
+ class _Nothing[T](Maybe[T]):
162
+ __match_args__ = ()
163
+
164
+ def __init__(self, _: None = None) -> None:
165
+ self.val = None
166
+
167
+ def __repr__(self) -> str:
168
+ return 'Nothing'
169
+
170
+ def __bool__(self) -> bool:
171
+ return False
172
+
173
+ Nothing = _Nothing()
174
+
175
+ def maybe[T](val: T | None, predicate: Callable[[T], bool] = lambda v: v is not None) -> Maybe[T]:
176
+ """
177
+ Returns a ``Some`` instance wrapping ``val`` if either ``val`` is not ``None`` or ``predicate(val)`` is ``True``,
178
+ otherwise returns the ``Nothing`` singleton.
179
+
180
+ :param val: A value to wrap.
181
+ :param predicate: An optional function that takes ``val`` and, if it returns ``False``, discards ``val``
182
+ and returns a ``Nothing`` instance. Regardless of the predicate, ``Nothing`` is always returned if
183
+ ``val`` is ``None``.
184
+ """
185
+ return Nothing if (val is None) or not predicate(val) else Some(val)
maybetype/const.py ADDED
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ PROJECT_VERSION: str = importlib.metadata.version('maybetype')
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-maybetype
3
+ Version: 0.5.0
4
+ Summary: A basic implementation of a maybe/option type in Python, largely inspired by Rust's Option.
5
+ Project-URL: Homepage, https://github.com/svioletg/py-maybetype
6
+ Project-URL: Repository, https://github.com/svioletg/py-maybetype
7
+ Project-URL: Documentation, https://py-maybetype.readthedocs.io/en/latest/
8
+ Project-URL: Changelog, https://github.com/svioletg/py-maybetype/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/svioletg/py-maybetype/issues
10
+ Author: Seth 'Violet' Gibbs
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.12
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=9.0.2; extra == 'dev'
22
+ Requires-Dist: ruff>=0.14.8; extra == 'dev'
23
+ Provides-Extra: docs
24
+ Requires-Dist: furo>=2025.12.19; extra == 'docs'
25
+ Requires-Dist: myst-parser>=4.0.1; extra == 'docs'
26
+ Requires-Dist: sphinx-autobuild>=2025.8.25; extra == 'docs'
27
+ Requires-Dist: sphinx<9.0,>=6.0; extra == 'docs'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # py-maybetype
31
+
32
+ Documentation: <https://py-maybetype.readthedocs.io/en/latest/>
33
+
34
+ A basic implementation of a maybe/option type in Python, largely inspired by Rust's `Option`.
35
+ This was created as part of a separate project I had been working on, but I decided to make it into
36
+ its own package as I wanted to use it elsewhere and its scope grew. This is not meant to be a 1:1
37
+ replication or replacement for Rust's `Option` or Haskell's `Maybe`, but rather just an
38
+ interperetation of the idea that I feel works for Python.
39
+
40
+ ## Usage
41
+
42
+ Install the package with `pip` using the repository link:
43
+
44
+ ```bash
45
+ pip install git+https://github.com/svioletg/py-maybetype
46
+ ```
47
+
48
+ Call the `maybe()` function with a `T | None` value to return a `Maybe[T]`—either a `Some` instance
49
+ containing the wrapped value, or the `Nothing` singleton.
50
+
51
+ ```python
52
+ from maybetype import Maybe, maybe
53
+
54
+ # Only the maybe() function should be used,
55
+ # the Maybe class is only imported here for type annotations
56
+
57
+ def try_int(x: str) -> int | None:
58
+ """Attempts to convert a string of digits into an `int`, returning `None` if not possible."""
59
+ try:
60
+ return int(x)
61
+ except ValueError:
62
+ return None
63
+
64
+ num1: Maybe[int] = maybe(try_int('5'))
65
+ num2: Maybe[int] = maybe(try_int('five'))
66
+
67
+ print(num1.unwrap()) # 5
68
+ print(num2.unwrap()) # (raises ValueError)
69
+
70
+ # Some() instances are always truthy, Nothing is falsy
71
+
72
+ assert bool(num1) is True
73
+ assert bool(num2) is False
74
+ ```
75
+
76
+ This example in particular can also be done with `Maybe`'s built-in `int()` class method:
77
+
78
+ ```python
79
+ num1: Maybe[int] = Maybe.int('5')
80
+ num2: Maybe[int] = Maybe.int('five')
81
+ ```
82
+
83
+ The `maybe` constructor can be given an optional predicate argument to specify a custom condition
84
+ for which `Some(value)` is returned. This argument must be a `Callable` that returns `bool`,
85
+ where returning `False` causes the constructor to return `Nothing`.
86
+
87
+ ```python
88
+ import re
89
+ import uuid
90
+
91
+ from maybetype import maybe
92
+
93
+ def is_valid_uuid(s: str) -> bool:
94
+ 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
95
+
96
+ assert maybe('3b1bcc3a-41d5-49a5-8273-10cc605e31f9', is_valid_uuid)
97
+ assert maybe('3b1bcc3a41d549a5827310cc605e31f9', is_valid_uuid)
98
+ assert not maybe('qwertyuiopasdfghjklzxcvbnm', is_valid_uuid)
99
+ assert not maybe('nf0cmmdq-l0gt-rq5a-upry-706trht3ocv9', is_valid_uuid)
100
+ ```
101
+
102
+ `Maybe` instances can also be used in `match`/`case` pattern matching to access the wrapped value,
103
+ like so:
104
+
105
+ ```python
106
+ from maybetype import maybe, Some
107
+
108
+ match maybe(1):
109
+ case Some(val):
110
+ print('Value: ', val)
111
+ case _:
112
+ print('No value')
113
+ ```
114
+
115
+ ## Other examples
116
+
117
+ Converting a `str | None` timestamp into a `datetime` object if not `None`, otherwise returning `None`:
118
+
119
+ ```python
120
+ from datetime import datetime
121
+ from maybetype import maybe
122
+
123
+ date_str = '2025-09-06T030000'
124
+ date = maybe('2025-09-06T030000').then(datetime.fromisoformat)
125
+ # date == datetime.datetime(2025, 9, 6, 3, 0)
126
+
127
+ date_str = None
128
+ date = maybe(date_str).then(datetime.fromisoformat)
129
+ # date == None
130
+
131
+ date_str = ''
132
+ date = maybe(date_str or None).then(datetime.fromisoformat)
133
+ # date == None
134
+ # Maybe does not treat falsy values as None, only strictly x-is-None values
135
+ # Without `or None` here, datetime.fromisoformat would have raised a ValueError
136
+ ```
@@ -0,0 +1,6 @@
1
+ maybetype/__init__.py,sha256=CQFFyrpwovLYg_17QDcxY8jssh9CISi6UMlsilL-3kc,7696
2
+ maybetype/const.py,sha256=Vu_oNFgMuJFf4wGqh2ywIO7I0rA0zlrJb4KdPeoupns,90
3
+ py_maybetype-0.5.0.dist-info/METADATA,sha256=jxcQQqsJTYiZ_qhQFrMqJ_d6lpMJosG-JrdHzW39KEc,4562
4
+ py_maybetype-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ py_maybetype-0.5.0.dist-info/licenses/LICENSE,sha256=eMbi_IczZg-O56zEg00k6bmfExkQTpx01DN1xLY2w9I,1076
6
+ py_maybetype-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Seth "Violet" Gibbs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.