├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── MIGRATING.md ├── Makefile ├── README.md ├── RELEASING.md ├── codecov.yml ├── docs ├── .pages ├── README.md └── result.md ├── pyproject.toml ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── src └── result │ ├── __init__.py │ ├── py.typed │ └── result.py ├── tests ├── test_pattern_matching.py ├── test_result.py ├── test_result_do.py └── type_checking │ └── test_result.yml └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | name: CI 9 | 10 | jobs: 11 | 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python: 17 | - '3.12' 18 | - '3.11' 19 | - '3.10' 20 | - '3.9' 21 | - '3.8' 22 | name: Python ${{ matrix.python }} 23 | steps: 24 | # Python 25 | - name: Setup python ${{ matrix.python }} 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python }} 29 | 30 | # Check out code 31 | - uses: actions/checkout@v2 32 | 33 | # Cached dependencies 34 | - uses: actions/cache@v1 35 | with: 36 | path: ~/.cache/pip 37 | key: ${{ runner.os }}-pip-py${{ matrix.python }}-${{ hashFiles('**/requirements-dev.txt') }} 38 | restore-keys: | 39 | ${{ runner.os }}-pip-py${{ matrix.python }}- 40 | - name: Install dev dependencies 41 | # There's no virtual env in CI environment, just bypass it with a dummy 42 | # env var value 43 | run: VIRTUAL_ENV=none make install 44 | 45 | # Tests 46 | - name: Run tests 47 | run: pytest --ignore=tests/test_pattern_matching.py --ignore=tests/type_checking/test_result.yml 48 | - name: Run tests (type checking) 49 | if: matrix.python != '3.8' && matrix.python != '3.9' 50 | # These started breaking for <= 3.9, due to the type checker using a 51 | # '|' for unions rather than 'Union[...]', so it's not possible to run 52 | # the tests without maintaining two duplicate files (one for <= 3.9 and 53 | # one for > 3.9) 54 | run: pytest tests/type_checking/test_result.yml 55 | - name: Run tests (pattern matching) 56 | if: matrix.python == '3.10' || matrix.python == '3.11' || matrix.python == '3.12' 57 | run: pytest tests/test_pattern_matching.py 58 | 59 | # Linters 60 | - name: Run flake8 (Python >= 3.10) 61 | run: make lint-flake 62 | if: matrix.python != '3.9' && matrix.python != '3.8' 63 | - name: Run flake8 (Python < 3.10) 64 | run: make lint-flake-pre310 65 | if: matrix.python == '3.9' || matrix.python == '3.8' 66 | - name: Run mypy 67 | run: make lint-mypy 68 | 69 | # Packaging 70 | - name: Build packages 71 | run: | 72 | pip install --upgrade build pip setuptools wheel 73 | python -m build 74 | 75 | # Coverage 76 | - name: Upload coverage to codecov.io 77 | uses: codecov/codecov-action@v1 78 | if: matrix.python == '3.9' 79 | with: 80 | token: ${{ secrets.CODECOV_TOKEN }} 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .coverage 3 | coverage.xml 4 | *.swp 5 | *.pyc 6 | __pycache__ 7 | dist/ 8 | *.egg-info/ 9 | build/ 10 | .idea/ 11 | .mypy_cache/ 12 | venv/ 13 | /.tox/ 14 | .vscode 15 | pyrightconfig.json 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This project follows semantic versioning. 4 | 5 | Possible log types: 6 | 7 | - `[added]` for new features. 8 | - `[changed]` for changes in existing functionality. 9 | - `[deprecated]` for once-stable features removed in upcoming releases. 10 | - `[removed]` for deprecated features removed in this release. 11 | - `[fixed]` for any bug fixes. 12 | - `[security]` to invite users to upgrade in case of vulnerabilities. 13 | 14 | ## [Unreleased] 15 | 16 | - `[changed]` Improve type narrowing for `is_ok` and `is_err` type guards by 17 | replacing `typing.TypeGuard` with `typing.TypeIs` (#193) 18 | 19 | ## [0.17.0] - 2024-06-02 20 | 21 | - `[added]` Add `inspect()` and `inspect_err()` methods (#185) 22 | 23 | ## [0.16.1] - 2024-02-29 24 | 25 | - `[fixed]` PyPI not showing description (#176) 26 | 27 | ## [0.16.0] - 2023-12-23 28 | 29 | - `[added]` Add `map_async` for async functions (#165) 30 | - `[fixed]` Add `do_async()` to handle edge case in `do()` involving multiple inlined awaits (#149) 31 | - `[added]` Add support for Python 3.12 (#157) 32 | 33 | ## [0.15.0] - 2023-12-04 34 | 35 | - `[added]` Add `do` function to support Haskell-style do-notation (#149) 36 | 37 | ## [0.14.0] - 2023-11-10 38 | 39 | - `[added]` `is_ok` and `is_err` type guard functions as alternatives to `isinstance` checks (#69) 40 | - `[added]` Add `and_then_async` for async functions (#148) 41 | 42 | ## [0.13.1] - 2023-07-19 43 | 44 | - `[fixed]` Use `self._value` instead of deprecated `self.value` in `Err.expect` and `Err.unwrap` to avoid raising a warning (#133) 45 | 46 | ## [0.13.0] - 2023-07-15 47 | 48 | - `[changed]` Include captured `Err` value when `expect` and `unwrap` are called and an `UnwrapError` is raised (#98, #132) 49 | 50 | ## [0.12.0] - 2023-06-11 51 | 52 | - `[removed]` Drop support for Python 3.7 (#126) 53 | - `[fixed]` Pattern matching deprecation warning (#128) 54 | - `[changed]` Minor internal implementation details (#129, #130) 55 | 56 | ## [0.11.0] - 2023-06-11 57 | 58 | - `[changed]` `Ok` now requires an explicit value during instantiation. Please 59 | check out [MIGRATING.md], it will guide you through the necessary change in 60 | your codebase. 61 | - `[deprecated]` `value` property to access the inner value (#37, #121) 62 | - `[added]` `ok_value` and `err_value` to access the inner value more safely (#37, #121) 63 | 64 | ## [0.10.0] - 2023-04-29 65 | 66 | - `[fixed]` Make python version check PEP 484 compliant (#118) 67 | - `[added]` `as_async_result` decorator to turn regular async functions into 68 | `Result` returning ones (#116) 69 | 70 | ## [0.9.0] - 2022-12-09 71 | 72 | - `[added]` Implement `unwrap_or_raise` (#95) 73 | - `[added]` Add support for Python 3.11 (#107) 74 | - `[changed]` Narrowing of return types on methods of `Err` and `Ok`. (#106) 75 | - `[fixed]` Fix failing type inference for `Result.map` and similar method 76 | unions (#106) 77 | 78 | ## [0.8.0] - 2022-04-17 79 | 80 | - `[added]` `as_result` decorator to turn regular functions into 81 | `Result` returning ones (#33, 71) 82 | - `[removed]` Drop support for Python 3.6 (#49) 83 | - `[added]` Implement `unwrap_or_else` (#74), `and_then` (#90) and `or_else` (#90) 84 | 85 | ## [0.7.0] - 2021-11-19 86 | 87 | - `[removed]` Drop support for Python 3.5 (#34) 88 | - `[added]` Add support for Python 3.9 and 3.10 (#50) 89 | - `[changed]` Make the `Ok` type covariant in regard to its wrapped type `T`. 90 | Likewise for `Err` in regard to `E`. This should result in more intuitive 91 | type checking behaviour. For instance, `Err[TypeError]` will get recognized 92 | as a subtype of `Err[Exception]` by type checkers. See [PEP 438] for a 93 | detailed explanation of covariance and its implications. 94 | - `[added]` Add support for Python 3.10 pattern matching (#47) 95 | - `[changed]` `Ok` and `Err` now define `__slots__` to save memory (#55, #58) 96 | - `[changed]` The generic type of `UnwrapError.result` now explicitly specifies `Any` (#67) 97 | 98 | [PEP 438]: https://www.python.org/dev/peps/pep-0483/#covariance-and-contravariance 99 | 100 | ## [0.6.0] - 2021-03-17 101 | 102 | **IMPORTANT:** This release a big API refactoring to make the API more type 103 | safe. Unfortunately this means some breaking changes. Please check out 104 | [MIGRATING.md], it will guide you through the necessary changes in your 105 | codebase. 106 | 107 | 108 | - [changed] Split result type into `Ok` and `Err` classes (#17, #27) 109 | - [deprecated] Python 3.4 support is deprecated and will be removed in the next 110 | release 111 | 112 | ## [0.5.0] - 2020-03-03 113 | 114 | - [added] Implement `map`, `map_err`, `map_or` and `map_or_else` (#19) 115 | - [added] Add `unwrap_err` and `expect_err` methods (#26) 116 | - [changed] Type annotations: Change parameter order 117 | from `Result[E, T]` to `Result[T, E]` to match Rust/OCaml/F# (#7) 118 | 119 | ## [0.4.1] - 2020-02-17 120 | 121 | - [added] Add `py.typed` for PEP561 package compliance (#16) 122 | 123 | ## [0.4.0] - 2019-04-17 124 | 125 | - [added] Add `unwrap`, `unwrap_or` and `expect` (#9) 126 | - [removed] Drop support for Python 2 and 3.3 127 | - [changed] Only install typing dependency for Python <3.5 128 | 129 | ## [0.3.0] - 2017-07-12 130 | 131 | - [added] This library is now fully type annotated (#4, thanks @tyehle) 132 | - [added] Implementations for `__ne__`, `__hash__` and `__repr__` 133 | - [deprecated] Python 2 support is deprecated and will be removed in the 0.4 release 134 | 135 | ## [0.2.2] - 2016-09-21 136 | 137 | - [added] `__eq__` magic method 138 | 139 | ## [0.2.0] - 2016-05-05 140 | 141 | - [added] Convenience default: `Ok()` == `Ok(True)` 142 | 143 | ## [0.1.1] - 2015-12-14 144 | 145 | - [fixed] Import bugfix 146 | 147 | ## [0.1.0] - 2015-12-14 148 | 149 | - Initial version 150 | 151 | [MIGRATING.md]: https://github.com/rustedpy/result/blob/main/MIGRATING.md 152 | [Unreleased]: https://github.com/rustedpy/result/compare/v0.17.0...HEAD 153 | [0.17.0]: https://github.com/rustedpy/result/compare/v0.16.1...v0.17.0 154 | [0.16.1]: https://github.com/rustedpy/result/compare/v0.16.0...v0.16.1 155 | [0.16.0]: https://github.com/rustedpy/result/compare/v0.15.0...v0.16.0 156 | [0.15.0]: https://github.com/rustedpy/result/compare/v0.14.0...v0.15.0 157 | [0.14.0]: https://github.com/rustedpy/result/compare/v0.13.1...v0.14.0 158 | [0.13.1]: https://github.com/rustedpy/result/compare/v0.13.0...v0.13.1 159 | [0.13.0]: https://github.com/rustedpy/result/compare/v0.12.0...v0.13.0 160 | [0.12.0]: https://github.com/rustedpy/result/compare/v0.11.0...v0.12.0 161 | [0.11.0]: https://github.com/rustedpy/result/compare/v0.10.0...v0.11.0 162 | [0.10.0]: https://github.com/rustedpy/result/compare/v0.9.0...v0.10.0 163 | [0.9.0]: https://github.com/rustedpy/result/compare/v0.8.0...v0.9.0 164 | [0.8.0]: https://github.com/rustedpy/result/compare/v0.7.0...v0.8.0 165 | [0.7.0]: https://github.com/rustedpy/result/compare/v0.6.0...v0.7.0 166 | [0.6.0]: https://github.com/rustedpy/result/compare/v0.5.0...v0.6.0 167 | [0.5.0]: https://github.com/rustedpy/result/compare/v0.4.1...v0.5.0 168 | [0.4.1]: https://github.com/rustedpy/result/compare/v0.4.0...v0.4.1 169 | [0.4.0]: https://github.com/rustedpy/result/compare/v0.3.0...v0.4.0 170 | [0.3.0]: https://github.com/rustedpy/result/compare/v0.2.2...v0.3.0 171 | [0.2.2]: https://github.com/rustedpy/result/compare/v0.2.1...v0.2.2 172 | [0.2.1]: https://github.com/rustedpy/result/compare/v0.2.0...v0.2.1 173 | [0.2.0]: https://github.com/rustedpy/result/compare/v0.1.1...v0.2.0 174 | [0.1.1]: https://github.com/rustedpy/result/compare/v0.1.0...v0.1.1 175 | [0.1.0]: https://github.com/rustedpy/result/compare/3ca7d83...v0.1.0 176 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2020 Danilo Bargen 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/result/py.typed 2 | -------------------------------------------------------------------------------- /MIGRATING.md: -------------------------------------------------------------------------------- 1 | # Migration guides 2 | 3 | ## 0.11.0 -> 0.12 migration 4 | 5 | ``.value`` is now deprecated. New code should use ``.ok_value`` on instances of 6 | ``Ok`` and ``.err_value`` on instances of ``Err``. Existing code using 7 | ``.value`` will continue to work, but will result in a deprecation warning being 8 | logged. Users of this library are encouraged to migrate away from ``.value`` 9 | before it is removed in a future version. 10 | 11 | ## 0.10 -> 0.11 migration 12 | 13 | The 0.11 migration includes one breaking change: 14 | 15 | `Ok` now requires an explicit value during instantiation. Previously, if no 16 | value was provides, a default value of `True` was implicitly used. 17 | 18 | To retain this behavior you can change any no-argument instantiations with: 19 | 20 | ```diff 21 | - r = Ok() 22 | + r = Ok(True) 23 | ``` 24 | 25 | ## 0.5 -> 0.6 migration 26 | 27 | The 0.6 migration includes two breaking changes and some useful new functionality: 28 | 29 | 1\. The `Result.Ok()` and `Result.Err()` class methods have been removed. 30 | These should be replaced by direct use of `Ok()` and `Err()`. As an example, the following code: 31 | 32 | ```python 33 | from result import Result 34 | res1 = Result.Ok('yay') 35 | res2 = Result.Err('nay') 36 | ``` 37 | 38 | should be replaced by: 39 | 40 | ```python 41 | from result import Ok, Err 42 | res1 = Ok('yay') 43 | res2 = Err('nay') 44 | ``` 45 | 46 | 2\. Result is now a Union type between `Ok[T]` and `Err[E]`. As such, you cannot use `isinstance(res, Result)` anymore. 47 | These should be replaced by `isinstance(res, OkErr)`. As an example, the following code: 48 | 49 | ```python 50 | from result import Ok, Result 51 | res = Ok('yay') 52 | if isinstance(res, Result): 53 | print("Result type!") 54 | ``` 55 | 56 | should be replaced with: 57 | 58 | ```python 59 | from result import Ok, OkErr 60 | res = Ok('yay') 61 | if isinstance(res, OkErr): 62 | print("Result type!") 63 | ``` 64 | 65 | 3\. Because `Result` is now a union type MyPy can statically check the Result type. 66 | In previous versions MyPy saw the following types: 67 | 68 | ```python 69 | r: Result[int, str] = Ok(2) 70 | if r.is_ok(): 71 | reveal_type(r.value) # returns Union[int, str] 72 | ``` 73 | 74 | but now, by using `isinstance`: 75 | 76 | ```python 77 | r: Result[int, str] = Ok(2) # == Union[Ok[int], Err[str]] 78 | if isinstance(r, Ok): 79 | reveal_type(r.value) # returns int 80 | ``` 81 | 82 | This allows for better type checking, but requires the use of `isinstance` instead of `is_ok()` or `is_err()`. 83 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # phony trick from https://keleshev.com/my-book-writing-setup/ 2 | .PHONY: phony 3 | 4 | install: phony 5 | ifndef VIRTUAL_ENV 6 | $(error install can only be run inside a Python virtual environment) 7 | endif 8 | @echo Installing dependencies... 9 | pip install -r requirements-dev.txt 10 | pip install -e . 11 | 12 | lint: phony lint-flake lint-mypy 13 | 14 | lint-flake: phony 15 | flake8 16 | 17 | lint-flake-pre310: phony 18 | # Python <3.10 doesn't support pattern matching. 19 | flake8 --extend-exclude tests/test_pattern_matching.py 20 | 21 | lint-mypy: phony 22 | mypy 23 | 24 | test: phony 25 | pytest 26 | 27 | docs: phony 28 | lazydocs \ 29 | --overview-file README.md \ 30 | --src-base-url https://github.com/rustedpy/result/blob/main/ \ 31 | ./src/result 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Result 2 | 3 | [![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/rustedpy/result/ci.yml?branch=main)](https://github.com/rustedpy/result/actions/workflows/ci.yml?query=branch%3Amain) 4 | [![Coverage](https://codecov.io/gh/rustedpy/result/branch/main/graph/badge.svg)](https://codecov.io/gh/rustedpy/result) 5 | 6 | A simple Result type for Python 3 [inspired by 7 | Rust](https://doc.rust-lang.org/std/result/), fully type annotated. 8 | 9 | ## Installation 10 | 11 | Latest release: 12 | 13 | ``` sh 14 | $ pip install result 15 | ``` 16 | 17 | Latest GitHub `main` branch version: 18 | 19 | ``` sh 20 | $ pip install git+https://github.com/rustedpy/result 21 | ``` 22 | 23 | ## Summary 24 | 25 | The idea is that a result value can be either `Ok(value)` or 26 | `Err(error)`, with a way to differentiate between the two. `Ok` and 27 | `Err` are both classes encapsulating an arbitrary value. `Result[T, E]` 28 | is a generic type alias for `typing.Union[Ok[T], Err[E]]`. It will 29 | change code like this: 30 | 31 | ``` python 32 | def get_user_by_email(email: str) -> Tuple[Optional[User], Optional[str]]: 33 | """ 34 | Return the user instance or an error message. 35 | """ 36 | if not user_exists(email): 37 | return None, 'User does not exist' 38 | if not user_active(email): 39 | return None, 'User is inactive' 40 | user = get_user(email) 41 | return user, None 42 | 43 | user, reason = get_user_by_email('ueli@example.com') 44 | if user is None: 45 | raise RuntimeError('Could not fetch user: %s' % reason) 46 | else: 47 | do_something(user) 48 | ``` 49 | 50 | To something like this: 51 | 52 | ``` python 53 | from result import Ok, Err, Result, is_ok, is_err 54 | 55 | def get_user_by_email(email: str) -> Result[User, str]: 56 | """ 57 | Return the user instance or an error message. 58 | """ 59 | if not user_exists(email): 60 | return Err('User does not exist') 61 | if not user_active(email): 62 | return Err('User is inactive') 63 | user = get_user(email) 64 | return Ok(user) 65 | 66 | user_result = get_user_by_email(email) 67 | if is_ok(user_result): 68 | # type(user_result.ok_value) == User 69 | do_something(user_result.ok_value) 70 | else: 71 | # type(user_result.err_value) == str 72 | raise RuntimeError('Could not fetch user: %s' % user_result.err_value) 73 | ``` 74 | 75 | Note that `.ok_value` exists only on an instance of `Ok` and 76 | `.err_value` exists only on an instance of `Err`. 77 | 78 | And if you're using python version `3.10` or later, you can use the 79 | elegant `match` statement as well: 80 | 81 | ``` python 82 | from result import Result, Ok, Err 83 | 84 | def divide(a: int, b: int) -> Result[int, str]: 85 | if b == 0: 86 | return Err("Cannot divide by zero") 87 | return Ok(a // b) 88 | 89 | values = [(10, 0), (10, 5)] 90 | for a, b in values: 91 | match divide(a, b): 92 | case Ok(value): 93 | print(f"{a} // {b} == {value}") 94 | case Err(e): 95 | print(e) 96 | ``` 97 | 98 | Not all methods 99 | () have been 100 | implemented, only the ones that make sense in the Python context. 101 | All of this in a package allowing easier handling of values that can 102 | be OK or not, without resorting to custom exceptions. 103 | 104 | ## API 105 | 106 | Auto generated API docs are also available at 107 | [./docs/README.md](./docs/README.md). 108 | 109 | Creating an instance: 110 | 111 | ``` python 112 | >>> from result import Ok, Err 113 | >>> res1 = Ok('yay') 114 | >>> res2 = Err('nay') 115 | ``` 116 | 117 | Checking whether a result is `Ok` or `Err`: 118 | 119 | ``` python 120 | if is_err(result): 121 | raise RuntimeError(result.err_value) 122 | do_something(result.ok_value) 123 | ``` 124 | or 125 | ``` python 126 | if is_ok(result): 127 | do_something(result.ok_value) 128 | else: 129 | raise RuntimeError(result.err_value) 130 | ``` 131 | 132 | Alternatively, `isinstance` can be used (interchangeably to type guard functions 133 | `is_ok` and `is_err`). However, relying on `isinstance` may result in code that 134 | is slightly less readable and less concise: 135 | 136 | ``` python 137 | if isinstance(result, Err): 138 | raise RuntimeError(result.err_value) 139 | do_something(result.ok_value) 140 | ``` 141 | 142 | You can also check if an object is `Ok` or `Err` by using the `OkErr` 143 | type. Please note that this type is designed purely for convenience, and 144 | should not be used for anything else. Using `(Ok, Err)` also works fine: 145 | 146 | ``` python 147 | >>> res1 = Ok('yay') 148 | >>> res2 = Err('nay') 149 | >>> isinstance(res1, OkErr) 150 | True 151 | >>> isinstance(res2, OkErr) 152 | True 153 | >>> isinstance(1, OkErr) 154 | False 155 | >>> isinstance(res1, (Ok, Err)) 156 | True 157 | ``` 158 | 159 | Convert a `Result` to the value or `None`: 160 | 161 | ``` python 162 | >>> res1 = Ok('yay') 163 | >>> res2 = Err('nay') 164 | >>> res1.ok() 165 | 'yay' 166 | >>> res2.ok() 167 | None 168 | ``` 169 | 170 | Convert a `Result` to the error or `None`: 171 | 172 | ``` python 173 | >>> res1 = Ok('yay') 174 | >>> res2 = Err('nay') 175 | >>> res1.err() 176 | None 177 | >>> res2.err() 178 | 'nay' 179 | ``` 180 | 181 | Access the value directly, without any other checks: 182 | 183 | ``` python 184 | >>> res1 = Ok('yay') 185 | >>> res2 = Err('nay') 186 | >>> res1.ok_value 187 | 'yay' 188 | >>> res2.err_value 189 | 'nay' 190 | ``` 191 | 192 | Note that this is a property, you cannot assign to it. Results are 193 | immutable. 194 | 195 | When the value inside is irrelevant, we suggest using `None` or a 196 | `bool`, but you're free to use any value you think works best. An 197 | instance of a `Result` (`Ok` or `Err`) must always contain something. If 198 | you're looking for a type that might contain a value you may be 199 | interested in a [maybe](https://github.com/rustedpy/maybe). 200 | 201 | The `unwrap` method returns the value if `Ok` and `unwrap_err` method 202 | returns the error value if `Err`, otherwise it raises an `UnwrapError`: 203 | 204 | ``` python 205 | >>> res1 = Ok('yay') 206 | >>> res2 = Err('nay') 207 | >>> res1.unwrap() 208 | 'yay' 209 | >>> res2.unwrap() 210 | Traceback (most recent call last): 211 | File "", line 1, in 212 | File "C:\project\result\result.py", line 107, in unwrap 213 | return self.expect("Called `Result.unwrap()` on an `Err` value") 214 | File "C:\project\result\result.py", line 101, in expect 215 | raise UnwrapError(message) 216 | result.result.UnwrapError: Called `Result.unwrap()` on an `Err` value 217 | >>> res1.unwrap_err() 218 | Traceback (most recent call last): 219 | ... 220 | >>>res2.unwrap_err() 221 | 'nay' 222 | ``` 223 | 224 | A custom error message can be displayed instead by using `expect` and 225 | `expect_err`: 226 | 227 | ``` python 228 | >>> res1 = Ok('yay') 229 | >>> res2 = Err('nay') 230 | >>> res1.expect('not ok') 231 | 'yay' 232 | >>> res2.expect('not ok') 233 | Traceback (most recent call last): 234 | File "", line 1, in 235 | File "C:\project\result\result.py", line 101, in expect 236 | raise UnwrapError(message) 237 | result.result.UnwrapError: not ok 238 | >>> res1.expect_err('not err') 239 | Traceback (most recent call last): 240 | ... 241 | >>> res2.expect_err('not err') 242 | 'nay' 243 | ``` 244 | 245 | A default value can be returned instead by using `unwrap_or` or 246 | `unwrap_or_else`: 247 | 248 | ``` python 249 | >>> res1 = Ok('yay') 250 | >>> res2 = Err('nay') 251 | >>> res1.unwrap_or('default') 252 | 'yay' 253 | >>> res2.unwrap_or('default') 254 | 'default' 255 | >>> res1.unwrap_or_else(str.upper) 256 | 'yay' 257 | >>> res2.unwrap_or_else(str.upper) 258 | 'NAY' 259 | ``` 260 | 261 | The `unwrap` method will raised an `UnwrapError`. A custom exception can 262 | be raised by using the `unwrap_or_raise` method instead: 263 | 264 | ``` python 265 | >>> res1 = Ok('yay') 266 | >>> res2 = Err('nay') 267 | >>> res1.unwrap_or_raise(ValueError) 268 | 'yay' 269 | >>> res2.unwrap_or_raise(ValueError) 270 | ValueError: nay 271 | ``` 272 | 273 | Values and errors can be mapped using `map`, `map_or`, `map_or_else` and 274 | `map_err`: 275 | 276 | ``` python 277 | >>> Ok(1).map(lambda x: x + 1) 278 | Ok(2) 279 | >>> Err('nay').map(lambda x: x + 1) 280 | Err('nay') 281 | >>> Ok(1).map_or(-1, lambda x: x + 1) 282 | 2 283 | >>> Err(1).map_or(-1, lambda x: x + 1) 284 | -1 285 | >>> Ok(1).map_or_else(lambda: 3, lambda x: x + 1) 286 | 2 287 | >>> Err('nay').map_or_else(lambda: 3, lambda x: x + 1) 288 | 3 289 | >>> Ok(1).map_err(lambda x: x + 1) 290 | Ok(1) 291 | >>> Err(1).map_err(lambda x: x + 1) 292 | Err(2) 293 | ``` 294 | 295 | To save memory, both the `Ok` and `Err` classes are ‘slotted’, i.e. they 296 | define `__slots__`. This means assigning arbitrary attributes to 297 | instances will raise `AttributeError`. 298 | 299 | ### `as_result` Decorator 300 | 301 | The `as_result()` decorator can be used to quickly turn ‘normal’ 302 | functions into `Result` returning ones by specifying one or more 303 | exception types: 304 | 305 | ``` python 306 | @as_result(ValueError, IndexError) 307 | def f(value: int) -> int: 308 | if value == 0: 309 | raise ValueError # becomes Err 310 | elif value == 1: 311 | raise IndexError # becomes Err 312 | elif value == 2: 313 | raise KeyError # raises Exception 314 | else: 315 | return value # becomes Ok 316 | 317 | res = f(0) # Err[ValueError()] 318 | res = f(1) # Err[IndexError()] 319 | res = f(2) # raises KeyError 320 | res = f(3) # Ok[3] 321 | ``` 322 | 323 | `Exception` (or even `BaseException`) can be specified to create a 324 | ‘catch all’ `Result` return type. This is effectively the same as `try` 325 | followed by `except Exception`, which is not considered good practice in 326 | most scenarios, and hence this requires explicit opt-in. 327 | 328 | Since `as_result` is a regular decorator, it can be used to wrap 329 | existing functions (also from other libraries), albeit with a slightly 330 | unconventional syntax (without the usual `@`): 331 | 332 | ``` python 333 | import third_party 334 | 335 | x = third_party.do_something(...) # could raise; who knows? 336 | 337 | safe_do_something = as_result(Exception)(third_party.do_something) 338 | 339 | res = safe_do_something(...) # Ok(...) or Err(...) 340 | if is_ok(res): 341 | print(res.ok_value) 342 | ``` 343 | 344 | ### Do notation 345 | 346 | Do notation is syntactic sugar for a sequence of `and_then()` calls. 347 | Much like the equivalent in Rust or Haskell, but with different syntax. 348 | Instead of `x <- Ok(1)` we write `for x in Ok(1)`. Since the syntax is 349 | generator-based, the final result must be the first line, not the last. 350 | 351 | ``` python 352 | final_result: Result[int, str] = do( 353 | Ok(x + y) 354 | for x in Ok(1) 355 | for y in Ok(2) 356 | ) 357 | ``` 358 | 359 | Note that if you exclude the type annotation, 360 | `final_result: Result[float, int] = ...`, your type checker may be 361 | unable to infer the return type. To avoid an errors or warnings from 362 | your type checker, you should add a type hint when using the `do` 363 | function. 364 | 365 | This is similar to Rust's [m! 366 | macro](https://docs.rs/do-notation/latest/do_notation/): 367 | 368 | ``` rust 369 | use do_notation::m; 370 | let r = m! { 371 | x <- Some(1); 372 | y <- Some(2); 373 | Some(x + y) 374 | }; 375 | ``` 376 | 377 | Note that if your do statement has multiple for\`s, you can access an identifier bound in a 379 | previous \`for. Example: 380 | 381 | ``` python 382 | my_result: Result[int, str] = do( 383 | f(x, y, z) 384 | for x in get_x() 385 | for y in calculate_y_from_x(x) 386 | for z in calculate_z_from_x_y(x, y) 387 | ) 388 | ``` 389 | 390 | You can use `do()` with awaited values as follows: 391 | 392 | ``` python 393 | async def process_data(data) -> Result[int, str]: 394 | res1 = await get_result_1(data) 395 | res2 = await get_result_2(data) 396 | return do( 397 | Ok(x + y) 398 | for x in res1 399 | for y in res2 400 | ) 401 | ``` 402 | 403 | However, if you want to await something inside the expression, use 404 | `do_async()`: 405 | 406 | ``` python 407 | async def process_data(data) -> Result[int, str]: 408 | return do_async( 409 | Ok(x + y) 410 | for x in await get_result_1(data) 411 | for y in await get_result_2(data) 412 | ) 413 | ``` 414 | 415 | Troubleshooting `do()` calls: 416 | 417 | ``` python 418 | TypeError("Got async_generator but expected generator") 419 | ``` 420 | 421 | Sometimes regular `do()` can handle async values, but this error means 422 | you have hit a case where it does not. You should use `do_async()` here 423 | instead. 424 | 425 | ## Contributing 426 | 427 | These steps should work on any Unix-based system (Linux, macOS, etc) with Python 428 | and `make` installed. On Windows, you will need to refer to the Python 429 | documentation (linked below) and reference the `Makefile` for commands to run 430 | from the non-unix shell you're using on Windows. 431 | 432 | 1. Setup and activate a virtual environment. See [Python docs][pydocs-venv] for more 433 | information about virtual environments and setup. 434 | 2. Run `make install` to install dependencies 435 | 3. Switch to a new git branch and make your changes 436 | 4. Test your changes: 437 | - `make test` 438 | - `make lint` 439 | - You can also start a Python REPL and import `result` 440 | 5. Update documentation 441 | - Edit any relevant docstrings, markdown files 442 | - Run `make docs` 443 | 6. Add an entry to the [changelog](./CHANGELOG.md) 444 | 5. Git commit all your changes and create a new PR. 445 | 446 | [pydocs-venv]: https://docs.python.org/3/library/venv.html 447 | 448 | ## FAQ 449 | 450 | - **Why should I use the `is_ok` (`is_err`) type guard function over the `is_ok` (`is_err`) method?** 451 | 452 | As you can see in the following example, MyPy can only narrow the type correctly 453 | while using the type guard **functions**: 454 | ```python 455 | result: Result[int, str] 456 | 457 | if is_ok(result): 458 | reveal_type(result) # "result.result.Ok[builtins.int]" 459 | else: 460 | reveal_type(result) # "result.result.Err[builtins.str]" 461 | 462 | if result.is_ok(): 463 | reveal_type(result) # "Union[result.result.Ok[builtins.int], result.result.Err[builtins.str]]" 464 | else: 465 | reveal_type(result) # "Union[result.result.Ok[builtins.int], result.result.Err[builtins.str]]" 466 | ``` 467 | 468 | - **Why do I get the "Cannot infer type argument" error with MyPy?** 469 | 470 | There is [a bug in MyPy](https://github.com/python/mypy/issues/230) 471 | which can be triggered in some scenarios. Using `if isinstance(res, Ok)` 472 | instead of `if res.is_ok()` will help in some cases. Otherwise using 473 | [one of these 474 | workarounds](https://github.com/python/mypy/issues/3889#issuecomment-325997911) 475 | can help. 476 | 477 | ## Related Projects 478 | 479 | - [dry-python/returns: Make your functions return something meaningful, typed, and safe!](https://github.com/dry-python/returns) 480 | - [alexandermalyga/poltergeist: Rust-like error handling in Python, with type-safety in mind.](https://github.com/alexandermalyga/poltergeist) 481 | 482 | ## License 483 | 484 | MIT License 485 | -------------------------------------------------------------------------------- /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/result/__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/result/__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/result-${VERSION}* 38 | git push 39 | git push --tags 40 | ``` 41 | 42 | 7) Optionally check the new version is published correctly 43 | - https://github.com/rustedpy/result/tags 44 | - https://pypi.org/project/result/#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/result/__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 | - [`result`](./result.md#module-result) 8 | 9 | ## Classes 10 | 11 | - [`result.DoException`](./result.md#class-doexception): This is used to signal to `do()` that the result is an `Err`, 12 | - [`result.Err`](./result.md#class-err): A value that signifies failure and which stores arbitrary data for the error. 13 | - [`result.Ok`](./result.md#class-ok): A value that indicates success and which stores arbitrary data for the return value. 14 | - [`result.UnwrapError`](./result.md#class-unwraperror): Exception raised from ``.unwrap_<...>`` and ``.expect_<...>`` calls. 15 | 16 | ## Functions 17 | 18 | - [`result.as_async_result`](./result.md#function-as_async_result): Make a decorator to turn an async function into one that returns a ``Result``. 19 | - [`result.as_result`](./result.md#function-as_result): Make a decorator to turn a function into one that returns a ``Result``. 20 | - [`result.do`](./result.md#function-do): Do notation for Result (syntactic sugar for sequence of `and_then()` calls). 21 | - [`result.do_async`](./result.md#function-do_async): Async version of do. Example: 22 | - [`result.is_err`](./result.md#function-is_err): A type guard to check if a result is an Err 23 | - [`result.is_ok`](./result.md#function-is_ok): A type guard to check if a result is an Ok 24 | 25 | 26 | --- 27 | 28 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ 29 | -------------------------------------------------------------------------------- /docs/result.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `result` 6 | 7 | 8 | 9 | 10 | **Global Variables** 11 | --------------- 12 | - **OkErr** 13 | 14 | --- 15 | 16 | 17 | 18 | ## function `as_result` 19 | 20 | ```python 21 | as_result( 22 | *exceptions: 'Type[TBE]' 23 | ) → Callable[[Callable[P, R]], Callable[P, Result[R, TBE]]] 24 | ``` 25 | 26 | Make a decorator to turn a function into one that returns a ``Result``. 27 | 28 | Regular return values are turned into ``Ok(return_value)``. Raised exceptions of the specified exception type(s) are turned into ``Err(exc)``. 29 | 30 | 31 | --- 32 | 33 | 34 | 35 | ## function `as_async_result` 36 | 37 | ```python 38 | as_async_result( 39 | *exceptions: 'Type[TBE]' 40 | ) → Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[Result[R, TBE]]]] 41 | ``` 42 | 43 | Make a decorator to turn an async function into one that returns a ``Result``. Regular return values are turned into ``Ok(return_value)``. Raised exceptions of the specified exception type(s) are turned into ``Err(exc)``. 44 | 45 | 46 | --- 47 | 48 | 49 | 50 | ## function `is_ok` 51 | 52 | ```python 53 | is_ok(result: 'Result[T, E]') → TypeIs[Ok[T]] 54 | ``` 55 | 56 | A type guard to check if a result is an Ok 57 | 58 | Usage: 59 | 60 | ``` python 61 | r: Result[int, str] = get_a_result() 62 | if is_ok(r): 63 | r # r is of type Ok[int] 64 | elif is_err(r): 65 | r # r is of type Err[str] 66 | ``` 67 | 68 | 69 | --- 70 | 71 | 72 | 73 | ## function `is_err` 74 | 75 | ```python 76 | is_err(result: 'Result[T, E]') → TypeIs[Err[E]] 77 | ``` 78 | 79 | A type guard to check if a result is an Err 80 | 81 | Usage: 82 | 83 | ``` python 84 | r: Result[int, str] = get_a_result() 85 | if is_ok(r): 86 | r # r is of type Ok[int] 87 | elif is_err(r): 88 | r # r is of type Err[str] 89 | ``` 90 | 91 | 92 | --- 93 | 94 | 95 | 96 | ## function `do` 97 | 98 | ```python 99 | do(gen: 'Generator[Result[T, E], None, None]') → Result[T, E] 100 | ``` 101 | 102 | Do notation for Result (syntactic sugar for sequence of `and_then()` calls). 103 | 104 | 105 | 106 | Usage: 107 | 108 | ``` rust 109 | // This is similar to 110 | use do_notation::m; 111 | let final_result = m! { 112 | x <- Ok("hello"); 113 | y <- Ok(True); 114 | Ok(len(x) + int(y) + 0.5) 115 | }; 116 | ``` 117 | 118 | ``` rust 119 | final_result: Result[float, int] = do( 120 | Ok(len(x) + int(y) + 0.5) 121 | for x in Ok("hello") 122 | for y in Ok(True) 123 | ) 124 | ``` 125 | 126 | NOTE: If you exclude the type annotation e.g. `Result[float, int]` your type checker might be unable to infer the return type. To avoid an error, you might need to help it with the type hint. 127 | 128 | 129 | --- 130 | 131 | 132 | 133 | ## function `do_async` 134 | 135 | ```python 136 | do_async( 137 | gen: 'Union[Generator[Result[T, E], None, None], AsyncGenerator[Result[T, E], None]]' 138 | ) → Result[T, E] 139 | ``` 140 | 141 | Async version of do. Example: 142 | 143 | ``` python 144 | final_result: Result[float, int] = await do_async( 145 | Ok(len(x) + int(y) + z) 146 | for x in await get_async_result_1() 147 | for y in await get_async_result_2() 148 | for z in get_sync_result_3() 149 | ) 150 | ``` 151 | 152 | NOTE: Python makes generators async in a counter-intuitive way. 153 | 154 | ``` python 155 | # This is a regular generator: 156 | async def foo(): ... 157 | do(Ok(1) for x in await foo()) 158 | ``` 159 | 160 | ``` python 161 | # But this is an async generator: 162 | async def foo(): ... 163 | async def bar(): ... 164 | do( 165 | Ok(1) 166 | for x in await foo() 167 | for y in await bar() 168 | ) 169 | ``` 170 | 171 | We let users try to use regular `do()`, which works in some cases of awaiting async values. If we hit a case like above, we raise an exception telling the user to use `do_async()` instead. See `do()`. 172 | 173 | However, for better usability, it's better for `do_async()` to also accept regular generators, as you get in the first case: 174 | 175 | ``` python 176 | async def foo(): ... 177 | do(Ok(1) for x in await foo()) 178 | ``` 179 | 180 | Furthermore, neither mypy nor pyright can infer that the second case is actually an async generator, so we cannot annotate `do_async()` as accepting only an async generator. This is additional motivation to accept either. 181 | 182 | 183 | --- 184 | 185 | 186 | 187 | ## class `Ok` 188 | A value that indicates success and which stores arbitrary data for the return value. 189 | 190 | 191 | 192 | ### method `__init__` 193 | 194 | ```python 195 | __init__(value: 'T') → None 196 | ``` 197 | 198 | 199 | 200 | 201 | 202 | 203 | --- 204 | 205 | #### property ok_value 206 | 207 | Return the inner value. 208 | 209 | --- 210 | 211 | #### property value 212 | 213 | Return the inner value. 214 | 215 | @deprecated Use `ok_value` or `err_value` instead. This method will be removed in a future version. 216 | 217 | 218 | 219 | --- 220 | 221 | 222 | 223 | ### method `and_then` 224 | 225 | ```python 226 | and_then(op: 'Callable[[T], Result[U, E]]') → Result[U, E] 227 | ``` 228 | 229 | The contained result is `Ok`, so return the result of `op` with the original value passed in 230 | 231 | --- 232 | 233 | 234 | 235 | ### method `and_then_async` 236 | 237 | ```python 238 | and_then_async(op: 'Callable[[T], Awaitable[Result[U, E]]]') → Result[U, E] 239 | ``` 240 | 241 | The contained result is `Ok`, so return the result of `op` with the original value passed in 242 | 243 | --- 244 | 245 | 246 | 247 | ### method `err` 248 | 249 | ```python 250 | err() → None 251 | ``` 252 | 253 | Return `None`. 254 | 255 | --- 256 | 257 | 258 | 259 | ### method `expect` 260 | 261 | ```python 262 | expect(_message: 'str') → T 263 | ``` 264 | 265 | Return the value. 266 | 267 | --- 268 | 269 | 270 | 271 | ### method `expect_err` 272 | 273 | ```python 274 | expect_err(message: 'str') → NoReturn 275 | ``` 276 | 277 | Raise an UnwrapError since this type is `Ok` 278 | 279 | --- 280 | 281 | 282 | 283 | ### method `inspect` 284 | 285 | ```python 286 | inspect(op: 'Callable[[T], Any]') → Result[T, E] 287 | ``` 288 | 289 | Calls a function with the contained value if `Ok`. Returns the original result. 290 | 291 | --- 292 | 293 | 294 | 295 | ### method `inspect_err` 296 | 297 | ```python 298 | inspect_err(op: 'Callable[[E], Any]') → Result[T, E] 299 | ``` 300 | 301 | Calls a function with the contained value if `Err`. Returns the original result. 302 | 303 | --- 304 | 305 | 306 | 307 | ### method `is_err` 308 | 309 | ```python 310 | is_err() → Literal[False] 311 | ``` 312 | 313 | 314 | 315 | 316 | 317 | --- 318 | 319 | 320 | 321 | ### method `is_ok` 322 | 323 | ```python 324 | is_ok() → Literal[True] 325 | ``` 326 | 327 | 328 | 329 | 330 | 331 | --- 332 | 333 | 334 | 335 | ### method `map` 336 | 337 | ```python 338 | map(op: 'Callable[[T], U]') → Ok[U] 339 | ``` 340 | 341 | The contained result is `Ok`, so return `Ok` with original value mapped to a new value using the passed in function. 342 | 343 | --- 344 | 345 | 346 | 347 | ### method `map_async` 348 | 349 | ```python 350 | map_async(op: 'Callable[[T], Awaitable[U]]') → Ok[U] 351 | ``` 352 | 353 | The contained result is `Ok`, so return the result of `op` with the original value passed in 354 | 355 | --- 356 | 357 | 358 | 359 | ### method `map_err` 360 | 361 | ```python 362 | map_err(op: 'object') → Ok[T] 363 | ``` 364 | 365 | The contained result is `Ok`, so return `Ok` with the original value 366 | 367 | --- 368 | 369 | 370 | 371 | ### method `map_or` 372 | 373 | ```python 374 | map_or(default: 'object', op: 'Callable[[T], U]') → U 375 | ``` 376 | 377 | The contained result is `Ok`, so return the original value mapped to a new value using the passed in function. 378 | 379 | --- 380 | 381 | 382 | 383 | ### method `map_or_else` 384 | 385 | ```python 386 | map_or_else(default_op: 'object', op: 'Callable[[T], U]') → U 387 | ``` 388 | 389 | The contained result is `Ok`, so return original value mapped to a new value using the passed in `op` function. 390 | 391 | --- 392 | 393 | 394 | 395 | ### method `ok` 396 | 397 | ```python 398 | ok() → T 399 | ``` 400 | 401 | Return the value. 402 | 403 | --- 404 | 405 | 406 | 407 | ### method `or_else` 408 | 409 | ```python 410 | or_else(op: 'object') → Ok[T] 411 | ``` 412 | 413 | The contained result is `Ok`, so return `Ok` with the original value 414 | 415 | --- 416 | 417 | 418 | 419 | ### method `unwrap` 420 | 421 | ```python 422 | unwrap() → T 423 | ``` 424 | 425 | Return the value. 426 | 427 | --- 428 | 429 | 430 | 431 | ### method `unwrap_err` 432 | 433 | ```python 434 | unwrap_err() → NoReturn 435 | ``` 436 | 437 | Raise an UnwrapError since this type is `Ok` 438 | 439 | --- 440 | 441 | 442 | 443 | ### method `unwrap_or` 444 | 445 | ```python 446 | unwrap_or(_default: 'U') → T 447 | ``` 448 | 449 | Return the value. 450 | 451 | --- 452 | 453 | 454 | 455 | ### method `unwrap_or_else` 456 | 457 | ```python 458 | unwrap_or_else(op: 'object') → T 459 | ``` 460 | 461 | Return the value. 462 | 463 | --- 464 | 465 | 466 | 467 | ### method `unwrap_or_raise` 468 | 469 | ```python 470 | unwrap_or_raise(e: 'object') → T 471 | ``` 472 | 473 | Return the value. 474 | 475 | 476 | --- 477 | 478 | 479 | 480 | ## class `DoException` 481 | This is used to signal to `do()` that the result is an `Err`, which short-circuits the generator and returns that Err. Using this exception for control flow in `do()` allows us to simulate `and_then()` in the Err case: namely, we don't call `op`, we just return `self` (the Err). 482 | 483 | 484 | 485 | ### method `__init__` 486 | 487 | ```python 488 | __init__(err: 'Err[E]') → None 489 | ``` 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | --- 500 | 501 | 502 | 503 | ## class `Err` 504 | A value that signifies failure and which stores arbitrary data for the error. 505 | 506 | 507 | 508 | ### method `__init__` 509 | 510 | ```python 511 | __init__(value: 'E') → None 512 | ``` 513 | 514 | 515 | 516 | 517 | 518 | 519 | --- 520 | 521 | #### property err_value 522 | 523 | Return the inner value. 524 | 525 | --- 526 | 527 | #### property value 528 | 529 | Return the inner value. 530 | 531 | @deprecated Use `ok_value` or `err_value` instead. This method will be removed in a future version. 532 | 533 | 534 | 535 | --- 536 | 537 | 538 | 539 | ### method `and_then` 540 | 541 | ```python 542 | and_then(op: 'object') → Err[E] 543 | ``` 544 | 545 | The contained result is `Err`, so return `Err` with the original value 546 | 547 | --- 548 | 549 | 550 | 551 | ### method `and_then_async` 552 | 553 | ```python 554 | and_then_async(op: 'object') → Err[E] 555 | ``` 556 | 557 | The contained result is `Err`, so return `Err` with the original value 558 | 559 | --- 560 | 561 | 562 | 563 | ### method `err` 564 | 565 | ```python 566 | err() → E 567 | ``` 568 | 569 | Return the error. 570 | 571 | --- 572 | 573 | 574 | 575 | ### method `expect` 576 | 577 | ```python 578 | expect(message: 'str') → NoReturn 579 | ``` 580 | 581 | Raises an `UnwrapError`. 582 | 583 | --- 584 | 585 | 586 | 587 | ### method `expect_err` 588 | 589 | ```python 590 | expect_err(_message: 'str') → E 591 | ``` 592 | 593 | Return the inner value 594 | 595 | --- 596 | 597 | 598 | 599 | ### method `inspect` 600 | 601 | ```python 602 | inspect(op: 'Callable[[T], Any]') → Result[T, E] 603 | ``` 604 | 605 | Calls a function with the contained value if `Ok`. Returns the original result. 606 | 607 | --- 608 | 609 | 610 | 611 | ### method `inspect_err` 612 | 613 | ```python 614 | inspect_err(op: 'Callable[[E], Any]') → Result[T, E] 615 | ``` 616 | 617 | Calls a function with the contained value if `Err`. Returns the original result. 618 | 619 | --- 620 | 621 | 622 | 623 | ### method `is_err` 624 | 625 | ```python 626 | is_err() → Literal[True] 627 | ``` 628 | 629 | 630 | 631 | 632 | 633 | --- 634 | 635 | 636 | 637 | ### method `is_ok` 638 | 639 | ```python 640 | is_ok() → Literal[False] 641 | ``` 642 | 643 | 644 | 645 | 646 | 647 | --- 648 | 649 | 650 | 651 | ### method `map` 652 | 653 | ```python 654 | map(op: 'object') → Err[E] 655 | ``` 656 | 657 | Return `Err` with the same value 658 | 659 | --- 660 | 661 | 662 | 663 | ### method `map_async` 664 | 665 | ```python 666 | map_async(op: 'object') → Err[E] 667 | ``` 668 | 669 | The contained result is `Ok`, so return the result of `op` with the original value passed in 670 | 671 | --- 672 | 673 | 674 | 675 | ### method `map_err` 676 | 677 | ```python 678 | map_err(op: 'Callable[[E], F]') → Err[F] 679 | ``` 680 | 681 | The contained result is `Err`, so return `Err` with original error mapped to a new value using the passed in function. 682 | 683 | --- 684 | 685 | 686 | 687 | ### method `map_or` 688 | 689 | ```python 690 | map_or(default: 'U', op: 'object') → U 691 | ``` 692 | 693 | Return the default value 694 | 695 | --- 696 | 697 | 698 | 699 | ### method `map_or_else` 700 | 701 | ```python 702 | map_or_else(default_op: 'Callable[[], U]', op: 'object') → U 703 | ``` 704 | 705 | Return the result of the default operation 706 | 707 | --- 708 | 709 | 710 | 711 | ### method `ok` 712 | 713 | ```python 714 | ok() → None 715 | ``` 716 | 717 | Return `None`. 718 | 719 | --- 720 | 721 | 722 | 723 | ### method `or_else` 724 | 725 | ```python 726 | or_else(op: 'Callable[[E], Result[T, F]]') → Result[T, F] 727 | ``` 728 | 729 | The contained result is `Err`, so return the result of `op` with the original value passed in 730 | 731 | --- 732 | 733 | 734 | 735 | ### method `unwrap` 736 | 737 | ```python 738 | unwrap() → NoReturn 739 | ``` 740 | 741 | Raises an `UnwrapError`. 742 | 743 | --- 744 | 745 | 746 | 747 | ### method `unwrap_err` 748 | 749 | ```python 750 | unwrap_err() → E 751 | ``` 752 | 753 | Return the inner value 754 | 755 | --- 756 | 757 | 758 | 759 | ### method `unwrap_or` 760 | 761 | ```python 762 | unwrap_or(default: 'U') → U 763 | ``` 764 | 765 | Return `default`. 766 | 767 | --- 768 | 769 | 770 | 771 | ### method `unwrap_or_else` 772 | 773 | ```python 774 | unwrap_or_else(op: 'Callable[[E], T]') → T 775 | ``` 776 | 777 | The contained result is ``Err``, so return the result of applying ``op`` to the error value. 778 | 779 | --- 780 | 781 | 782 | 783 | ### method `unwrap_or_raise` 784 | 785 | ```python 786 | unwrap_or_raise(e: 'Type[TBE]') → NoReturn 787 | ``` 788 | 789 | The contained result is ``Err``, so raise the exception with the value. 790 | 791 | 792 | --- 793 | 794 | 795 | 796 | ## class `UnwrapError` 797 | Exception raised from ``.unwrap_<...>`` and ``.expect_<...>`` calls. 798 | 799 | The original ``Result`` can be accessed via the ``.result`` attribute, but this is not intended for regular use, as type information is lost: ``UnwrapError`` doesn't know about both ``T`` and ``E``, since it's raised from ``Ok()`` or ``Err()`` which only knows about either ``T`` or ``E``, not both. 800 | 801 | 802 | 803 | ### method `__init__` 804 | 805 | ```python 806 | __init__(result: 'Result[object, object]', message: 'str') → None 807 | ``` 808 | 809 | 810 | 811 | 812 | 813 | 814 | --- 815 | 816 | #### property result 817 | 818 | Returns the original result. 819 | 820 | 821 | 822 | 823 | 824 | 825 | --- 826 | 827 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ 828 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.mypy] 6 | python_version = "3.12" 7 | files = [ 8 | "src", 9 | "tests", 10 | ] 11 | # Exclude files with pattern matching syntax until we drop support for Python 12 | # versions that don't support pattern matching. Trying to use with an older 13 | # Python version results in a "invalid syntax" error from mypy 14 | exclude = "tests/test_pattern_matching.py" 15 | check_untyped_defs = true 16 | disallow_incomplete_defs = true 17 | disallow_untyped_decorators = true 18 | disallow_any_generics = true 19 | disallow_subclassing_any = true 20 | disallow_untyped_calls = true 21 | disallow_untyped_defs = true 22 | ignore_missing_imports = true 23 | no_implicit_optional = true 24 | no_implicit_reexport = true 25 | show_column_numbers = true 26 | show_error_codes = true 27 | show_error_context = true 28 | strict_equality = true 29 | strict_optional = true 30 | warn_redundant_casts = true 31 | warn_return_any = true 32 | warn_unused_configs = true 33 | warn_unused_ignores = true 34 | 35 | [tool.pytest.ini_options] 36 | addopts = [ 37 | "--tb=short", 38 | "--cov=result", 39 | "--cov=tests", 40 | "--cov-report=term", 41 | "--cov-report=xml", 42 | 43 | # By default, ignore tests that only run on Python 3.10+ 44 | "--ignore=tests/test_pattern_matching.py", 45 | ] 46 | testpaths = [ 47 | "tests", 48 | ] 49 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | build 2 | flake8 3 | twine 4 | pytest 5 | pytest-cov 6 | mypy 7 | pytest-mypy-plugins 8 | pytest-asyncio 9 | lazydocs 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = result 3 | version = attr: result.__version__ 4 | description = A Rust-like result type for Python 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | keywords = rust, result, enum 8 | author = Danilo Bargen 9 | author_email = mail@dbrgn.ch 10 | maintainer = rustedpy github org members (https://github.com/rustedpy) 11 | url = https://github.com/rustedpy/result 12 | license = MIT 13 | license_file = LICENSE 14 | classifiers = 15 | Development Status :: 4 - Beta 16 | License :: OSI Approved :: MIT License 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.8 19 | Programming Language :: Python :: 3.9 20 | Programming Language :: Python :: 3.10 21 | Programming Language :: Python :: 3.11 22 | Programming Language :: Python :: 3.12 23 | Programming Language :: Python :: 3 :: Only 24 | 25 | [options] 26 | include_package_data = True 27 | install_requires = 28 | typing_extensions>=4.10.0;python_version<'3.13' 29 | package_dir = 30 | =src 31 | packages = find: 32 | python_requires = >=3.8 33 | zip_safe = True 34 | 35 | [options.packages.find] 36 | where = src 37 | 38 | [options.package_data] 39 | result = py.typed 40 | 41 | [flake8] 42 | # flake8 does not (yet?) support pyproject.toml; see 43 | # https://github.com/PyCQA/flake8/issues/234 44 | max-line-length = 99 45 | exclude = 46 | .direnv/ 47 | .tox/ 48 | venv/ 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/result/__init__.py: -------------------------------------------------------------------------------- 1 | from .result import ( 2 | Err, 3 | Ok, 4 | OkErr, 5 | Result, 6 | UnwrapError, 7 | as_async_result, 8 | as_result, 9 | is_ok, 10 | is_err, 11 | do, 12 | do_async, 13 | ) 14 | 15 | __all__ = [ 16 | "Err", 17 | "Ok", 18 | "OkErr", 19 | "Result", 20 | "UnwrapError", 21 | "as_async_result", 22 | "as_result", 23 | "is_ok", 24 | "is_err", 25 | "do", 26 | "do_async", 27 | ] 28 | __version__ = "0.18.0.dev0" 29 | -------------------------------------------------------------------------------- /src/result/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustedpy/result/0b855e1e38a08d6f0a4b0138b10c127c01e54ab4/src/result/py.typed -------------------------------------------------------------------------------- /src/result/result.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | import inspect 5 | import sys 6 | from warnings import warn 7 | from typing import ( 8 | Any, 9 | AsyncGenerator, 10 | Awaitable, 11 | Callable, 12 | Final, 13 | Generator, 14 | Generic, 15 | Iterator, 16 | Literal, 17 | NoReturn, 18 | Type, 19 | TypeVar, 20 | Union, 21 | ) 22 | 23 | from typing_extensions import TypeIs 24 | 25 | if sys.version_info >= (3, 10): 26 | from typing import ParamSpec, TypeAlias 27 | else: 28 | from typing_extensions import ParamSpec, TypeAlias 29 | 30 | 31 | T = TypeVar("T", covariant=True) # Success type 32 | E = TypeVar("E", covariant=True) # Error type 33 | U = TypeVar("U") 34 | F = TypeVar("F") 35 | P = ParamSpec("P") 36 | R = TypeVar("R") 37 | TBE = TypeVar("TBE", bound=BaseException) 38 | 39 | 40 | class Ok(Generic[T]): 41 | """ 42 | A value that indicates success and which stores arbitrary data for the return value. 43 | """ 44 | 45 | __match_args__ = ("ok_value",) 46 | __slots__ = ("_value",) 47 | 48 | def __iter__(self) -> Iterator[T]: 49 | yield self._value 50 | 51 | def __init__(self, value: T) -> None: 52 | self._value = value 53 | 54 | def __repr__(self) -> str: 55 | return "Ok({})".format(repr(self._value)) 56 | 57 | def __eq__(self, other: Any) -> bool: 58 | return isinstance(other, Ok) and self._value == other._value 59 | 60 | def __ne__(self, other: Any) -> bool: 61 | return not (self == other) 62 | 63 | def __hash__(self) -> int: 64 | return hash((True, self._value)) 65 | 66 | def is_ok(self) -> Literal[True]: 67 | return True 68 | 69 | def is_err(self) -> Literal[False]: 70 | return False 71 | 72 | def ok(self) -> T: 73 | """ 74 | Return the value. 75 | """ 76 | return self._value 77 | 78 | def err(self) -> None: 79 | """ 80 | Return `None`. 81 | """ 82 | return None 83 | 84 | @property 85 | def value(self) -> T: 86 | """ 87 | Return the inner value. 88 | 89 | @deprecated Use `ok_value` or `err_value` instead. This method will be 90 | removed in a future version. 91 | """ 92 | warn( 93 | "Accessing `.value` on Result type is deprecated, please use " 94 | + "`.ok_value` or `.err_value` instead", 95 | DeprecationWarning, 96 | stacklevel=2, 97 | ) 98 | return self._value 99 | 100 | @property 101 | def ok_value(self) -> T: 102 | """ 103 | Return the inner value. 104 | """ 105 | return self._value 106 | 107 | def expect(self, _message: str) -> T: 108 | """ 109 | Return the value. 110 | """ 111 | return self._value 112 | 113 | def expect_err(self, message: str) -> NoReturn: 114 | """ 115 | Raise an UnwrapError since this type is `Ok` 116 | """ 117 | raise UnwrapError(self, message) 118 | 119 | def unwrap(self) -> T: 120 | """ 121 | Return the value. 122 | """ 123 | return self._value 124 | 125 | def unwrap_err(self) -> NoReturn: 126 | """ 127 | Raise an UnwrapError since this type is `Ok` 128 | """ 129 | raise UnwrapError(self, "Called `Result.unwrap_err()` on an `Ok` value") 130 | 131 | def unwrap_or(self, _default: U) -> T: 132 | """ 133 | Return the value. 134 | """ 135 | return self._value 136 | 137 | def unwrap_or_else(self, op: object) -> T: 138 | """ 139 | Return the value. 140 | """ 141 | return self._value 142 | 143 | def unwrap_or_raise(self, e: object) -> T: 144 | """ 145 | Return the value. 146 | """ 147 | return self._value 148 | 149 | def map(self, op: Callable[[T], U]) -> Ok[U]: 150 | """ 151 | The contained result is `Ok`, so return `Ok` with original value mapped to 152 | a new value using the passed in function. 153 | """ 154 | return Ok(op(self._value)) 155 | 156 | async def map_async( 157 | self, op: Callable[[T], Awaitable[U]] 158 | ) -> Ok[U]: 159 | """ 160 | The contained result is `Ok`, so return the result of `op` with the 161 | original value passed in 162 | """ 163 | return Ok(await op(self._value)) 164 | 165 | def map_or(self, default: object, op: Callable[[T], U]) -> U: 166 | """ 167 | The contained result is `Ok`, so return the original value mapped to a new 168 | value using the passed in function. 169 | """ 170 | return op(self._value) 171 | 172 | def map_or_else(self, default_op: object, op: Callable[[T], U]) -> U: 173 | """ 174 | The contained result is `Ok`, so return original value mapped to 175 | a new value using the passed in `op` function. 176 | """ 177 | return op(self._value) 178 | 179 | def map_err(self, op: object) -> Ok[T]: 180 | """ 181 | The contained result is `Ok`, so return `Ok` with the original value 182 | """ 183 | return self 184 | 185 | def and_then(self, op: Callable[[T], Result[U, E]]) -> Result[U, E]: 186 | """ 187 | The contained result is `Ok`, so return the result of `op` with the 188 | original value passed in 189 | """ 190 | return op(self._value) 191 | 192 | async def and_then_async( 193 | self, op: Callable[[T], Awaitable[Result[U, E]]] 194 | ) -> Result[U, E]: 195 | """ 196 | The contained result is `Ok`, so return the result of `op` with the 197 | original value passed in 198 | """ 199 | return await op(self._value) 200 | 201 | def or_else(self, op: object) -> Ok[T]: 202 | """ 203 | The contained result is `Ok`, so return `Ok` with the original value 204 | """ 205 | return self 206 | 207 | def inspect(self, op: Callable[[T], Any]) -> Result[T, E]: 208 | """ 209 | Calls a function with the contained value if `Ok`. Returns the original result. 210 | """ 211 | op(self._value) 212 | return self 213 | 214 | def inspect_err(self, op: Callable[[E], Any]) -> Result[T, E]: 215 | """ 216 | Calls a function with the contained value if `Err`. Returns the original result. 217 | """ 218 | return self 219 | 220 | 221 | class DoException(Exception): 222 | """ 223 | This is used to signal to `do()` that the result is an `Err`, 224 | which short-circuits the generator and returns that Err. 225 | Using this exception for control flow in `do()` allows us 226 | to simulate `and_then()` in the Err case: namely, we don't call `op`, 227 | we just return `self` (the Err). 228 | """ 229 | 230 | def __init__(self, err: Err[E]) -> None: 231 | self.err = err 232 | 233 | 234 | class Err(Generic[E]): 235 | """ 236 | A value that signifies failure and which stores arbitrary data for the error. 237 | """ 238 | 239 | __match_args__ = ("err_value",) 240 | __slots__ = ("_value",) 241 | 242 | def __iter__(self) -> Iterator[NoReturn]: 243 | def _iter() -> Iterator[NoReturn]: 244 | # Exception will be raised when the iterator is advanced, not when it's created 245 | raise DoException(self) 246 | yield # This yield will never be reached, but is necessary to create a generator 247 | 248 | return _iter() 249 | 250 | def __init__(self, value: E) -> None: 251 | self._value = value 252 | 253 | def __repr__(self) -> str: 254 | return "Err({})".format(repr(self._value)) 255 | 256 | def __eq__(self, other: Any) -> bool: 257 | return isinstance(other, Err) and self._value == other._value 258 | 259 | def __ne__(self, other: Any) -> bool: 260 | return not (self == other) 261 | 262 | def __hash__(self) -> int: 263 | return hash((False, self._value)) 264 | 265 | def is_ok(self) -> Literal[False]: 266 | return False 267 | 268 | def is_err(self) -> Literal[True]: 269 | return True 270 | 271 | def ok(self) -> None: 272 | """ 273 | Return `None`. 274 | """ 275 | return None 276 | 277 | def err(self) -> E: 278 | """ 279 | Return the error. 280 | """ 281 | return self._value 282 | 283 | @property 284 | def value(self) -> E: 285 | """ 286 | Return the inner value. 287 | 288 | @deprecated Use `ok_value` or `err_value` instead. This method will be 289 | removed in a future version. 290 | """ 291 | warn( 292 | "Accessing `.value` on Result type is deprecated, please use " 293 | + "`.ok_value` or '.err_value' instead", 294 | DeprecationWarning, 295 | stacklevel=2, 296 | ) 297 | return self._value 298 | 299 | @property 300 | def err_value(self) -> E: 301 | """ 302 | Return the inner value. 303 | """ 304 | return self._value 305 | 306 | def expect(self, message: str) -> NoReturn: 307 | """ 308 | Raises an `UnwrapError`. 309 | """ 310 | exc = UnwrapError( 311 | self, 312 | f"{message}: {self._value!r}", 313 | ) 314 | if isinstance(self._value, BaseException): 315 | raise exc from self._value 316 | raise exc 317 | 318 | def expect_err(self, _message: str) -> E: 319 | """ 320 | Return the inner value 321 | """ 322 | return self._value 323 | 324 | def unwrap(self) -> NoReturn: 325 | """ 326 | Raises an `UnwrapError`. 327 | """ 328 | exc = UnwrapError( 329 | self, 330 | f"Called `Result.unwrap()` on an `Err` value: {self._value!r}", 331 | ) 332 | if isinstance(self._value, BaseException): 333 | raise exc from self._value 334 | raise exc 335 | 336 | def unwrap_err(self) -> E: 337 | """ 338 | Return the inner value 339 | """ 340 | return self._value 341 | 342 | def unwrap_or(self, default: U) -> U: 343 | """ 344 | Return `default`. 345 | """ 346 | return default 347 | 348 | def unwrap_or_else(self, op: Callable[[E], T]) -> T: 349 | """ 350 | The contained result is ``Err``, so return the result of applying 351 | ``op`` to the error value. 352 | """ 353 | return op(self._value) 354 | 355 | def unwrap_or_raise(self, e: Type[TBE]) -> NoReturn: 356 | """ 357 | The contained result is ``Err``, so raise the exception with the value. 358 | """ 359 | raise e(self._value) 360 | 361 | def map(self, op: object) -> Err[E]: 362 | """ 363 | Return `Err` with the same value 364 | """ 365 | return self 366 | 367 | async def map_async(self, op: object) -> Err[E]: 368 | """ 369 | The contained result is `Ok`, so return the result of `op` with the 370 | original value passed in 371 | """ 372 | return self 373 | 374 | def map_or(self, default: U, op: object) -> U: 375 | """ 376 | Return the default value 377 | """ 378 | return default 379 | 380 | def map_or_else(self, default_op: Callable[[], U], op: object) -> U: 381 | """ 382 | Return the result of the default operation 383 | """ 384 | return default_op() 385 | 386 | def map_err(self, op: Callable[[E], F]) -> Err[F]: 387 | """ 388 | The contained result is `Err`, so return `Err` with original error mapped to 389 | a new value using the passed in function. 390 | """ 391 | return Err(op(self._value)) 392 | 393 | def and_then(self, op: object) -> Err[E]: 394 | """ 395 | The contained result is `Err`, so return `Err` with the original value 396 | """ 397 | return self 398 | 399 | async def and_then_async(self, op: object) -> Err[E]: 400 | """ 401 | The contained result is `Err`, so return `Err` with the original value 402 | """ 403 | return self 404 | 405 | def or_else(self, op: Callable[[E], Result[T, F]]) -> Result[T, F]: 406 | """ 407 | The contained result is `Err`, so return the result of `op` with the 408 | original value passed in 409 | """ 410 | return op(self._value) 411 | 412 | def inspect(self, op: Callable[[T], Any]) -> Result[T, E]: 413 | """ 414 | Calls a function with the contained value if `Ok`. Returns the original result. 415 | """ 416 | return self 417 | 418 | def inspect_err(self, op: Callable[[E], Any]) -> Result[T, E]: 419 | """ 420 | Calls a function with the contained value if `Err`. Returns the original result. 421 | """ 422 | op(self._value) 423 | return self 424 | 425 | 426 | # define Result as a generic type alias for use 427 | # in type annotations 428 | """ 429 | A simple `Result` type inspired by Rust. 430 | Not all methods (https://doc.rust-lang.org/std/result/enum.Result.html) 431 | have been implemented, only the ones that make sense in the Python context. 432 | """ 433 | Result: TypeAlias = Union[Ok[T], Err[E]] 434 | 435 | """ 436 | A type to use in `isinstance` checks. 437 | This is purely for convenience sake, as you could also just write `isinstance(res, (Ok, Err)) 438 | """ 439 | OkErr: Final = (Ok, Err) 440 | 441 | 442 | class UnwrapError(Exception): 443 | """ 444 | Exception raised from ``.unwrap_<...>`` and ``.expect_<...>`` calls. 445 | 446 | The original ``Result`` can be accessed via the ``.result`` attribute, but 447 | this is not intended for regular use, as type information is lost: 448 | ``UnwrapError`` doesn't know about both ``T`` and ``E``, since it's raised 449 | from ``Ok()`` or ``Err()`` which only knows about either ``T`` or ``E``, 450 | not both. 451 | """ 452 | 453 | _result: Result[object, object] 454 | 455 | def __init__(self, result: Result[object, object], message: str) -> None: 456 | self._result = result 457 | super().__init__(message) 458 | 459 | @property 460 | def result(self) -> Result[Any, Any]: 461 | """ 462 | Returns the original result. 463 | """ 464 | return self._result 465 | 466 | 467 | def as_result( 468 | *exceptions: Type[TBE], 469 | ) -> Callable[[Callable[P, R]], Callable[P, Result[R, TBE]]]: 470 | """ 471 | Make a decorator to turn a function into one that returns a ``Result``. 472 | 473 | Regular return values are turned into ``Ok(return_value)``. Raised 474 | exceptions of the specified exception type(s) are turned into ``Err(exc)``. 475 | """ 476 | if not exceptions or not all( 477 | inspect.isclass(exception) and issubclass(exception, BaseException) 478 | for exception in exceptions 479 | ): 480 | raise TypeError("as_result() requires one or more exception types") 481 | 482 | def decorator(f: Callable[P, R]) -> Callable[P, Result[R, TBE]]: 483 | """ 484 | Decorator to turn a function into one that returns a ``Result``. 485 | """ 486 | 487 | @functools.wraps(f) 488 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[R, TBE]: 489 | try: 490 | return Ok(f(*args, **kwargs)) 491 | except exceptions as exc: 492 | return Err(exc) 493 | 494 | return wrapper 495 | 496 | return decorator 497 | 498 | 499 | def as_async_result( 500 | *exceptions: Type[TBE], 501 | ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[Result[R, TBE]]]]: 502 | """ 503 | Make a decorator to turn an async function into one that returns a ``Result``. 504 | Regular return values are turned into ``Ok(return_value)``. Raised 505 | exceptions of the specified exception type(s) are turned into ``Err(exc)``. 506 | """ 507 | if not exceptions or not all( 508 | inspect.isclass(exception) and issubclass(exception, BaseException) 509 | for exception in exceptions 510 | ): 511 | raise TypeError("as_result() requires one or more exception types") 512 | 513 | def decorator( 514 | f: Callable[P, Awaitable[R]] 515 | ) -> Callable[P, Awaitable[Result[R, TBE]]]: 516 | """ 517 | Decorator to turn a function into one that returns a ``Result``. 518 | """ 519 | 520 | @functools.wraps(f) 521 | async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[R, TBE]: 522 | try: 523 | return Ok(await f(*args, **kwargs)) 524 | except exceptions as exc: 525 | return Err(exc) 526 | 527 | return async_wrapper 528 | 529 | return decorator 530 | 531 | 532 | def is_ok(result: Result[T, E]) -> TypeIs[Ok[T]]: 533 | """A type guard to check if a result is an Ok 534 | 535 | Usage: 536 | 537 | ``` python 538 | r: Result[int, str] = get_a_result() 539 | if is_ok(r): 540 | r # r is of type Ok[int] 541 | elif is_err(r): 542 | r # r is of type Err[str] 543 | ``` 544 | 545 | """ 546 | return result.is_ok() 547 | 548 | 549 | def is_err(result: Result[T, E]) -> TypeIs[Err[E]]: 550 | """A type guard to check if a result is an Err 551 | 552 | Usage: 553 | 554 | ``` python 555 | r: Result[int, str] = get_a_result() 556 | if is_ok(r): 557 | r # r is of type Ok[int] 558 | elif is_err(r): 559 | r # r is of type Err[str] 560 | ``` 561 | 562 | """ 563 | return result.is_err() 564 | 565 | 566 | def do(gen: Generator[Result[T, E], None, None]) -> Result[T, E]: 567 | """Do notation for Result (syntactic sugar for sequence of `and_then()` calls). 568 | 569 | 570 | Usage: 571 | 572 | ``` rust 573 | // This is similar to 574 | use do_notation::m; 575 | let final_result = m! { 576 | x <- Ok("hello"); 577 | y <- Ok(True); 578 | Ok(len(x) + int(y) + 0.5) 579 | }; 580 | ``` 581 | 582 | ``` rust 583 | final_result: Result[float, int] = do( 584 | Ok(len(x) + int(y) + 0.5) 585 | for x in Ok("hello") 586 | for y in Ok(True) 587 | ) 588 | ``` 589 | 590 | NOTE: If you exclude the type annotation e.g. `Result[float, int]` 591 | your type checker might be unable to infer the return type. 592 | To avoid an error, you might need to help it with the type hint. 593 | """ 594 | try: 595 | return next(gen) 596 | except DoException as e: 597 | out: Err[E] = e.err # type: ignore 598 | return out 599 | except TypeError as te: 600 | # Turn this into a more helpful error message. 601 | # Python has strange rules involving turning generators involving `await` 602 | # into async generators, so we want to make sure to help the user clearly. 603 | if "'async_generator' object is not an iterator" in str(te): 604 | raise TypeError( 605 | "Got async_generator but expected generator." 606 | "See the section on do notation in the README." 607 | ) 608 | raise te 609 | 610 | 611 | async def do_async( 612 | gen: Union[Generator[Result[T, E], None, None], AsyncGenerator[Result[T, E], None]] 613 | ) -> Result[T, E]: 614 | """Async version of do. Example: 615 | 616 | ``` python 617 | final_result: Result[float, int] = await do_async( 618 | Ok(len(x) + int(y) + z) 619 | for x in await get_async_result_1() 620 | for y in await get_async_result_2() 621 | for z in get_sync_result_3() 622 | ) 623 | ``` 624 | 625 | NOTE: Python makes generators async in a counter-intuitive way. 626 | 627 | ``` python 628 | # This is a regular generator: 629 | async def foo(): ... 630 | do(Ok(1) for x in await foo()) 631 | ``` 632 | 633 | ``` python 634 | # But this is an async generator: 635 | async def foo(): ... 636 | async def bar(): ... 637 | do( 638 | Ok(1) 639 | for x in await foo() 640 | for y in await bar() 641 | ) 642 | ``` 643 | 644 | We let users try to use regular `do()`, which works in some cases 645 | of awaiting async values. If we hit a case like above, we raise 646 | an exception telling the user to use `do_async()` instead. 647 | See `do()`. 648 | 649 | However, for better usability, it's better for `do_async()` to also accept 650 | regular generators, as you get in the first case: 651 | 652 | ``` python 653 | async def foo(): ... 654 | do(Ok(1) for x in await foo()) 655 | ``` 656 | 657 | Furthermore, neither mypy nor pyright can infer that the second case is 658 | actually an async generator, so we cannot annotate `do_async()` 659 | as accepting only an async generator. This is additional motivation 660 | to accept either. 661 | """ 662 | try: 663 | if isinstance(gen, AsyncGenerator): 664 | return await gen.__anext__() 665 | else: 666 | return next(gen) 667 | except DoException as e: 668 | out: Err[E] = e.err # type: ignore 669 | return out 670 | -------------------------------------------------------------------------------- /tests/test_pattern_matching.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from result import Err, Ok, Result 4 | 5 | 6 | def test_pattern_matching_on_ok_type() -> None: 7 | """ 8 | Pattern matching on ``Ok()`` matches the contained value. 9 | """ 10 | o: Result[str, int] = Ok("yay") 11 | match o: 12 | case Ok(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: Result[int, str] = Err("nay") 24 | match n: 25 | case Err(value): 26 | reached = True 27 | 28 | assert value == "nay" 29 | assert reached 30 | -------------------------------------------------------------------------------- /tests/test_result.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Callable 4 | 5 | import pytest 6 | 7 | from result import Err, Ok, OkErr, Result, UnwrapError, as_async_result, as_result 8 | 9 | 10 | def test_ok_factories() -> None: 11 | instance = Ok(1) 12 | assert instance._value == 1 13 | assert instance.is_ok() is True 14 | 15 | 16 | def test_err_factories() -> None: 17 | instance = Err(2) 18 | assert instance._value == 2 19 | assert instance.is_err() is True 20 | 21 | 22 | def test_eq() -> None: 23 | assert Ok(1) == Ok(1) 24 | assert Err(1) == Err(1) 25 | assert Ok(1) != Err(1) 26 | assert Ok(1) != Ok(2) 27 | assert Err(1) != Err(2) 28 | assert not (Ok(1) != Ok(1)) 29 | assert Ok(1) != "abc" 30 | assert Ok("0") != Ok(0) 31 | 32 | 33 | def test_hash() -> None: 34 | assert len({Ok(1), Err("2"), Ok(1), Err("2")}) == 2 35 | assert len({Ok(1), Ok(2)}) == 2 36 | assert len({Ok("a"), Err("a")}) == 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 = Ok(123) 44 | n = Err(-1) 45 | 46 | assert repr(o) == "Ok(123)" 47 | assert o == eval(repr(o)) 48 | 49 | assert repr(n) == "Err(-1)" 50 | assert n == eval(repr(n)) 51 | 52 | 53 | def test_ok_value() -> None: 54 | res = Ok('haha') 55 | assert res.ok_value == 'haha' 56 | 57 | 58 | def test_err_value() -> None: 59 | res = Err('haha') 60 | assert res.err_value == 'haha' 61 | 62 | 63 | def test_ok() -> None: 64 | res = Ok('haha') 65 | assert res.is_ok() is True 66 | assert res.is_err() is False 67 | assert res.ok_value == 'haha' 68 | 69 | 70 | def test_err() -> None: 71 | res = Err(':(') 72 | assert res.is_ok() is False 73 | assert res.is_err() is True 74 | assert res.err_value == ':(' 75 | 76 | 77 | def test_err_value_is_exception() -> None: 78 | res = Err(ValueError("Some Error")) 79 | assert res.is_ok() is False 80 | assert res.is_err() is True 81 | 82 | with pytest.raises(UnwrapError): 83 | res.unwrap() 84 | 85 | try: 86 | res.unwrap() 87 | except UnwrapError as e: 88 | cause = e.__cause__ 89 | assert isinstance(cause, ValueError) 90 | 91 | 92 | def test_ok_method() -> None: 93 | o = Ok('yay') 94 | n = Err('nay') 95 | assert o.ok() == 'yay' 96 | assert n.ok() is None # type: ignore[func-returns-value] 97 | 98 | 99 | def test_err_method() -> None: 100 | o = Ok('yay') 101 | n = Err('nay') 102 | assert o.err() is None # type: ignore[func-returns-value] 103 | assert n.err() == 'nay' 104 | 105 | 106 | def test_expect() -> None: 107 | o = Ok('yay') 108 | n = Err('nay') 109 | assert o.expect('failure') == 'yay' 110 | with pytest.raises(UnwrapError): 111 | n.expect('failure') 112 | 113 | 114 | def test_expect_err() -> None: 115 | o = Ok('yay') 116 | n = Err('nay') 117 | assert n.expect_err('hello') == 'nay' 118 | with pytest.raises(UnwrapError): 119 | o.expect_err('hello') 120 | 121 | 122 | def test_unwrap() -> None: 123 | o = Ok('yay') 124 | n = Err('nay') 125 | assert o.unwrap() == 'yay' 126 | with pytest.raises(UnwrapError): 127 | n.unwrap() 128 | 129 | 130 | def test_unwrap_err() -> None: 131 | o = Ok('yay') 132 | n = Err('nay') 133 | assert n.unwrap_err() == 'nay' 134 | with pytest.raises(UnwrapError): 135 | o.unwrap_err() 136 | 137 | 138 | def test_unwrap_or() -> None: 139 | o = Ok('yay') 140 | n = Err('nay') 141 | assert o.unwrap_or('some_default') == 'yay' 142 | assert n.unwrap_or('another_default') == 'another_default' 143 | 144 | 145 | def test_unwrap_or_else() -> None: 146 | o = Ok('yay') 147 | n = Err('nay') 148 | assert o.unwrap_or_else(str.upper) == 'yay' 149 | assert n.unwrap_or_else(str.upper) == 'NAY' 150 | 151 | 152 | def test_unwrap_or_raise() -> None: 153 | o = Ok('yay') 154 | n = Err('nay') 155 | assert o.unwrap_or_raise(ValueError) == 'yay' 156 | with pytest.raises(ValueError) as exc_info: 157 | n.unwrap_or_raise(ValueError) 158 | assert exc_info.value.args == ('nay',) 159 | 160 | 161 | def test_map() -> None: 162 | o = Ok('yay') 163 | n = Err('nay') 164 | assert o.map(str.upper).ok() == 'YAY' 165 | assert n.map(str.upper).err() == 'nay' 166 | 167 | num = Ok(3) 168 | errnum = Err(2) 169 | assert num.map(str).ok() == '3' 170 | assert errnum.map(str).err() == 2 171 | 172 | 173 | def test_map_or() -> None: 174 | o = Ok('yay') 175 | n = Err('nay') 176 | assert o.map_or('hay', str.upper) == 'YAY' 177 | assert n.map_or('hay', str.upper) == 'hay' 178 | 179 | num = Ok(3) 180 | errnum = Err(2) 181 | assert num.map_or('-1', str) == '3' 182 | assert errnum.map_or('-1', str) == '-1' 183 | 184 | 185 | def test_map_or_else() -> None: 186 | o = Ok('yay') 187 | n = Err('nay') 188 | assert o.map_or_else(lambda: 'hay', str.upper) == 'YAY' 189 | assert n.map_or_else(lambda: 'hay', str.upper) == 'hay' 190 | 191 | num = Ok(3) 192 | errnum = Err(2) 193 | assert num.map_or_else(lambda: '-1', str) == '3' 194 | assert errnum.map_or_else(lambda: '-1', str) == '-1' 195 | 196 | 197 | def test_map_err() -> None: 198 | o = Ok('yay') 199 | n = Err('nay') 200 | assert o.map_err(str.upper).ok() == 'yay' 201 | assert n.map_err(str.upper).err() == 'NAY' 202 | 203 | 204 | def test_and_then() -> None: 205 | assert Ok(2).and_then(sq).and_then(sq).ok() == 16 206 | assert Ok(2).and_then(sq).and_then(to_err).err() == 4 207 | assert Ok(2).and_then(to_err).and_then(sq).err() == 2 208 | assert Err(3).and_then(sq).and_then(sq).err() == 3 209 | 210 | assert Ok(2).and_then(sq_lambda).and_then(sq_lambda).ok() == 16 211 | assert Ok(2).and_then(sq_lambda).and_then(to_err_lambda).err() == 4 212 | assert Ok(2).and_then(to_err_lambda).and_then(sq_lambda).err() == 2 213 | assert Err(3).and_then(sq_lambda).and_then(sq_lambda).err() == 3 214 | 215 | 216 | def test_inspect() -> None: 217 | oks: list[int] = [] 218 | add_to_oks: Callable[[int], None] = lambda x: oks.append(x) 219 | 220 | assert Ok(2).inspect(add_to_oks) == Ok(2) 221 | assert Err("e").inspect(add_to_oks) == Err("e") 222 | assert oks == [2] 223 | 224 | 225 | def test_inspect_err() -> None: 226 | errs: list[str] = [] 227 | add_to_errs: Callable[[str], None] = lambda x: errs.append(x) 228 | 229 | assert Ok(2).inspect_err(add_to_errs) == Ok(2) 230 | assert Err("e").inspect_err(add_to_errs) == Err("e") 231 | assert errs == ["e"] 232 | 233 | 234 | def test_inspect_regular_fn() -> None: 235 | oks: list[str] = [] 236 | 237 | def _add_to_oks(x: str) -> str: 238 | oks.append(x) 239 | return x + x 240 | 241 | assert Ok("hello").inspect(_add_to_oks) == Ok("hello") 242 | assert Err("error").inspect(_add_to_oks) == Err("error") 243 | assert oks == ["hello"] 244 | 245 | 246 | @pytest.mark.asyncio 247 | async def test_and_then_async() -> None: 248 | assert (await (await Ok(2).and_then_async(sq_async)).and_then_async(sq_async)).ok() == 16 249 | assert (await (await Ok(2).and_then_async(sq_async)).and_then_async(to_err_async)).err() == 4 250 | assert ( 251 | await (await Ok(2).and_then_async(to_err_async)).and_then_async(to_err_async) 252 | ).err() == 2 253 | assert ( 254 | await (await Err(3).and_then_async(sq_async)).and_then_async(sq_async) 255 | ).err() == 3 256 | 257 | 258 | @pytest.mark.asyncio 259 | async def test_map_async() -> None: 260 | async def str_upper_async(s: str) -> str: 261 | return s.upper() 262 | 263 | async def str_async(x: int) -> str: 264 | return str(x) 265 | 266 | o = Ok('yay') 267 | n = Err('nay') 268 | assert (await o.map_async(str_upper_async)).ok() == 'YAY' 269 | assert (await n.map_async(str_upper_async)).err() == 'nay' 270 | 271 | num = Ok(3) 272 | errnum = Err(2) 273 | assert (await num.map_async(str_async)).ok() == '3' 274 | assert (await errnum.map_async(str_async)).err() == 2 275 | 276 | 277 | def test_or_else() -> None: 278 | assert Ok(2).or_else(sq).or_else(sq).ok() == 2 279 | assert Ok(2).or_else(to_err).or_else(sq).ok() == 2 280 | assert Err(3).or_else(sq).or_else(to_err).ok() == 9 281 | assert Err(3).or_else(to_err).or_else(to_err).err() == 3 282 | 283 | assert Ok(2).or_else(sq_lambda).or_else(sq).ok() == 2 284 | assert Ok(2).or_else(to_err_lambda).or_else(sq_lambda).ok() == 2 285 | assert Err(3).or_else(sq_lambda).or_else(to_err_lambda).ok() == 9 286 | assert Err(3).or_else(to_err_lambda).or_else(to_err_lambda).err() == 3 287 | 288 | 289 | def test_isinstance_result_type() -> None: 290 | o = Ok('yay') 291 | n = Err('nay') 292 | assert isinstance(o, OkErr) 293 | assert isinstance(n, OkErr) 294 | assert not isinstance(1, OkErr) 295 | 296 | 297 | def test_error_context() -> None: 298 | n = Err('nay') 299 | with pytest.raises(UnwrapError) as exc_info: 300 | n.unwrap() 301 | exc = exc_info.value 302 | assert exc.result is n 303 | 304 | 305 | def test_slots() -> None: 306 | """ 307 | Ok and Err have slots, so assigning arbitrary attributes fails. 308 | """ 309 | o = Ok('yay') 310 | n = Err('nay') 311 | with pytest.raises(AttributeError): 312 | o.some_arbitrary_attribute = 1 # type: ignore[attr-defined] 313 | with pytest.raises(AttributeError): 314 | n.some_arbitrary_attribute = 1 # type: ignore[attr-defined] 315 | 316 | 317 | def test_as_result() -> None: 318 | """ 319 | ``as_result()`` turns functions into ones that return a ``Result``. 320 | """ 321 | 322 | @as_result(ValueError) 323 | def good(value: int) -> int: 324 | return value 325 | 326 | @as_result(IndexError, ValueError) 327 | def bad(value: int) -> int: 328 | raise ValueError 329 | 330 | good_result = good(123) 331 | bad_result = bad(123) 332 | 333 | assert isinstance(good_result, Ok) 334 | assert good_result.unwrap() == 123 335 | assert isinstance(bad_result, Err) 336 | assert isinstance(bad_result.unwrap_err(), ValueError) 337 | 338 | 339 | def test_as_result_other_exception() -> None: 340 | """ 341 | ``as_result()`` only catches the specified exceptions. 342 | """ 343 | 344 | @as_result(ValueError) 345 | def f() -> int: 346 | raise IndexError 347 | 348 | with pytest.raises(IndexError): 349 | f() 350 | 351 | 352 | def test_as_result_invalid_usage() -> None: 353 | """ 354 | Invalid use of ``as_result()`` raises reasonable errors. 355 | """ 356 | message = "requires one or more exception types" 357 | 358 | with pytest.raises(TypeError, match=message): 359 | 360 | @as_result() # No exception types specified 361 | def f() -> int: 362 | return 1 363 | 364 | with pytest.raises(TypeError, match=message): 365 | 366 | @as_result("not an exception type") # type: ignore[arg-type] 367 | def g() -> int: 368 | return 1 369 | 370 | 371 | def test_as_result_type_checking() -> None: 372 | """ 373 | The ``as_result()`` is a signature-preserving decorator. 374 | """ 375 | 376 | @as_result(ValueError) 377 | def f(a: int) -> int: 378 | return a 379 | 380 | res: Result[int, ValueError] 381 | res = f(123) # No mypy error here. 382 | assert res.ok() == 123 383 | 384 | 385 | @pytest.mark.asyncio 386 | async def test_as_async_result() -> None: 387 | """ 388 | ``as_async_result()`` turns functions into ones that return a ``Result``. 389 | """ 390 | 391 | @as_async_result(ValueError) 392 | async def good(value: int) -> int: 393 | return value 394 | 395 | @as_async_result(IndexError, ValueError) 396 | async def bad(value: int) -> int: 397 | raise ValueError 398 | 399 | good_result = await good(123) 400 | bad_result = await bad(123) 401 | 402 | assert isinstance(good_result, Ok) 403 | assert good_result.unwrap() == 123 404 | assert isinstance(bad_result, Err) 405 | assert isinstance(bad_result.unwrap_err(), ValueError) 406 | 407 | 408 | def sq(i: int) -> Result[int, int]: 409 | return Ok(i * i) 410 | 411 | 412 | async def sq_async(i: int) -> Result[int, int]: 413 | return Ok(i * i) 414 | 415 | 416 | def to_err(i: int) -> Result[int, int]: 417 | return Err(i) 418 | 419 | 420 | async def to_err_async(i: int) -> Result[int, int]: 421 | return Err(i) 422 | 423 | 424 | # Lambda versions of the same functions, just for test/type coverage 425 | sq_lambda: Callable[[int], Result[int, int]] = lambda i: Ok(i * i) 426 | to_err_lambda: Callable[[int], Result[int, int]] = lambda i: Err(i) 427 | -------------------------------------------------------------------------------- /tests/test_result_do.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | import pytest 5 | 6 | from result import Err, Ok, Result, do, do_async 7 | 8 | 9 | def test_result_do_general() -> None: 10 | def resx(is_suc: bool) -> Result[str, int]: 11 | return Ok("hello") if is_suc else Err(1) 12 | 13 | def resy(is_suc: bool) -> Result[bool, int]: 14 | return Ok(True) if is_suc else Err(2) 15 | 16 | def _get_output(is_suc1: bool, is_suc2: bool) -> Result[float, int]: 17 | out: Result[float, int] = do( 18 | Ok(len(x) + int(y) + 0.5) for x in resx(is_suc1) for y in resy(is_suc2) 19 | ) 20 | return out 21 | 22 | assert _get_output(True, True) == Ok(6.5) 23 | assert _get_output(True, False) == Err(2) 24 | assert _get_output(False, True) == Err(1) 25 | assert _get_output(False, False) == Err(1) 26 | 27 | def _get_output_return_immediately( 28 | is_suc1: bool, is_suc2: bool 29 | ) -> Result[float, int]: 30 | return do( 31 | Ok(len(x) + int(y) + 0.5) for x in resx(is_suc1) for y in resy(is_suc2) 32 | ) 33 | 34 | assert _get_output_return_immediately(True, True) == Ok(6.5) 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_result_do_general_with_async_values() -> None: 39 | # Asyncio works with regular `do()` as long as you await 40 | # the async calls outside the `do()` expression. 41 | # This causes the generator to be a regular (not async) generator. 42 | async def aget_resx(is_suc: bool) -> Result[str, int]: 43 | return Ok("hello") if is_suc else Err(1) 44 | 45 | async def aget_resy(is_suc: bool) -> Result[bool, int]: 46 | return Ok(True) if is_suc else Err(2) 47 | 48 | async def _aget_output(is_suc1: bool, is_suc2: bool) -> Result[float, int]: 49 | resx, resy = await aget_resx(is_suc1), await aget_resy(is_suc2) 50 | out: Result[float, int] = do( 51 | Ok(len(x) + int(y) + 0.5) for x in resx for y in resy 52 | ) 53 | return out 54 | 55 | assert await _aget_output(True, True) == Ok(6.5) 56 | assert await _aget_output(True, False) == Err(2) 57 | assert await _aget_output(False, True) == Err(1) 58 | assert await _aget_output(False, False) == Err(1) 59 | 60 | 61 | @pytest.mark.asyncio 62 | async def test_result_do_async_one_value() -> None: 63 | """This is a strange case where Python creates a regular 64 | (non async) generator despite an `await` inside the generator expression. 65 | For convenience, although this works with regular `do()`, we want to support this 66 | with `do_async()` as well.""" 67 | 68 | async def aget_resx(is_suc: bool) -> Result[str, int]: 69 | return Ok("hello") if is_suc else Err(1) 70 | 71 | def get_resz(is_suc: bool) -> Result[float, int]: 72 | return Ok(0.5) if is_suc else Err(3) 73 | 74 | assert await do_async(Ok(len(x)) for x in await aget_resx(True)) == Ok(5) 75 | assert await do_async(Ok(len(x)) for x in await aget_resx(False)) == Err(1) 76 | 77 | async def _aget_output(is_suc1: bool, is_suc3: bool) -> Result[float, int]: 78 | return await do_async( 79 | Ok(len(x) + z) for x in await aget_resx(is_suc1) for z in get_resz(is_suc3) 80 | ) 81 | 82 | assert await _aget_output(True, True) == Ok(5.5) 83 | assert await _aget_output(True, False) == Err(3) 84 | assert await _aget_output(False, True) == Err(1) 85 | assert await _aget_output(False, False) == Err(1) 86 | 87 | 88 | @pytest.mark.asyncio 89 | async def test_result_do_async_general() -> None: 90 | async def aget_resx(is_suc: bool) -> Result[str, int]: 91 | return Ok("hello") if is_suc else Err(1) 92 | 93 | async def aget_resy(is_suc: bool) -> Result[bool, int]: 94 | return Ok(True) if is_suc else Err(2) 95 | 96 | def get_resz(is_suc: bool) -> Result[float, int]: 97 | return Ok(0.5) if is_suc else Err(3) 98 | 99 | async def _aget_output( 100 | is_suc1: bool, is_suc2: bool, is_suc3: bool 101 | ) -> Result[float, int]: 102 | out: Result[float, int] = await do_async( 103 | Ok(len(x) + int(y) + z) 104 | for x in await aget_resx(is_suc1) 105 | for y in await aget_resy(is_suc2) 106 | for z in get_resz(is_suc3) 107 | ) 108 | return out 109 | 110 | assert await _aget_output(True, True, True) == Ok(6.5) 111 | assert await _aget_output(True, False, True) == Err(2) 112 | assert await _aget_output(False, True, True) == Err(1) 113 | assert await _aget_output(False, False, True) == Err(1) 114 | 115 | assert await _aget_output(True, True, False) == Err(3) 116 | assert await _aget_output(True, False, False) == Err(2) 117 | assert await _aget_output(False, True, False) == Err(1) 118 | assert await _aget_output(False, False, False) == Err(1) 119 | 120 | async def _aget_output_return_immediately( 121 | is_suc1: bool, is_suc2: bool, is_suc3: bool 122 | ) -> Result[float, int]: 123 | return await do_async( 124 | Ok(len(x) + int(y) + z) 125 | for x in await aget_resx(is_suc1) 126 | for y in await aget_resy(is_suc2) 127 | for z in get_resz(is_suc3) 128 | ) 129 | 130 | assert await _aget_output_return_immediately(True, True, True) == Ok(6.5) 131 | 132 | 133 | @pytest.mark.asyncio 134 | async def test_result_do_async_further_processing() -> None: 135 | async def aget_resx(is_suc: bool) -> Result[str, int]: 136 | return Ok("hello") if is_suc else Err(1) 137 | 138 | async def aget_resy(is_suc: bool) -> Result[bool, int]: 139 | return Ok(True) if is_suc else Err(2) 140 | 141 | def get_resz(is_suc: bool) -> Result[float, int]: 142 | return Ok(0.5) if is_suc else Err(3) 143 | 144 | async def process_xyz(x: str, y: bool, z: float) -> Result[float, int]: 145 | return Ok(len(x) + int(y) + z) 146 | 147 | async def _aget_output( 148 | is_suc1: bool, is_suc2: bool, is_suc3: bool 149 | ) -> Result[float, int]: 150 | out: Result[float, int] = await do_async( 151 | Ok(w) 152 | for x in await aget_resx(is_suc1) 153 | for y in await aget_resy(is_suc2) 154 | for z in get_resz(is_suc3) 155 | for w in await process_xyz(x, y, z) 156 | ) 157 | return out 158 | 159 | assert await _aget_output(True, True, True) == Ok(6.5) 160 | assert await _aget_output(True, False, True) == Err(2) 161 | assert await _aget_output(False, True, True) == Err(1) 162 | assert await _aget_output(False, False, True) == Err(1) 163 | 164 | assert await _aget_output(True, True, False) == Err(3) 165 | assert await _aget_output(True, False, False) == Err(2) 166 | assert await _aget_output(False, True, False) == Err(1) 167 | assert await _aget_output(False, False, False) == Err(1) 168 | 169 | 170 | @pytest.mark.asyncio 171 | async def test_result_do_general_with_async_values_inline_error() -> None: 172 | """ 173 | Due to subtle behavior, `do()` works in certain cases involving async 174 | calls but not others. We surface a more helpful error to the user 175 | in cases where it doesn't work indicating to use `do_async()` instead. 176 | Contrast this with `test_result_do_general_with_async_values()` 177 | in which using `do()` works with async functions as long as 178 | their return values are resolved outside the `do()` expression. 179 | """ 180 | 181 | async def aget_resx(is_suc: bool) -> Result[str, int]: 182 | return Ok("hello") if is_suc else Err(1) 183 | 184 | async def aget_resy(is_suc: bool) -> Result[bool, int]: 185 | return Ok(True) if is_suc else Err(2) 186 | 187 | def get_resz(is_suc: bool) -> Result[float, int]: 188 | return Ok(0.5) if is_suc else Err(3) 189 | 190 | with pytest.raises(TypeError) as excinfo: 191 | do( 192 | Ok(len(x) + int(y) + z) 193 | for x in await aget_resx(True) 194 | for y in await aget_resy(True) 195 | for z in get_resz(True) 196 | ) 197 | 198 | assert ( 199 | "Got async_generator but expected generator.See the section on do notation in the README." 200 | ) in excinfo.value.args[0] 201 | 202 | 203 | @pytest.mark.asyncio 204 | async def test_result_do_async_swap_order() -> None: 205 | def foo() -> Result[int, str]: 206 | return Ok(1) 207 | 208 | async def bar() -> Result[int, str]: 209 | return Ok(2) 210 | 211 | result1: Result[int, str] = await do_async( 212 | Ok(x + y) 213 | # x first 214 | for x in foo() 215 | # then y 216 | for y in await bar() 217 | ) 218 | 219 | result2: Result[int, str] = await do_async( 220 | Ok(x + y) 221 | # y first 222 | for y in await bar() 223 | # then x 224 | for x in foo() 225 | ) 226 | 227 | assert result1 == result2 == Ok(3) 228 | -------------------------------------------------------------------------------- /tests/type_checking/test_result.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # reveal_type(res3) # N: Revealed type is "result.result.Err[builtins.int]" 3 | - case: failure_lash 4 | disable_cache: false 5 | main: | 6 | from typing import Callable, List, Optional 7 | 8 | from result import Result, Ok, Err 9 | 10 | 11 | res1: Result[str, int] = Ok('hello') 12 | reveal_type(res1) # N: Revealed type is "Union[result.result.Ok[builtins.str], result.result.Err[builtins.int]]" 13 | if isinstance(res1, Ok): 14 | ok: Ok[str] = res1 15 | reveal_type(ok) # N: Revealed type is "result.result.Ok[builtins.str]" 16 | okValue: str = res1.ok() 17 | reveal_type(okValue) # 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 | err: Err[int] = res1 22 | reveal_type(err) # N: Revealed type is "result.result.Err[builtins.int]" 23 | errValue: int = err.err() 24 | reveal_type(errValue) # N: Revealed type is "builtins.int" 25 | mapped_to_list: Optional[List[int]] = res1.map_err(lambda e: [e]).err() 26 | reveal_type(mapped_to_list) # N: Revealed type is "Union[builtins.list[builtins.int], None]" 27 | 28 | # Test constructor functions 29 | res2 = Ok(42) 30 | reveal_type(res2) # N: Revealed type is "result.result.Ok[builtins.int]" 31 | res3 = Err(1) 32 | reveal_type(res3) # N: Revealed type is "result.result.Err[builtins.int]" 33 | 34 | res4 = Ok(4) 35 | add1: Callable[[int], Result[int, str]] = lambda i: Ok(i + 1) 36 | toint: Callable[[str], Result[int, str]] = lambda i: Ok(int(i)) 37 | res5 = res4.and_then(add1) 38 | reveal_type(res5) # N: Revealed type is "Union[result.result.Ok[builtins.int], result.result.Err[builtins.str]]" 39 | res6 = res4.or_else(toint) 40 | reveal_type(res6) # N: Revealed type is "result.result.Ok[builtins.int]" 41 | 42 | - case: covariance 43 | disable_cache: false 44 | main: | 45 | from result import Result, Ok, Err 46 | 47 | ok_int: Ok[int] = Ok(42) 48 | ok_float: Ok[float] = ok_int 49 | ok_int = ok_float # E: Incompatible types in assignment (expression has type "Ok[float]", variable has type "Ok[int]") [assignment] 50 | 51 | err_type: Err[TypeError] = Err(TypeError("foo")) 52 | err_exc: Err[Exception] = err_type 53 | err_type = err_exc # E: Incompatible types in assignment (expression has type "Err[Exception]", variable has type "Err[TypeError]") [assignment] 54 | 55 | result_int_type: Result[int, TypeError] = ok_int or err_type 56 | result_float_exc: Result[float, Exception] = result_int_type 57 | result_int_type = result_float_exc # E: Incompatible types in assignment (expression has type "Ok[float] | Err[Exception]", variable has type "Ok[int] | Err[TypeError]") [assignment] 58 | 59 | - case: map_ok_err 60 | disable_cache: false 61 | main: | 62 | from result import Err, Ok 63 | 64 | o = Ok("42") 65 | reveal_type(o.map(int)) # N: Revealed type is "result.result.Ok[builtins.int]" 66 | reveal_type(o.map_err(int)) # N: Revealed type is "result.result.Ok[builtins.str]" 67 | 68 | e = Err("42") 69 | reveal_type(e.map(int)) # N: Revealed type is "result.result.Err[builtins.str]" 70 | reveal_type(e.map_err(int)) # N: Revealed type is "result.result.Err[builtins.int]" 71 | 72 | - case: map_result 73 | disable_cache: false 74 | main: | 75 | from result import Result, Ok 76 | 77 | greeting_res: Result[str, ValueError] = Ok("Hello") 78 | 79 | personalized_greeting_res = greeting_res.map(lambda g: f"{g}, John") 80 | reveal_type(personalized_greeting_res) # N: Revealed type is "Union[result.result.Ok[builtins.str], result.result.Err[builtins.ValueError]]" 81 | 82 | personalized_greeting = personalized_greeting_res.ok() 83 | reveal_type(personalized_greeting) # N: Revealed type is "Union[builtins.str, None]" 84 | 85 | - case: map_result 86 | disable_cache: false 87 | main: | 88 | from result import Result, Ok, Err, is_ok, is_err 89 | 90 | res1: Result[int, str] = Ok(1) 91 | if is_ok(res1): 92 | reveal_type(res1) # N: Revealed type is "result.result.Ok[builtins.int]" 93 | else: 94 | reveal_type(res1) # N: Revealed type is "result.result.Err[builtins.str]" 95 | 96 | res2: Result[int, str] = Err("error") 97 | if is_err(res2): 98 | reveal_type(res2) # N: Revealed type is "result.result.Err[builtins.str]" 99 | else: 100 | reveal_type(res2) # N: Revealed type is "result.result.Ok[builtins.int]" 101 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py312,py311,py310,py39,py38 3 | 4 | [testenv] 5 | deps = -rrequirements-dev.txt 6 | commands = pytest {posargs} 7 | 8 | [testenv:py310] 9 | deps = -rrequirements-dev.txt 10 | commands = 11 | pytest {posargs} 12 | pytest {posargs} tests/test_pattern_matching.py 13 | --------------------------------------------------------------------------------