├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── RELEASING.md
├── codecov.yml
├── docs
├── .pages
├── README.md
└── maybe.md
├── pyproject.toml
├── requirements-dev.txt
├── setup.cfg
├── setup.py
├── src
└── maybe
│ ├── __init__.py
│ ├── maybe.py
│ └── py.typed
├── tests
├── test_maybe.py
├── test_pattern_matching.py
└── type-checking
│ └── test_maybe.yml
└── tox.ini
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
2 | version: 2
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "monthly"
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | ---
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 |
8 | name: CI
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python:
16 | - version: '3.12'
17 | - version: '3.11'
18 | - version: '3.10'
19 | - version: '3.9'
20 | exclude-pattern-matching: true
21 | - version: '3.8'
22 | exclude-pattern-matching: true
23 | name: Python ${{ matrix.python.version }}
24 | steps:
25 | # Check out code
26 | - uses: actions/checkout@v4
27 |
28 | # Python
29 | - name: Setup python ${{ matrix.python.version }}
30 | uses: actions/setup-python@v5
31 | with:
32 | python-version: ${{ matrix.python.version }}
33 | cache: pip
34 | cache-dependency-path: requirements-dev.txt
35 |
36 | - name: Install dev dependencies
37 | run: pip install --root-user-action=ignore --upgrade pip --requirement requirements-dev.txt
38 |
39 | # Install library
40 | - name: Install maybe
41 | run: pip install --root-user-action=ignore --editable .[result]
42 |
43 | # Tests
44 | - name: Run tests (excluding pattern matching)
45 | if: ${{ matrix.python.exclude-pattern-matching }}
46 | run: pytest --ignore=tests/test_pattern_matching.py
47 | - name: Run tests (including pattern matching)
48 | if: ${{ ! matrix.python.exclude-pattern-matching }}
49 | run: pytest
50 |
51 | # Linters
52 | - name: Run flake8 (excluding pattern matching)
53 | if: ${{ matrix.python.exclude-pattern-matching }}
54 | run: flake8 --extend-exclude tests/test_pattern_matching.py
55 | - name: Run flake8 (including pattern matching)
56 | if: ${{ ! matrix.python.exclude-pattern-matching }}
57 | run: flake8
58 | - name: Run mypy
59 | run: mypy
60 |
61 | # Packaging
62 | - name: Build packages
63 | run: |
64 | pip install --root-user-action=ignore --upgrade build pip setuptools wheel
65 | python -m build
66 |
67 | # Coverage
68 | - name: Upload coverage to codecov.io
69 | uses: codecov/codecov-action@v4
70 | if: matrix.python == '3.9'
71 | with:
72 | token: ${{ secrets.CODECOV_TOKEN }}
73 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/python
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python
3 |
4 | ### Python ###
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 |
10 | # Distribution / packaging
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | cover/
54 |
55 | # IPython
56 | profile_default/
57 | ipython_config.py
58 |
59 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
60 | __pypackages__/
61 |
62 | # Environments
63 | .env
64 | .venv
65 | env/
66 | venv/
67 | ENV/
68 | env.bak/
69 | venv.bak/
70 |
71 | # Spyder project settings
72 | .spyderproject
73 | .spyproject
74 |
75 | # Rope project settings
76 | .ropeproject
77 |
78 | # mkdocs documentation
79 | /site
80 |
81 | # mypy
82 | .mypy_cache/
83 | .dmypy.json
84 | dmypy.json
85 |
86 | # Pyre type checker
87 | .pyre/
88 |
89 | # pytype static type analyzer
90 | .pytype/
91 |
92 | # Cython debug symbols
93 | cython_debug/
94 |
95 | # PyCharm
96 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
97 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
98 | # and can be added to the global gitignore or merged into this file. For a more nuclear
99 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
100 | .idea/
101 |
102 | ### Python Patch ###
103 |
104 | # ruff
105 | .ruff_cache/
106 |
107 | # LSP config files
108 | pyrightconfig.json
109 |
110 | # End of https://www.toptal.com/developers/gitignore/api/python
111 | # Created by https://www.toptal.com/developers/gitignore/api/vim
112 | # Edit at https://www.toptal.com/developers/gitignore?templates=vim
113 |
114 | ### Vim ###
115 | # Swap
116 | [._]*.s[a-v][a-z]
117 | !*.svg # comment out if you don't need vector files
118 | [._]*.sw[a-p]
119 | [._]s[a-rt-v][a-z]
120 | [._]ss[a-gi-z]
121 | [._]sw[a-p]
122 |
123 | # Session
124 | Session.vim
125 | Sessionx.vim
126 |
127 | # Temporary
128 | .netrwhist
129 | *~
130 | # Auto-generated tag files
131 | tags
132 | # Persistent undo
133 | [._]*.un~
134 |
135 | # End of https://www.toptal.com/developers/gitignore/api/vim
136 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
137 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
138 |
139 | ### VisualStudioCode ###
140 | .vscode/
141 | # !.vscode/settings.json
142 | # !.vscode/tasks.json
143 | # !.vscode/launch.json
144 | # !.vscode/extensions.json
145 | # !.vscode/*.code-snippets
146 |
147 | # Local History for Visual Studio Code
148 | .history/
149 |
150 | # Built Visual Studio Code Extensions
151 | *.vsix
152 |
153 | ### VisualStudioCode Patch ###
154 | # Ignore all local history of files
155 | .history
156 | .ionide
157 |
158 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
159 | # Created by https://www.toptal.com/developers/gitignore/api/jetbrains
160 | # Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains
161 |
162 | ### JetBrains ###
163 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
164 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
165 |
166 | # User-specific stuff
167 | .idea/**/workspace.xml
168 | .idea/**/tasks.xml
169 | .idea/**/usage.statistics.xml
170 | .idea/**/dictionaries
171 | .idea/**/shelf
172 |
173 | # AWS User-specific
174 | .idea/**/aws.xml
175 |
176 | # Generated files
177 | .idea/**/contentModel.xml
178 |
179 | # Sensitive or high-churn files
180 | .idea/**/dataSources/
181 | .idea/**/dataSources.ids
182 | .idea/**/dataSources.local.xml
183 | .idea/**/sqlDataSources.xml
184 | .idea/**/dynamic.xml
185 | .idea/**/uiDesigner.xml
186 | .idea/**/dbnavigator.xml
187 |
188 | # Gradle
189 | .idea/**/gradle.xml
190 | .idea/**/libraries
191 |
192 | # Gradle and Maven with auto-import
193 | # When using Gradle or Maven with auto-import, you should exclude module files,
194 | # since they will be recreated, and may cause churn. Uncomment if using
195 | # auto-import.
196 | # .idea/artifacts
197 | # .idea/compiler.xml
198 | # .idea/jarRepositories.xml
199 | # .idea/modules.xml
200 | # .idea/*.iml
201 | # .idea/modules
202 | # *.iml
203 | # *.ipr
204 |
205 | # CMake
206 | cmake-build-*/
207 |
208 | # Mongo Explorer plugin
209 | .idea/**/mongoSettings.xml
210 |
211 | # File-based project format
212 | *.iws
213 |
214 | # IntelliJ
215 | out/
216 |
217 | # mpeltonen/sbt-idea plugin
218 | .idea_modules/
219 |
220 | # JIRA plugin
221 | atlassian-ide-plugin.xml
222 |
223 | # Cursive Clojure plugin
224 | .idea/replstate.xml
225 |
226 | # SonarLint plugin
227 | .idea/sonarlint/
228 |
229 | # Crashlytics plugin (for Android Studio and IntelliJ)
230 | com_crashlytics_export_strings.xml
231 | crashlytics.properties
232 | crashlytics-build.properties
233 | fabric.properties
234 |
235 | # Editor-based Rest Client
236 | .idea/httpRequests
237 |
238 | # Android studio 3.1+ serialized cache file
239 | .idea/caches/build_file_checksums.ser
240 |
241 | ### JetBrains Patch ###
242 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
243 |
244 | # *.iml
245 | # modules.xml
246 | # .idea/misc.xml
247 | # *.ipr
248 |
249 | # Sonarlint plugin
250 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
251 | .idea/**/sonarlint/
252 |
253 | # SonarQube Plugin
254 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
255 | .idea/**/sonarIssues.xml
256 |
257 | # Markdown Navigator plugin
258 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
259 | .idea/**/markdown-navigator.xml
260 | .idea/**/markdown-navigator-enh.xml
261 | .idea/**/markdown-navigator/
262 |
263 | # Cache file creation bug
264 | # See https://youtrack.jetbrains.com/issue/JBR-2257
265 | .idea/$CACHE_FILE$
266 |
267 | # CodeStream plugin
268 | # https://plugins.jetbrains.com/plugin/12206-codestream
269 | .idea/codestream.xml
270 |
271 | # Azure Toolkit for IntelliJ plugin
272 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
273 | .idea/**/azureSettings.xml
274 |
275 | # End of https://www.toptal.com/developers/gitignore/api/jetbrains
276 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2023 rustedpy maintainers and contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include src/maybe/py.typed
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # phony trick from https://keleshev.com/my-book-writing-setup/
2 | .PHONY: phony
3 |
4 | # "True" if running Python < (3, 10); "False" otherwise.
5 | PYTHON_PRE_310 := $(shell python -c "import sys; print(sys.version_info < (3, 10))")
6 |
7 | install: phony
8 | @echo Installing dependencies...
9 | python -m pip install --require-virtualenv -r requirements-dev.txt
10 | python -m pip install --require-virtualenv -e .[result]
11 |
12 | lint: phony lint-flake lint-mypy
13 |
14 | lint-flake:
15 | ifeq ($(PYTHON_PRE_310), True)
16 | @# Python <3.10 doesn't support pattern matching.
17 | flake8 --extend-exclude tests/test_pattern_matching.py
18 | else
19 | flake8
20 | endif
21 |
22 | lint-mypy: phony
23 | mypy
24 |
25 | test: phony
26 | pytest -vv
27 |
28 | docs: phony
29 | lazydocs \
30 | --overview-file README.md \
31 | --src-base-url https://github.com/rustedpy/maybe/blob/main/ \
32 | ./src/maybe
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Maybe
2 |
3 | [](https://opensource.org/licenses/MIT)
4 | [](https://pypi.org/project/rustedpy-maybe/)
5 | [](https://pypi.org/project/rustedpy-maybe/)
6 | [](https://github.com/rustedpy/maybe/actions/workflows/ci.yml?query=branch%3Amaster)
7 | [](http://mypy-lang.org/)
8 | [](https://codecov.io/gh/rustedpy/maybe)
9 |
10 | A simple Maybe (Option) type for Python 3 [inspired by Rust](
11 | https://doc.rust-lang.org/std/option/), fully type annotated.
12 |
13 | ## Installation
14 |
15 | Latest release:
16 |
17 | ```sh
18 | pip install rustedpy-maybe
19 | ```
20 |
21 | Latest GitHub `master` branch version:
22 |
23 | ```sh
24 | pip install git+https://github.com/rustedpy/maybe
25 | ```
26 |
27 | There are no dependencies outside of the Python standard library. However, if
28 | you wish to use the `Result` conversion methods (see examples in the next
29 | section), you will need to install the `result` extra.
30 |
31 | In this case, rather than installing via one of the commands above, you can
32 | install the package with the `result` extra either from the latest release:
33 |
34 | ```sh
35 | pip install rustedpy-maybe[result]
36 | ```
37 |
38 | or from the GitHub `master` branch:
39 |
40 | ```sh
41 | pip install git+https://github.com/rustedpy/maybe[result]
42 | ```
43 |
44 | ## Summary
45 |
46 | **Experimental. API subject to change.**
47 |
48 | The idea is that a possible value can be either `Some(value)` or `Nothing()`,
49 | with a way to differentiate between the two. `Some` and `Nothing` are both
50 | classes encapsulating a possible value.
51 |
52 | Example usage:
53 |
54 | ```python
55 | from maybe import Nothing, Some
56 |
57 | o = Some('yay')
58 | n = Nothing()
59 | assert o.unwrap_or_else(str.upper) == 'yay'
60 | assert n.unwrap_or_else(lambda: 'default') == 'default'
61 | ```
62 |
63 | There are some methods that support conversion from a `Maybe` to a `Result` type
64 | in the [result library](https://github.com/rustedpy/result/). If you wish to
65 | leverage these methods, you must install the `result` extra as described in the
66 | installation section.
67 |
68 | Example usage:
69 |
70 | ```python
71 | from maybe import Nothing, Some
72 | from result import Ok, Err
73 |
74 | o = Some('yay')
75 | n = Nothing()
76 | assert o.ok_or('error') == Ok('yay')
77 | assert o.ok_or_else(lambda: 'error') == Ok('yay')
78 | assert n.ok_or('error') == Err('error')
79 | assert n.ok_or_else(lambda: 'error') == Err('error')
80 | ```
81 |
82 | ## Contributing
83 |
84 | These steps should work on any Unix-based system (Linux, macOS, etc) with Python
85 | and `make` installed. On Windows, you will need to refer to the Python
86 | documentation (linked below) and reference the `Makefile` for commands to run
87 | from the non-unix shell you're using on Windows.
88 |
89 | 1. Setup and activate a virtual environment. See [Python docs][pydocs-venv] for
90 | more information about virtual environments and setup.
91 | 1. Run `make install` to install dependencies
92 | 1. Switch to a new git branch and make your changes
93 | 1. Test your changes:
94 | - `make test`
95 | - `make lint`
96 | - You can also start a Python REPL and import `maybe`
97 | 1. Update documentation
98 | - Edit any relevant docstrings, markdown files
99 | - Run `make docs`
100 | 1. Add an entry to the [changelog](./CHANGELOG.md)
101 | 1. Git commit all your changes and create a new PR.
102 |
103 | [pydocs-venv]: https://docs.python.org/3/library/venv.html
104 |
105 | ## License
106 |
107 | MIT License
108 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Release process
2 |
3 | 1) Export the necessary environment variables:
4 | ```
5 | # Examples: '0.8.0', '0.8.0rc1', '0.8.0b1'
6 | export VERSION={VERSION BEING RELEASED}
7 |
8 | # See `gpg -k`
9 | export GPG={YOUR GPG}
10 | ```
11 |
12 | 2) Update version numbers to match the version being released:
13 | ```
14 | vim -p src/maybe/__init__.py CHANGELOG.md
15 | ```
16 |
17 | 3) Update diff link in CHANGELOG.md ([see example][diff-link-update-pr-example]):
18 | ```
19 | vim CHANGELOG.md
20 | ```
21 |
22 | 4) Do a signed commit and signed tag of the release:
23 | ```
24 | git add src/maybe/__init__.py CHANGELOG.md
25 | git commit -S${GPG} -m "Release v${VERSION}"
26 | git tag -u ${GPG} -m "Release v${VERSION}" v${VERSION}
27 | ```
28 |
29 | 5) Build source and binary distributions:
30 | ```
31 | rm -rf ./dist
32 | python3 -m build
33 | ```
34 |
35 | 6) Upload package to PyPI:
36 | ```
37 | twine upload dist/rustedpy-maybe-${VERSION}.tar.gz dist/rustedpy_maybe-${VERSION}-*.whl
38 | git push
39 | git push --tags
40 | ```
41 |
42 | 7) Optionally check the new version is published correctly
43 | - https://github.com/rustedpy/maybe/tags
44 | - https://pypi.org/project/maybe/#history
45 |
46 | 8) Update version number to next dev version (for example after `v0.9.0` this should be set to `0.10.0.dev0`:
47 | ```
48 | vim -p src/maybe/__init__.py
49 | ```
50 |
51 | [diff-link-update-pr-example]: https://github.com/rustedpy/result/pull/77/files
52 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ---
2 | coverage:
3 | precision: 2
4 | round: nearest
5 | range: "80...100"
6 | status:
7 | project:
8 | default:
9 | target: 85%
10 | threshold: 3%
11 | comment:
12 | layout: "diff, flags, files"
13 | behavior: default
14 | require_changes: true
15 |
--------------------------------------------------------------------------------
/docs/.pages:
--------------------------------------------------------------------------------
1 | title: API Reference
2 | nav:
3 | - Overview: README.md
4 | - ...
5 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # API Overview
4 |
5 | ## Modules
6 |
7 | - [`maybe`](./maybe.md#module-maybe)
8 |
9 | ## Classes
10 |
11 | - [`maybe.Nothing`](./maybe.md#class-nothing): An object that indicates no inner value is present
12 | - [`maybe.Some`](./maybe.md#class-some): An object that indicates some inner value is present
13 | - [`maybe.UnwrapError`](./maybe.md#class-unwraperror): Exception raised from ``.unwrap_<...>`` and ``.expect_<...>`` calls.
14 |
15 | ## Functions
16 |
17 | - [`maybe.is_nothing`](./maybe.md#function-is_nothing): A typeguard to check if a maybe is a `Nothing`.
18 | - [`maybe.is_some`](./maybe.md#function-is_some): A typeguard to check if a maybe is a `Some`.
19 |
20 |
21 | ---
22 |
23 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._
24 |
--------------------------------------------------------------------------------
/docs/maybe.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `maybe`
6 |
7 |
8 |
9 |
10 | **Global Variables**
11 | ---------------
12 | - **SomeNothing**
13 |
14 | ---
15 |
16 |
17 |
18 | ## function `is_some`
19 |
20 | ```python
21 | is_some(maybe: 'Maybe[T]') → TypeGuard[Some[T]]
22 | ```
23 |
24 | A typeguard to check if a maybe is a `Some`.
25 |
26 | Usage:
27 |
28 | ```plain
29 | >>> r: Maybe[int, str] = get_a_maybe()
30 | >>> if is_some(r):
31 | ... r # r is of type Some[int]
32 | ... elif is_nothing(r):
33 | ... r # r is of type Nothing[str]
34 | ```
35 |
36 |
37 | ---
38 |
39 |
40 |
41 | ## function `is_nothing`
42 |
43 | ```python
44 | is_nothing(maybe: 'Maybe[T]') → TypeGuard[Nothing]
45 | ```
46 |
47 | A typeguard to check if a maybe is a `Nothing`.
48 |
49 | Usage:
50 |
51 | ```plain
52 | >>> r: Maybe[int, str] = get_a_maybe()
53 | >>> if is_some(r):
54 | ... r # r is of type Some[int]
55 | ... elif is_nothing(r):
56 | ... r # r is of type Nothing[str]
57 | ```
58 |
59 |
60 | ---
61 |
62 |
63 |
64 | ## class `Some`
65 | An object that indicates some inner value is present
66 |
67 |
68 |
69 | ### method `__init__`
70 |
71 | ```python
72 | __init__(value: 'T') → None
73 | ```
74 |
75 |
76 |
77 |
78 |
79 |
80 | ---
81 |
82 | #### property some_value
83 |
84 | Return the inner value.
85 |
86 |
87 |
88 | ---
89 |
90 |
91 |
92 | ### method `and_then`
93 |
94 | ```python
95 | and_then(op: 'Callable[[T], Maybe[U]]') → Maybe[U]
96 | ```
97 |
98 | There is a contained value, so return the maybe of `op` with the original value passed in
99 |
100 | ---
101 |
102 |
103 |
104 | ### method `expect`
105 |
106 | ```python
107 | expect(_message: 'str') → T
108 | ```
109 |
110 | Return the value.
111 |
112 | ---
113 |
114 |
115 |
116 | ### method `is_nothing`
117 |
118 | ```python
119 | is_nothing() → Literal[False]
120 | ```
121 |
122 |
123 |
124 |
125 |
126 | ---
127 |
128 |
129 |
130 | ### method `is_some`
131 |
132 | ```python
133 | is_some() → Literal[True]
134 | ```
135 |
136 |
137 |
138 |
139 |
140 | ---
141 |
142 |
143 |
144 | ### method `map`
145 |
146 | ```python
147 | map(op: 'Callable[[T], U]') → Some[U]
148 | ```
149 |
150 | There is a contained value, so return `Some` with original value mapped to a new value using the passed in function.
151 |
152 | ---
153 |
154 |
155 |
156 | ### method `map_or`
157 |
158 | ```python
159 | map_or(_default: 'object', op: 'Callable[[T], U]') → U
160 | ```
161 |
162 | There is a contained value, so return the original value mapped to a new value using the passed in function.
163 |
164 | ---
165 |
166 |
167 |
168 | ### method `map_or_else`
169 |
170 | ```python
171 | map_or_else(_default_op: 'object', op: 'Callable[[T], U]') → U
172 | ```
173 |
174 | There is a contained value, so return original value mapped to a new value using the passed in `op` function.
175 |
176 | ---
177 |
178 |
179 |
180 | ### method `ok_or`
181 |
182 | ```python
183 | ok_or(_error: 'E') → Ok[T]
184 | ```
185 |
186 | Return a `result.Ok` with the inner value.
187 |
188 | **NOTE**: This method is available only if the `result` package is installed.
189 |
190 | ---
191 |
192 |
193 |
194 | ### method `ok_or_else`
195 |
196 | ```python
197 | ok_or_else(_op: 'Callable[[], E]') → Ok[T]
198 | ```
199 |
200 | Return a `result.Ok` with the inner value.
201 |
202 | **NOTE**: This method is available only if the `result` package is installed.
203 |
204 | ---
205 |
206 |
207 |
208 | ### method `or_else`
209 |
210 | ```python
211 | or_else(_op: 'object') → Some[T]
212 | ```
213 |
214 | There is a contained value, so return `Some` with the original value
215 |
216 | ---
217 |
218 |
219 |
220 | ### method `some`
221 |
222 | ```python
223 | some() → T
224 | ```
225 |
226 | Return the value.
227 |
228 | ---
229 |
230 |
231 |
232 | ### method `unwrap`
233 |
234 | ```python
235 | unwrap() → T
236 | ```
237 |
238 | Return the value.
239 |
240 | ---
241 |
242 |
243 |
244 | ### method `unwrap_or`
245 |
246 | ```python
247 | unwrap_or(_default: 'U') → T
248 | ```
249 |
250 | Return the value.
251 |
252 | ---
253 |
254 |
255 |
256 | ### method `unwrap_or_else`
257 |
258 | ```python
259 | unwrap_or_else(op: 'object') → T
260 | ```
261 |
262 | Return the value.
263 |
264 | ---
265 |
266 |
267 |
268 | ### method `unwrap_or_raise`
269 |
270 | ```python
271 | unwrap_or_raise(e: 'object') → T
272 | ```
273 |
274 | Return the value.
275 |
276 |
277 | ---
278 |
279 |
280 |
281 | ## class `Nothing`
282 | An object that indicates no inner value is present
283 |
284 |
285 |
286 | ### method `__init__`
287 |
288 | ```python
289 | __init__() → None
290 | ```
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 | ---
300 |
301 |
302 |
303 | ### method `and_then`
304 |
305 | ```python
306 | and_then(_op: 'object') → Nothing
307 | ```
308 |
309 | There is no contained value, so return `Nothing`
310 |
311 | ---
312 |
313 |
314 |
315 | ### method `expect`
316 |
317 | ```python
318 | expect(message: 'str') → NoReturn
319 | ```
320 |
321 | Raises an `UnwrapError`.
322 |
323 | ---
324 |
325 |
326 |
327 | ### method `is_nothing`
328 |
329 | ```python
330 | is_nothing() → Literal[True]
331 | ```
332 |
333 |
334 |
335 |
336 |
337 | ---
338 |
339 |
340 |
341 | ### method `is_some`
342 |
343 | ```python
344 | is_some() → Literal[False]
345 | ```
346 |
347 |
348 |
349 |
350 |
351 | ---
352 |
353 |
354 |
355 | ### method `map`
356 |
357 | ```python
358 | map(_op: 'object') → Nothing
359 | ```
360 |
361 | Return `Nothing`
362 |
363 | ---
364 |
365 |
366 |
367 | ### method `map_or`
368 |
369 | ```python
370 | map_or(default: 'U', _op: 'object') → U
371 | ```
372 |
373 | Return the default value
374 |
375 | ---
376 |
377 |
378 |
379 | ### method `map_or_else`
380 |
381 | ```python
382 | map_or_else(default_op: 'Callable[[], U]', op: 'object') → U
383 | ```
384 |
385 | Return the result of the `default_op` function
386 |
387 | ---
388 |
389 |
390 |
391 | ### method `ok_or`
392 |
393 | ```python
394 | ok_or(error: 'E') → Err[E]
395 | ```
396 |
397 | There is no contained value, so return a `result.Err` with the given error value.
398 |
399 | **NOTE**: This method is available only if the `result` package is installed.
400 |
401 | ---
402 |
403 |
404 |
405 | ### method `ok_or_else`
406 |
407 | ```python
408 | ok_or_else(op: 'Callable[[], E]') → Err[E]
409 | ```
410 |
411 | There is no contained value, so return a `result.Err` with the result of `op`.
412 |
413 | **NOTE**: This method is available only if the `result` package is installed.
414 |
415 | ---
416 |
417 |
418 |
419 | ### method `or_else`
420 |
421 | ```python
422 | or_else(op: 'Callable[[], Maybe[T]]') → Maybe[T]
423 | ```
424 |
425 | There is no contained value, so return the result of `op`
426 |
427 | ---
428 |
429 |
430 |
431 | ### method `some`
432 |
433 | ```python
434 | some() → None
435 | ```
436 |
437 | Return `None`.
438 |
439 | ---
440 |
441 |
442 |
443 | ### method `unwrap`
444 |
445 | ```python
446 | unwrap() → NoReturn
447 | ```
448 |
449 | Raises an `UnwrapError`.
450 |
451 | ---
452 |
453 |
454 |
455 | ### method `unwrap_or`
456 |
457 | ```python
458 | unwrap_or(default: 'U') → U
459 | ```
460 |
461 | Return `default`.
462 |
463 | ---
464 |
465 |
466 |
467 | ### method `unwrap_or_else`
468 |
469 | ```python
470 | unwrap_or_else(op: 'Callable[[], T]') → T
471 | ```
472 |
473 | There is no contained value, so return a new value by calling `op`.
474 |
475 | ---
476 |
477 |
478 |
479 | ### method `unwrap_or_raise`
480 |
481 | ```python
482 | unwrap_or_raise(e: 'Type[TBE]') → NoReturn
483 | ```
484 |
485 | There is no contained value, so raise the exception with the value.
486 |
487 |
488 | ---
489 |
490 |
491 |
492 | ## class `UnwrapError`
493 | Exception raised from ``.unwrap_<...>`` and ``.expect_<...>`` calls.
494 |
495 | The original ``Maybe`` can be accessed via the ``.maybe`` attribute, but this is not intended for regular use, as type information is lost: ``UnwrapError`` doesn't know about ``T``, since it's raised from ``Some()`` or ``Nothing()`` which only knows about either ``T`` or no-value, not both.
496 |
497 |
498 |
499 | ### method `__init__`
500 |
501 | ```python
502 | __init__(maybe: 'Maybe[object]', message: 'str') → None
503 | ```
504 |
505 |
506 |
507 |
508 |
509 |
510 | ---
511 |
512 | #### property maybe
513 |
514 | Returns the original maybe.
515 |
516 |
517 |
518 |
519 |
520 |
521 | ---
522 |
523 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._
524 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.mypy]
6 | python_version = "3.11"
7 | files = ["src", "tests"]
8 | # Exclude files with pattern matching syntax until we drop support for Python
9 | # versions that don't support pattern matching. Trying to use with an older
10 | # Python version results in a "invalid syntax" error from mypy
11 | exclude = "tests/test_pattern_matching.py"
12 | check_untyped_defs = true
13 | disallow_incomplete_defs = true
14 | disallow_untyped_decorators = true
15 | disallow_any_generics = true
16 | disallow_subclassing_any = true
17 | disallow_untyped_calls = true
18 | disallow_untyped_defs = true
19 | ignore_missing_imports = true
20 | no_implicit_optional = true
21 | no_implicit_reexport = true
22 | pretty = true
23 | show_column_numbers = true
24 | show_error_codes = true
25 | show_error_context = true
26 | strict_equality = true
27 | strict_optional = true
28 | warn_redundant_casts = true
29 | warn_return_any = true
30 | warn_unused_configs = true
31 | warn_unused_ignores = true
32 |
33 | [tool.coverage.run]
34 | # Ignore "Couldn't parse Python file" warnings produced when attempting to parse
35 | # Python 3.10+ code using an earlier version of Python.
36 | disable_warnings = ["couldnt-parse"]
37 |
38 | [tool.pytest.ini_options]
39 | addopts = [
40 | "--tb=short",
41 | "--cov=src",
42 | "--cov-report=term-missing",
43 | "--cov-report=xml",
44 |
45 | # By default, ignore tests that only run on Python 3.10+
46 | "--ignore=tests/test_pattern_matching.py",
47 | ]
48 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | build
2 | flake8
3 | lazydocs
4 | mypy
5 | pytest
6 | pytest-asyncio
7 | pytest-cov
8 | pytest-mypy-plugins
9 | twine
10 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = rustedpy-maybe
3 | version = attr: maybe.__version__
4 | description = A Rust-like option type for Python
5 | long_description = file: README.md
6 | keywords = rust, option, maybe, enum
7 | author = francium
8 | author_email = francium@francium.cc
9 | maintainer = rustedpy github org members (https://github.com/rustedpy)
10 | url = https://github.com/rustedpy/maybe
11 | license = MIT
12 | license_file = LICENSE
13 | classifiers =
14 | Development Status :: 4 - Beta
15 | License :: OSI Approved :: MIT License
16 | Programming Language :: Python :: 3
17 | Programming Language :: Python :: 3.8
18 | Programming Language :: Python :: 3.9
19 | Programming Language :: Python :: 3.10
20 | Programming Language :: Python :: 3.11
21 | Programming Language :: Python :: 3.12
22 | Programming Language :: Python :: 3 :: Only
23 |
24 | [options]
25 | include_package_data = True
26 | install_requires =
27 | typing_extensions;python_version<'3.10'
28 | package_dir =
29 | =src
30 | packages = find:
31 | python_requires = >=3.8
32 | zip_safe = True
33 |
34 | [options.packages.find]
35 | where = src
36 |
37 | [options.package_data]
38 | maybe = py.typed
39 |
40 | [options.extras_require]
41 | result = result
42 |
43 | [flake8]
44 | # flake8 does not (yet?) support pyproject.toml; see
45 | # https://github.com/PyCQA/flake8/issues/234
46 | max-line-length = 99
47 | exclude =
48 | .direnv/
49 | .tox/
50 | .venv/
51 | venv/
52 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup()
4 |
--------------------------------------------------------------------------------
/src/maybe/__init__.py:
--------------------------------------------------------------------------------
1 | from .maybe import (
2 | Nothing,
3 | Some,
4 | SomeNothing,
5 | Maybe,
6 | UnwrapError,
7 | is_some,
8 | is_nothing,
9 | )
10 |
11 | __all__ = [
12 | "Nothing",
13 | "Some",
14 | "SomeNothing",
15 | "Maybe",
16 | "UnwrapError",
17 | "is_some",
18 | "is_nothing",
19 | ]
20 | __version__ = "0.0.0"
21 |
--------------------------------------------------------------------------------
/src/maybe/maybe.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from typing import (
5 | Any,
6 | Callable,
7 | Final,
8 | Generic,
9 | Literal,
10 | NoReturn,
11 | Type,
12 | TypeVar,
13 | Union,
14 | )
15 |
16 | if sys.version_info >= (3, 10): # pragma: no cover
17 | from typing import ParamSpec, TypeAlias, TypeGuard
18 | else: # pragma: no cover
19 | from typing_extensions import ParamSpec, TypeAlias, TypeGuard
20 |
21 |
22 | try:
23 | import result
24 |
25 | _RESULT_INSTALLED = True
26 | except ImportError: # pragma: no cover
27 | _RESULT_INSTALLED = False
28 |
29 |
30 | T = TypeVar("T", covariant=True) # Success type
31 | U = TypeVar("U")
32 | E = TypeVar("E")
33 | P = ParamSpec("P")
34 | R = TypeVar("R")
35 | TBE = TypeVar("TBE", bound=BaseException)
36 |
37 |
38 | class Some(Generic[T]):
39 | """
40 | An object that indicates some inner value is present
41 | """
42 |
43 | __match_args__ = ("some_value",)
44 | __slots__ = ("_value",)
45 |
46 | def __init__(self, value: T) -> None:
47 | self._value = value
48 |
49 | def __repr__(self) -> str:
50 | return f"Some({self._value!r})"
51 |
52 | def __eq__(self, other: Any) -> bool:
53 | return isinstance(other, Some) and self._value == other._value
54 |
55 | def __ne__(self, other: Any) -> bool:
56 | return not (self == other)
57 |
58 | def __hash__(self) -> int:
59 | return hash((True, self._value))
60 |
61 | def is_some(self) -> Literal[True]:
62 | return True
63 |
64 | def is_nothing(self) -> Literal[False]:
65 | return False
66 |
67 | def some(self) -> T:
68 | """
69 | Return the value.
70 | """
71 | return self._value
72 |
73 | @property
74 | def some_value(self) -> T:
75 | """
76 | Return the inner value.
77 | """
78 | return self._value
79 |
80 | def expect(self, _message: str) -> T:
81 | """
82 | Return the value.
83 | """
84 | return self._value
85 |
86 | def unwrap(self) -> T:
87 | """
88 | Return the value.
89 | """
90 | return self._value
91 |
92 | def unwrap_or(self, _default: U) -> T: # pyright: ignore[reportInvalidTypeVarUse]
93 | """
94 | Return the value.
95 | """
96 | return self._value
97 |
98 | def unwrap_or_else(self, op: object) -> T:
99 | """
100 | Return the value.
101 | """
102 | return self._value
103 |
104 | def unwrap_or_raise(self, e: object) -> T:
105 | """
106 | Return the value.
107 | """
108 | return self._value
109 |
110 | def map(self, op: Callable[[T], U]) -> Some[U]:
111 | """
112 | There is a contained value, so return `Some` with original value mapped
113 | to a new value using the passed in function.
114 | """
115 | return Some(op(self._value))
116 |
117 | def map_or(self, _default: object, op: Callable[[T], U]) -> U:
118 | """
119 | There is a contained value, so return the original value mapped to a
120 | new value using the passed in function.
121 | """
122 | return op(self._value)
123 |
124 | def map_or_else(self, _default_op: object, op: Callable[[T], U]) -> U:
125 | """
126 | There is a contained value, so return original value mapped to a new
127 | value using the passed in `op` function.
128 | """
129 | return op(self._value)
130 |
131 | def and_then(self, op: Callable[[T], Maybe[U]]) -> Maybe[U]:
132 | """
133 | There is a contained value, so return the maybe of `op` with the
134 | original value passed in
135 | """
136 | return op(self._value)
137 |
138 | def or_else(self, _op: object) -> Some[T]:
139 | """
140 | There is a contained value, so return `Some` with the original value
141 | """
142 | return self
143 |
144 | if _RESULT_INSTALLED:
145 |
146 | def ok_or(self, _error: E) -> result.Ok[T]: # pyright: ignore[reportInvalidTypeVarUse]
147 | """
148 | Return a `result.Ok` with the inner value.
149 |
150 | **NOTE**: This method is available only if the `result` package is
151 | installed.
152 | """
153 | return result.Ok(self._value)
154 |
155 | def ok_or_else(self, _op: Callable[[], E]) -> result.Ok[T]:
156 | """
157 | Return a `result.Ok` with the inner value.
158 |
159 | **NOTE**: This method is available only if the `result` package is
160 | installed.
161 | """
162 | return result.Ok(self._value)
163 |
164 |
165 | class Nothing:
166 | """
167 | An object that indicates no inner value is present
168 | """
169 |
170 | __match_args__ = ("nothing_value",)
171 | __slots__ = ()
172 |
173 | def __init__(self) -> None:
174 | pass
175 |
176 | def __repr__(self) -> str:
177 | return "Nothing()"
178 |
179 | def __eq__(self, other: Any) -> bool:
180 | return isinstance(other, Nothing)
181 |
182 | def __ne__(self, other: Any) -> bool:
183 | return not isinstance(other, Nothing)
184 |
185 | def __hash__(self) -> int:
186 | # A large random number is used here to avoid a hash collision with
187 | # something else since there is no real inner value for us to hash.
188 | return hash((False, 982006445019657274590041599673))
189 |
190 | def is_some(self) -> Literal[False]:
191 | return False
192 |
193 | def is_nothing(self) -> Literal[True]:
194 | return True
195 |
196 | def some(self) -> None:
197 | """
198 | Return `None`.
199 | """
200 | return None
201 |
202 | def expect(self, message: str) -> NoReturn:
203 | """
204 | Raises an `UnwrapError`.
205 | """
206 | exc = UnwrapError(
207 | self,
208 | f"{message}",
209 | )
210 | raise exc
211 |
212 | def unwrap(self) -> NoReturn:
213 | """
214 | Raises an `UnwrapError`.
215 | """
216 | exc = UnwrapError(
217 | self,
218 | "Called `Maybe.unwrap()` on a `Nothing` value",
219 | )
220 | raise exc
221 |
222 | def unwrap_or(self, default: U) -> U:
223 | """
224 | Return `default`.
225 | """
226 | return default
227 |
228 | def unwrap_or_else(self, op: Callable[[], T]) -> T:
229 | """
230 | There is no contained value, so return a new value by calling `op`.
231 | """
232 | return op()
233 |
234 | def unwrap_or_raise(self, e: Type[TBE]) -> NoReturn:
235 | """
236 | There is no contained value, so raise the exception with the value.
237 | """
238 | raise e()
239 |
240 | def map(self, _op: object) -> Nothing:
241 | """
242 | Return `Nothing`
243 | """
244 | return self
245 |
246 | def map_or(self, default: U, _op: object) -> U:
247 | """
248 | Return the default value
249 | """
250 | return default
251 |
252 | def map_or_else(self, default_op: Callable[[], U], op: object) -> U:
253 | """
254 | Return the result of the `default_op` function
255 | """
256 | return default_op()
257 |
258 | def and_then(self, _op: object) -> Nothing:
259 | """
260 | There is no contained value, so return `Nothing`
261 | """
262 | return self
263 |
264 | def or_else(self, op: Callable[[], Maybe[T]]) -> Maybe[T]:
265 | """
266 | There is no contained value, so return the result of `op`
267 | """
268 | return op()
269 |
270 | if _RESULT_INSTALLED:
271 |
272 | def ok_or(self, error: E) -> result.Err[E]:
273 | """
274 | There is no contained value, so return a `result.Err` with the given
275 | error value.
276 |
277 | **NOTE**: This method is available only if the `result` package is
278 | installed.
279 | """
280 | return result.Err(error)
281 |
282 | def ok_or_else(self, op: Callable[[], E]) -> result.Err[E]:
283 | """
284 | There is no contained value, so return a `result.Err` with the
285 | result of `op`.
286 |
287 | **NOTE**: This method is available only if the `result` package is
288 | installed.
289 | """
290 | return result.Err(op())
291 |
292 |
293 | # Define Maybe as a generic type alias for use in type annotations
294 | Maybe: TypeAlias = Union[Some[T], Nothing]
295 | """
296 | A simple `Maybe` type inspired by Rust.
297 | Not all methods (https://doc.rust-lang.org/std/option/enum.Option.html)
298 | have been implemented, only the ones that make sense in the Python context.
299 | """
300 |
301 | SomeNothing: Final = (Some, Nothing)
302 | """
303 | A type to use in `isinstance` checks. This is purely for convenience sake, as you could
304 | also just write `isinstance(res, (Some, Nothing))
305 | """
306 |
307 |
308 | class UnwrapError(Exception):
309 | """
310 | Exception raised from ``.unwrap_<...>`` and ``.expect_<...>`` calls.
311 |
312 | The original ``Maybe`` can be accessed via the ``.maybe`` attribute, but
313 | this is not intended for regular use, as type information is lost:
314 | ``UnwrapError`` doesn't know about ``T``, since it's raised from ``Some()``
315 | or ``Nothing()`` which only knows about either ``T`` or no-value, not both.
316 | """
317 |
318 | _maybe: Maybe[object]
319 |
320 | def __init__(self, maybe: Maybe[object], message: str) -> None:
321 | self._maybe = maybe
322 | super().__init__(message)
323 |
324 | @property
325 | def maybe(self) -> Maybe[Any]:
326 | """
327 | Returns the original maybe.
328 | """
329 | return self._maybe
330 |
331 |
332 | def is_some(maybe: Maybe[T]) -> TypeGuard[Some[T]]:
333 | """A typeguard to check if a maybe is a `Some`.
334 |
335 | Usage:
336 |
337 | ```plain
338 | >>> r: Maybe[int, str] = get_a_maybe()
339 | >>> if is_some(r):
340 | ... r # r is of type Some[int]
341 | ... elif is_nothing(r):
342 | ... r # r is of type Nothing[str]
343 | ```
344 | """
345 | return maybe.is_some()
346 |
347 |
348 | def is_nothing(maybe: Maybe[T]) -> TypeGuard[Nothing]:
349 | """A typeguard to check if a maybe is a `Nothing`.
350 |
351 | Usage:
352 |
353 | ```plain
354 | >>> r: Maybe[int, str] = get_a_maybe()
355 | >>> if is_some(r):
356 | ... r # r is of type Some[int]
357 | ... elif is_nothing(r):
358 | ... r # r is of type Nothing[str]
359 | ```
360 | """
361 | return maybe.is_nothing()
362 |
--------------------------------------------------------------------------------
/src/maybe/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustedpy/maybe/93926b43929b34c2dc143c738a77b2f1e4e8661f/src/maybe/py.typed
--------------------------------------------------------------------------------
/tests/test_maybe.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Callable
4 |
5 | import pytest
6 | import result
7 |
8 | from maybe import Some, SomeNothing, Maybe, Nothing, UnwrapError, is_nothing, is_some
9 |
10 |
11 | def test_some_factories() -> None:
12 | instance = Some(1)
13 | assert instance._value == 1
14 | assert instance.is_some() is True
15 |
16 |
17 | def test_nothing_factory() -> None:
18 | instance = Nothing()
19 | assert instance.is_nothing() is True
20 |
21 |
22 | def test_eq() -> None:
23 | assert Some(1) == Some(1)
24 | assert Nothing() == Nothing()
25 | assert not (Nothing() != Nothing())
26 | assert Some(1) != Nothing()
27 | assert Some(1) != Some(2)
28 | assert not (Some(1) != Some(1))
29 | assert Some(1) != "abc"
30 | assert Some("0") != Some(0)
31 |
32 |
33 | def test_hash() -> None:
34 | assert len({Some(1), Nothing(), Some(1), Nothing()}) == 2
35 | assert len({Some(1), Some(2)}) == 2
36 | assert len({Some("a"), Nothing()}) == 2
37 |
38 |
39 | def test_repr() -> None:
40 | """
41 | ``repr()`` returns valid code if the wrapped value's ``repr()`` does as well.
42 | """
43 | o = Some(123)
44 | n = Nothing()
45 |
46 | assert repr(o) == "Some(123)"
47 | assert o == eval(repr(o))
48 |
49 | assert repr(n) == "Nothing()"
50 | assert n == eval(repr(n))
51 |
52 |
53 | def test_some_value() -> None:
54 | res = Some('haha')
55 | assert res.some_value == 'haha'
56 |
57 |
58 | def test_some() -> None:
59 | res = Some('haha')
60 | assert res.is_some() is True
61 | assert res.is_nothing() is False
62 | assert res.some_value == 'haha'
63 |
64 |
65 | def test_some_guard() -> None:
66 | assert is_some(Some(1))
67 |
68 |
69 | def test_nothing_guard() -> None:
70 | assert is_nothing(Nothing())
71 |
72 |
73 | def test_nothing() -> None:
74 | res = Nothing()
75 | assert res.is_some() is False
76 | assert res.is_nothing() is True
77 |
78 |
79 | def test_some_method() -> None:
80 | o = Some('yay')
81 | n = Nothing()
82 | assert o.some() == 'yay'
83 |
84 | # Unfortunately, it seems the mypy team made a very deliberate and highly contested
85 | # decision to mark using the return value from a function known to only return None
86 | # as an error, so we are forced to ignore the check here.
87 | # See https://github.com/python/mypy/issues/6549
88 | assert n.some() is None # type: ignore[func-returns-value]
89 |
90 |
91 | def test_expect() -> None:
92 | o = Some('yay')
93 | n = Nothing()
94 | assert o.expect('failure') == 'yay'
95 | with pytest.raises(UnwrapError):
96 | n.expect('failure')
97 |
98 |
99 | def test_unwrap() -> None:
100 | o = Some('yay')
101 | n = Nothing()
102 | assert o.unwrap() == 'yay'
103 | with pytest.raises(UnwrapError):
104 | n.unwrap()
105 |
106 |
107 | def test_unwrap_or() -> None:
108 | o = Some('yay')
109 | n = Nothing()
110 | assert o.unwrap_or('some_default') == 'yay'
111 | assert n.unwrap_or('another_default') == 'another_default'
112 |
113 |
114 | def test_unwrap_or_else() -> None:
115 | o = Some('yay')
116 | n = Nothing()
117 | assert o.unwrap_or_else(str.upper) == 'yay'
118 | assert n.unwrap_or_else(lambda: 'default') == 'default'
119 |
120 |
121 | def test_unwrap_or_raise() -> None:
122 | o = Some('yay')
123 | n = Nothing()
124 | assert o.unwrap_or_raise(ValueError) == 'yay'
125 | with pytest.raises(ValueError) as exc_info:
126 | n.unwrap_or_raise(ValueError)
127 | assert exc_info.value.args == ()
128 |
129 |
130 | def test_map() -> None:
131 | o = Some('yay')
132 | n = Nothing()
133 | assert o.map(str.upper).some() == 'YAY'
134 | assert n.map(str.upper).is_nothing()
135 |
136 |
137 | def test_map_or() -> None:
138 | o = Some('yay')
139 | n = Nothing()
140 | assert o.map_or('hay', str.upper) == 'YAY'
141 | assert n.map_or('hay', str.upper) == 'hay'
142 |
143 |
144 | def test_map_or_else() -> None:
145 | o = Some('yay')
146 | n = Nothing()
147 | assert o.map_or_else(lambda: 'hay', str.upper) == 'YAY'
148 | assert n.map_or_else(lambda: 'hay', str.upper) == 'hay'
149 |
150 |
151 | def test_and_then() -> None:
152 | assert Some(2).and_then(sq).and_then(sq).some() == 16
153 | assert Some(2).and_then(sq).and_then(to_nothing).is_nothing()
154 | assert Some(2).and_then(to_nothing).and_then(sq).is_nothing()
155 | assert Nothing().and_then(sq).and_then(sq).is_nothing()
156 |
157 | assert Some(2).and_then(sq_lambda).and_then(sq_lambda).some() == 16
158 | assert Some(2).and_then(sq_lambda).and_then(to_nothing_lambda).is_nothing()
159 | assert Some(2).and_then(to_nothing_lambda).and_then(sq_lambda).is_nothing()
160 | assert Nothing().and_then(sq_lambda).and_then(sq_lambda).is_nothing()
161 |
162 |
163 | def test_or_else() -> None:
164 | assert Some(2).or_else(sq).or_else(sq).some() == 2
165 | assert Some(2).or_else(to_nothing).or_else(sq).some() == 2
166 | assert Nothing().or_else(lambda: sq(3)).or_else(lambda: to_nothing(2)).some() == 9
167 | assert (
168 | Nothing()
169 | .or_else(lambda: to_nothing(2))
170 | .or_else(lambda: to_nothing(2))
171 | .is_nothing()
172 | )
173 |
174 | assert Some(2).or_else(sq_lambda).or_else(sq).some() == 2
175 | assert Some(2).or_else(to_nothing_lambda).or_else(sq_lambda).some() == 2
176 |
177 |
178 | def test_isinstance_result_type() -> None:
179 | o = Some('yay')
180 | n = Nothing()
181 | assert isinstance(o, SomeNothing)
182 | assert isinstance(n, SomeNothing)
183 | assert not isinstance(1, SomeNothing)
184 |
185 |
186 | def test_error_context() -> None:
187 | n = Nothing()
188 | with pytest.raises(UnwrapError) as exc_info:
189 | n.unwrap()
190 | exc = exc_info.value
191 | assert exc.maybe is n
192 |
193 |
194 | def test_slots() -> None:
195 | """
196 | Some and Nothing have slots, so assigning arbitrary attributes fails.
197 | """
198 | o = Some('yay')
199 | n = Nothing()
200 | with pytest.raises(AttributeError):
201 | o.some_arbitrary_attribute = 1 # type: ignore[attr-defined]
202 | with pytest.raises(AttributeError):
203 | n.some_arbitrary_attribute = 1 # type: ignore[attr-defined]
204 |
205 |
206 | def test_some_ok_or() -> None:
207 | assert Some(1).ok_or('error') == result.Ok(1)
208 |
209 |
210 | def test_some_ok_or_else() -> None:
211 | assert Some(1).ok_or_else(lambda: 'error') == result.Ok(1)
212 |
213 |
214 | def test_nothing_ok_or() -> None:
215 | assert Nothing().ok_or('error') == result.Err('error')
216 |
217 |
218 | def test_nothing_ok_or_else() -> None:
219 | assert Nothing().ok_or_else(lambda: 'error') == result.Err('error')
220 |
221 |
222 | def sq(i: int) -> Maybe[int]:
223 | return Some(i**2)
224 |
225 |
226 | def to_nothing(_: int) -> Maybe[int]:
227 | return Nothing()
228 |
229 |
230 | # Lambda versions of the same functions, just for test/type coverage
231 | sq_lambda: Callable[[int], Maybe[int]] = lambda i: Some(i * i)
232 | to_nothing_lambda: Callable[[int], Maybe[int]] = lambda _: Nothing()
233 |
--------------------------------------------------------------------------------
/tests/test_pattern_matching.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from maybe import Nothing, Some, Maybe
4 |
5 |
6 | def test_pattern_matching_on_some_type() -> None:
7 | """
8 | Pattern matching on ``Some()`` matches the contained value.
9 | """
10 | o: Maybe[str] = Some("yay")
11 | match o:
12 | case Some(value):
13 | reached = True
14 |
15 | assert value == "yay"
16 | assert reached
17 |
18 |
19 | def test_pattern_matching_on_err_type() -> None:
20 | """
21 | Pattern matching on ``Err()`` matches the contained value.
22 | """
23 | n: Maybe[int] = Nothing()
24 | match n:
25 | case Nothing():
26 | reached = True
27 |
28 | assert reached
29 |
--------------------------------------------------------------------------------
/tests/type-checking/test_maybe.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # reveal_type(res3) # N: Revealed type is "maybe.maybe.Nothing"
3 | - case: failure_lash
4 | disable_cache: false
5 | main: |
6 | from typing import Callable, List, Optional
7 |
8 | from maybe import Maybe, Some, Nothing
9 |
10 |
11 | res1: Maybe[str] = Some('hello')
12 | reveal_type(res1) # N: Revealed type is "Union[maybe.maybe.Some[builtins.str], maybe.maybe.Nothing]"
13 | if isinstance(res1, Some):
14 | some: Some[str] = res1
15 | reveal_type(some) # N: Revealed type is "maybe.maybe.Some[builtins.str]"
16 | someValue: str = res1.some()
17 | reveal_type(someValue) # N: Revealed type is "builtins.str"
18 | mapped_to_float: float = res1.map_or(1.0, lambda s: len(s) * 1.5)
19 | reveal_type(mapped_to_float) # N: Revealed type is "builtins.float"
20 | else:
21 | nothing: Nothing = res1
22 | reveal_type(nothing) # N: Revealed type is "maybe.maybe.Nothing"
23 |
24 | # Test constructor functions
25 | res2 = Some(42)
26 | reveal_type(res2) # N: Revealed type is "maybe.maybe.Some[builtins.int]"
27 | res3 = Nothing()
28 | reveal_type(res3) # N: Revealed type is "maybe.maybe.Nothing"
29 |
30 | res4 = Some(4)
31 | add1: Callable[[int], Maybe[int]] = lambda i: Some(i + 1)
32 | toint: Callable[[str], Maybe[int]] = lambda i: Some(int(i))
33 | res5 = res4.and_then(add1)
34 | reveal_type(res5) # N: Revealed type is "Union[maybe.maybe.Some[builtins.int], maybe.maybe.Nothing]"
35 | res6 = res4.or_else(toint)
36 | reveal_type(res6) # N: Revealed type is "maybe.maybe.Some[builtins.int]"
37 |
38 | - case: covariance_pre310
39 | skip: "sys.version_info >= (3, 10)"
40 | disable_cache: false
41 | main: |
42 | from maybe import Maybe, Some, Nothing
43 |
44 | some_int: Some[int] = Some(42)
45 | some_float: Some[float] = some_int
46 | some_int = some_float # E: Incompatible types in assignment (expression has type "Some[float]", variable has type "Some[int]") [assignment]
47 |
48 | nothing: Nothing = Nothing()
49 |
50 | maybe_int: Maybe[int] = some_int or nothing
51 | maybe_float: Maybe[float] = maybe_int
52 | maybe_int = maybe_float # E: Incompatible types in assignment (expression has type "Union[Some[float], Nothing]", variable has type "Union[Some[int], Nothing]") [assignment]
53 |
54 | - case: covariance
55 | skip: "sys.version_info < (3, 10)"
56 | disable_cache: false
57 | main: |
58 | import sys
59 | from maybe import Maybe, Some, Nothing
60 |
61 | some_int: Some[int] = Some(42)
62 | some_float: Some[float] = some_int
63 | some_int = some_float # E: Incompatible types in assignment (expression has type "Some[float]", variable has type "Some[int]") [assignment]
64 |
65 | nothing: Nothing = Nothing()
66 |
67 | maybe_int: Maybe[int] = some_int or nothing
68 | maybe_float: Maybe[float] = maybe_int
69 | maybe_int = maybe_float # E: Incompatible types in assignment (expression has type "Some[float] | Nothing", variable has type "Some[int] | Nothing") [assignment]
70 |
71 | - case: map_ok
72 | disable_cache: false
73 | main: |
74 | from maybe import Maybe, Some, Nothing
75 |
76 | s = Some("42")
77 | reveal_type(s.map(int)) # N: Revealed type is "maybe.maybe.Some[builtins.int]"
78 |
79 | n = Nothing()
80 | reveal_type(n.map(int)) # N: Revealed type is "maybe.maybe.Nothing"
81 |
82 | - case: map_maybe
83 | disable_cache: false
84 | main: |
85 | from maybe import Maybe, Some
86 |
87 | greeting: Maybe[str] = Some("Hello")
88 |
89 | personalized_greeting = greeting.map(lambda g: f"{g}, John")
90 | reveal_type(personalized_greeting) # N: Revealed type is "Union[maybe.maybe.Some[builtins.str], maybe.maybe.Nothing]"
91 |
92 | some = personalized_greeting.some()
93 | reveal_type(some) # N: Revealed type is "Union[builtins.str, None]"
94 |
95 | - case: ok_or
96 | disable_cache: false
97 | main: |
98 | from maybe import Maybe, Some, Nothing
99 | from result import Ok, Err
100 |
101 | greeting: Maybe[str] = Some("Hello")
102 |
103 | ok_greeting = greeting.ok_or("error")
104 | reveal_type(ok_greeting) # N: Revealed type is "Union[result.result.Ok[builtins.str], result.result.Err[builtins.str]]"
105 |
106 | nothing: Maybe[str] = Nothing()
107 |
108 | no_greeting = nothing.ok_or("error")
109 | reveal_type(no_greeting) # N: Revealed type is "Union[result.result.Ok[builtins.str], result.result.Err[builtins.str]]"
110 |
111 | - case: ok_or_else
112 | disable_cache: false
113 | main: |
114 | from maybe import Maybe, Some, Nothing
115 | from result import Ok, Err, Result
116 |
117 | greeting: Maybe[str] = Some("Hello")
118 |
119 | ok_greeting = greeting.ok_or_else(lambda: "error")
120 | reveal_type(ok_greeting) # N: Revealed type is "Union[result.result.Ok[builtins.str], result.result.Err[builtins.str]]"
121 | greeting.ok_or_else("error") # E: Argument 1 to "ok_or_else" of "Some" has incompatible type "str"; expected "Callable[[], Never]" [arg-type] # E: Argument 1 to "ok_or_else" of "Nothing" has incompatible type "str"; expected "Callable[[], Never]" [arg-type]
122 |
123 | nothing: Maybe[str] = Nothing()
124 |
125 | no_greeting: Result[str, ValueError] = nothing.ok_or_else(lambda: ValueError("error"))
126 | reveal_type(no_greeting) # N: Revealed type is "Union[result.result.Ok[builtins.str], result.result.Err[builtins.ValueError]]"
127 | nothing.ok_or_else("error") # E: Argument 1 to "ok_or_else" of "Some" has incompatible type "str"; expected "Callable[[], Never]" [arg-type] # E: Argument 1 to "ok_or_else" of "Nothing" has incompatible type "str"; expected "Callable[[], Never]" [arg-type]
128 |
129 | - case: typeguard
130 | disable_cache: false
131 | main: |
132 | from maybe import Maybe, Some, Nothing, is_some, is_nothing
133 |
134 | maybe = Some(1)
135 | nothing = Nothing()
136 | if is_some(maybe):
137 | reveal_type(maybe) # N: Revealed type is "maybe.maybe.Some[builtins.int]"
138 | elif is_nothing(nothing):
139 | reveal_type(nothing) # N: Revealed type is "maybe.maybe.Nothing"
140 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | ; Version 4 rewrite fixed https://github.com/tox-dev/tox/issues/1297, which was
3 | ; causing `usedevelop = true` to be ignored.
4 | min_version = 4.0
5 | envlist = py312,py311,py310,py39,py38
6 |
7 | [testenv]
8 | ; Required for test coverage to work correctly
9 | usedevelop = true
10 | deps = -rrequirements-dev.txt
11 | commands = pytest {posargs}
12 |
13 | [testenv:py310]
14 | deps = -rrequirements-dev.txt
15 | commands =
16 | pytest {posargs}
17 | ; Reset coverage options since we don't need to report coverage
18 | ; for testing pattern matching, which erroneously shows misses for
19 | ; code covered by the preceding command.
20 | pytest {posargs} --cov-reset tests/test_pattern_matching.py
21 |
--------------------------------------------------------------------------------