├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .readthedocs.yml ├── .style.yapf ├── CHEATSHEET.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.APACHE2 ├── LICENSE.MIT ├── MANIFEST.in ├── README.rst ├── ci.sh ├── ci └── rtd-requirements.txt ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── .gitkeep │ ├── api.rst │ ├── conf.py │ ├── history.rst │ ├── index.rst │ └── tutorial.rst ├── newsfragments ├── .gitkeep └── README.rst ├── pyproject.toml ├── src └── outcome │ ├── __init__.py │ ├── _impl.py │ ├── _util.py │ ├── _version.py │ └── py.typed ├── test-requirements.txt └── tests ├── __init__.py ├── test_async.py ├── test_sync.py └── type_tests.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Windows: 7 | name: 'Windows (${{ matrix.python }})' 8 | runs-on: 'windows-latest' 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Setup python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python }} 21 | cache: pip 22 | cache-dependency-path: test-requirements.txt 23 | - name: Run tests 24 | run: ./ci.sh 25 | shell: bash 26 | env: 27 | # Should match 'name:' up above 28 | JOB_NAME: 'Windows (${{ matrix.python }})' 29 | 30 | Ubuntu: 31 | name: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' 32 | timeout-minutes: 10 33 | runs-on: 'ubuntu-latest' 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 38 | check_formatting: ['0'] 39 | extra_name: [''] 40 | include: 41 | - python: '3.10' 42 | check_formatting: '1' 43 | extra_name: ', check formatting' 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | - name: Setup python 48 | uses: actions/setup-python@v4 49 | if: "!endsWith(matrix.python, '-dev')" 50 | with: 51 | python-version: ${{ matrix.python }} 52 | cache: pip 53 | cache-dependency-path: test-requirements.txt 54 | - name: Setup python (dev) 55 | uses: deadsnakes/action@v2.0.2 56 | if: endsWith(matrix.python, '-dev') 57 | with: 58 | python-version: '${{ matrix.python }}' 59 | - name: Run tests 60 | run: ./ci.sh 61 | env: 62 | CHECK_FORMATTING: '${{ matrix.check_formatting }}' 63 | # Should match 'name:' up above 64 | JOB_NAME: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' 65 | 66 | macOS: 67 | name: 'macOS (${{ matrix.python }})' 68 | timeout-minutes: 10 69 | runs-on: 'macos-latest' 70 | strategy: 71 | fail-fast: false 72 | matrix: 73 | python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 74 | steps: 75 | - name: Checkout 76 | uses: actions/checkout@v4 77 | - name: Setup python 78 | uses: actions/setup-python@v4 79 | with: 80 | python-version: ${{ matrix.python }} 81 | cache: pip 82 | cache-dependency-path: test-requirements.txt 83 | - name: Run tests 84 | run: ./ci.sh 85 | env: 86 | # Should match 'name:' up above 87 | JOB_NAME: 'macOS (${{ matrix.python }})' 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Add any project-specific files here: 2 | 3 | 4 | # Sphinx docs 5 | docs/build/ 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *~ 11 | \#* 12 | .#* 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | /build/ 20 | /develop-eggs/ 21 | /dist/ 22 | /eggs/ 23 | /lib/ 24 | /lib64/ 25 | /parts/ 26 | /sdist/ 27 | /var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | .pytest_cache 42 | .mypy_cache 43 | nosetests.xml 44 | coverage.xml 45 | 46 | # Translations 47 | *.mo 48 | 49 | # Mr Developer 50 | .mr.developer.cfg 51 | .project 52 | .pydevproject 53 | 54 | # Rope 55 | .ropeproject 56 | 57 | # Django stuff: 58 | *.log 59 | *.pot 60 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # https://docs.readthedocs.io/en/latest/yaml-config.html 4 | formats: 5 | - htmlzip 6 | - epub 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.13" 12 | 13 | python: 14 | install: 15 | - requirements: ci/rtd-requirements.txt 16 | - method: pip 17 | path: . 18 | 19 | sphinx: 20 | configuration: docs/source/conf.py 21 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | # Align closing bracket with visual indentation. 3 | align_closing_bracket_with_visual_indent=True 4 | 5 | # Allow dictionary keys to exist on multiple lines. For example: 6 | # 7 | # x = { 8 | # ('this is the first element of a tuple', 9 | # 'this is the second element of a tuple'): 10 | # value, 11 | # } 12 | allow_multiline_dictionary_keys=False 13 | 14 | # Allow lambdas to be formatted on more than one line. 15 | allow_multiline_lambdas=False 16 | 17 | # Insert a blank line before a class-level docstring. 18 | blank_line_before_class_docstring=False 19 | 20 | # Insert a blank line before a 'def' or 'class' immediately nested 21 | # within another 'def' or 'class'. For example: 22 | # 23 | # class Foo: 24 | # # <------ this blank line 25 | # def method(): 26 | # ... 27 | blank_line_before_nested_class_or_def=False 28 | 29 | # Do not split consecutive brackets. Only relevant when 30 | # dedent_closing_brackets is set. For example: 31 | # 32 | # call_func_that_takes_a_dict( 33 | # { 34 | # 'key1': 'value1', 35 | # 'key2': 'value2', 36 | # } 37 | # ) 38 | # 39 | # would reformat to: 40 | # 41 | # call_func_that_takes_a_dict({ 42 | # 'key1': 'value1', 43 | # 'key2': 'value2', 44 | # }) 45 | coalesce_brackets=False 46 | 47 | # The column limit. 48 | column_limit=79 49 | 50 | # Indent width used for line continuations. 51 | continuation_indent_width=4 52 | 53 | # Put closing brackets on a separate line, dedented, if the bracketed 54 | # expression can't fit in a single line. Applies to all kinds of brackets, 55 | # including function definitions and calls. For example: 56 | # 57 | # config = { 58 | # 'key1': 'value1', 59 | # 'key2': 'value2', 60 | # } # <--- this bracket is dedented and on a separate line 61 | # 62 | # time_series = self.remote_client.query_entity_counters( 63 | # entity='dev3246.region1', 64 | # key='dns.query_latency_tcp', 65 | # transform=Transformation.AVERAGE(window=timedelta(seconds=60)), 66 | # start_ts=now()-timedelta(days=3), 67 | # end_ts=now(), 68 | # ) # <--- this bracket is dedented and on a separate line 69 | dedent_closing_brackets=True 70 | 71 | # Place each dictionary entry onto its own line. 72 | each_dict_entry_on_separate_line=True 73 | 74 | # The regex for an i18n comment. The presence of this comment stops 75 | # reformatting of that line, because the comments are required to be 76 | # next to the string they translate. 77 | i18n_comment= 78 | 79 | # The i18n function call names. The presence of this function stops 80 | # reformattting on that line, because the string it has cannot be moved 81 | # away from the i18n comment. 82 | i18n_function_call= 83 | 84 | # Indent the dictionary value if it cannot fit on the same line as the 85 | # dictionary key. For example: 86 | # 87 | # config = { 88 | # 'key1': 89 | # 'value1', 90 | # 'key2': value1 + 91 | # value2, 92 | # } 93 | indent_dictionary_value=True 94 | 95 | # The number of columns to use for indentation. 96 | indent_width=4 97 | 98 | # Join short lines into one line. E.g., single line 'if' statements. 99 | join_multiple_lines=False 100 | 101 | # Use spaces around default or named assigns. 102 | spaces_around_default_or_named_assign=False 103 | 104 | # Use spaces around the power operator. 105 | spaces_around_power_operator=False 106 | 107 | # The number of spaces required before a trailing comment. 108 | spaces_before_comment=2 109 | 110 | # Insert a space between the ending comma and closing bracket of a list, 111 | # etc. 112 | space_between_ending_comma_and_closing_bracket=False 113 | 114 | # Split before arguments if the argument list is terminated by a 115 | # comma. 116 | split_arguments_when_comma_terminated=True 117 | 118 | # Set to True to prefer splitting before '&', '|' or '^' rather than 119 | # after. 120 | split_before_bitwise_operator=True 121 | 122 | # Split before a dictionary or set generator (comp_for). For example, note 123 | # the split before the 'for': 124 | # 125 | # foo = { 126 | # variable: 'Hello world, have a nice day!' 127 | # for variable in bar if variable != 42 128 | # } 129 | split_before_dict_set_generator=True 130 | 131 | # If an argument / parameter list is going to be split, then split before 132 | # the first argument. 133 | split_before_first_argument=True 134 | 135 | # Set to True to prefer splitting before 'and' or 'or' rather than 136 | # after. 137 | split_before_logical_operator=True 138 | 139 | # Split named assignments onto individual lines. 140 | split_before_named_assigns=True 141 | 142 | # The penalty for splitting right after the opening bracket. 143 | split_penalty_after_opening_bracket=30 144 | 145 | # The penalty for splitting the line after a unary operator. 146 | split_penalty_after_unary_operator=10000 147 | 148 | # The penalty for splitting right before an if expression. 149 | split_penalty_before_if_expr=0 150 | 151 | # The penalty of splitting the line around the '&', '|', and '^' 152 | # operators. 153 | split_penalty_bitwise_operator=300 154 | 155 | # The penalty for characters over the column limit. 156 | split_penalty_excess_character=4500 157 | 158 | # The penalty incurred by adding a line split to the unwrapped line. The 159 | # more line splits added the higher the penalty. 160 | split_penalty_for_added_line_split=30 161 | 162 | # The penalty of splitting a list of "import as" names. For example: 163 | # 164 | # from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, 165 | # long_argument_2, 166 | # long_argument_3) 167 | # 168 | # would reformat to something like: 169 | # 170 | # from a_very_long_or_indented_module_name_yada_yad import ( 171 | # long_argument_1, long_argument_2, long_argument_3) 172 | split_penalty_import_names=0 173 | 174 | # The penalty of splitting the line around the 'and' and 'or' 175 | # operators. 176 | split_penalty_logical_operator=0 177 | 178 | # Use the Tab character for indentation. 179 | use_tabs=False 180 | 181 | -------------------------------------------------------------------------------- /CHEATSHEET.rst: -------------------------------------------------------------------------------- 1 | Tips 2 | ==== 3 | 4 | To run tests 5 | ------------ 6 | 7 | * Install requirements: ``pip install -r test-requirements.txt`` 8 | (possibly in a virtualenv) 9 | 10 | * Actually run the tests: ``pytest tests`` 11 | 12 | 13 | To run yapf 14 | ----------- 15 | 16 | * Show what changes yapf wants to make: ``yapf -rpd setup.py 17 | src tests`` 18 | 19 | * Apply all changes directly to the source tree: ``yapf -rpi setup.py 20 | src tests`` 21 | 22 | 23 | To make a release 24 | ----------------- 25 | 26 | * Update the version in ``outcome/_version.py`` 27 | 28 | * Run ``towncrier`` to collect your release notes. 29 | 30 | * Review your release notes. 31 | 32 | * Check everything in. 33 | 34 | * Double-check it all works, docs build, etc. 35 | 36 | * Build your sdist and wheel: ``python setup.py sdist bdist_wheel`` 37 | 38 | * Upload to PyPI: ``twine upload dist/*`` 39 | 40 | * Use ``git tag`` to tag your version. 41 | 42 | * Don't forget to ``git push --tags``. 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Please adhere to the Trio code of conduct. You can find it here: 2 | 3 | https://trio.readthedocs.io/en/latest/code-of-conduct.html 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you want to contribute to this code, please see the Trio contributing guide: 2 | https://trio.readthedocs.io/en/latest/contributing.html 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is made available under the terms of *either* of the 2 | licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to are 3 | made under the terms of *both* these licenses. 4 | -------------------------------------------------------------------------------- /LICENSE.APACHE2: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHEATSHEET.rst LICENSE* CODE_OF_CONDUCT* CONTRIBUTING* 2 | include .coveragerc .style.yapf 3 | include test-requirements.txt 4 | include src/outcome/py.typed 5 | recursive-include docs * 6 | prune docs/build 7 | recursive-include tests *.py 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/badge/chat-join%20now-blue.svg 2 | :target: https://gitter.im/python-trio/general 3 | :alt: Join chatroom 4 | 5 | .. image:: https://img.shields.io/badge/forum-join%20now-blue.svg 6 | :target: https://trio.discourse.group 7 | :alt: Join forum 8 | 9 | .. image:: https://img.shields.io/badge/docs-read%20now-blue.svg 10 | :target: https://outcome.readthedocs.io 11 | :alt: Documentation 12 | 13 | .. image:: https://img.shields.io/pypi/v/outcome.svg 14 | :target: https://pypi.org/project/outcome 15 | :alt: Latest PyPI version 16 | 17 | .. image:: https://codecov.io/gh/python-trio/trio/branch/master/graph/badge.svg 18 | :target: https://codecov.io/gh/python-trio/outcome 19 | :alt: Test coverage 20 | 21 | outcome 22 | ======= 23 | 24 | Welcome to `outcome `__! 25 | 26 | Capture the outcome of Python function calls. Extracted from the 27 | `Trio `__ project. 28 | 29 | License: Your choice of MIT or Apache License 2.0 30 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex -o pipefail 4 | 5 | CHECK_FILES="src tests" 6 | YAPF_VERSION=0.20.1 7 | 8 | # Log some general info about the environment 9 | echo "::group::Environment" 10 | uname -a 11 | env | sort 12 | echo "::endgroup::" 13 | 14 | ################################################################ 15 | # We have a Python environment! 16 | ################################################################ 17 | 18 | echo "::group::Versions" 19 | python -c "import sys, struct; print('python:', sys.version); print('version_info:', sys.version_info); print('bits:', struct.calcsize('P') * 8)" 20 | echo "::endgroup::" 21 | 22 | echo "::group::Install dependencies" 23 | python -m pip install -U pip build 24 | python -m pip --version 25 | 26 | python -m build 27 | python -m pip install dist/*.whl 28 | echo "::endgroup::" 29 | 30 | echo "::group::Setup for tests" 31 | # Install dependencies. 32 | pip install -Ur test-requirements.txt 33 | echo "::endgroup::" 34 | 35 | if [ "$CHECK_FORMATTING" = "1" ]; then 36 | echo "::group::Yapf" 37 | pip install yapf==${YAPF_VERSION} "isort>=5" mypy pyright 38 | if ! yapf -rpd $CHECK_FILES; then 39 | echo "::endgroup::" 40 | cat <= 6.0 3 | sphinx_rtd_theme 4 | sphinxcontrib-trio 5 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = outcome 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=outcome 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/outcome/03ed6218b08001877745bb1a9e180c8c5cf7c903/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _api-reference: 2 | 3 | ============= 4 | API Reference 5 | ============= 6 | 7 | .. module:: outcome 8 | 9 | .. autofunction:: capture 10 | 11 | .. autofunction:: acapture 12 | 13 | .. autoclass:: Outcome 14 | :members: 15 | :inherited-members: 16 | 17 | .. py:data:: Maybe 18 | :value: Value[ResultT] | Error 19 | 20 | A convenience alias to a union of both results. This allows type checkers to perform 21 | exhaustiveness checking when ``isinstance()`` is used with either class:: 22 | 23 | outcome: Maybe[int] = capture(some_function, 1, 2, 3) 24 | if isinstance(outcome, Value): 25 | # Type checkers know it's a Value[int] here. 26 | else: 27 | # It must be an Error. 28 | 29 | .. autoclass:: Value 30 | :members: 31 | :inherited-members: 32 | 33 | .. autoclass:: Error 34 | :members: 35 | :inherited-members: 36 | 37 | .. autoclass:: AlreadyUsedError 38 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Jan 21 19:11:14 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | 22 | # So autodoc can import our package 23 | sys.path.insert(0, os.path.abspath('../..')) 24 | 25 | # Warn about all references to unknown targets 26 | nitpicky = True 27 | # Except for these ones, which we expect to point to unknown targets: 28 | nitpick_ignore = [ 29 | # Format is ('sphinx reference type', 'string'), e.g.: 30 | ('py:obj', 'bytes-like'), 31 | # Typevars aren't found, for some reason. 32 | ('py:class', 'ArgsT'), 33 | ('py:class', 'ArgsT.args'), 34 | ('py:class', 'ArgsT.kwargs'), 35 | ('py:class', 'ResultT'), 36 | ('py:class', 'outcome._impl.ResultT'), 37 | ('py:class', 'outcome._impl.ValueT'), 38 | ] 39 | 40 | # -- General configuration ------------------------------------------------ 41 | 42 | # If your documentation needs a minimal Sphinx version, state it here. 43 | # 44 | # needs_sphinx = '1.0' 45 | 46 | # Add any Sphinx extension module names here, as strings. They can be 47 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 48 | # ones. 49 | extensions = [ 50 | 'sphinx.ext.autodoc', 51 | 'sphinx.ext.intersphinx', 52 | 'sphinx.ext.coverage', 53 | 'sphinx.ext.napoleon', 54 | 'sphinxcontrib_trio', 55 | ] 56 | 57 | intersphinx_mapping = { 58 | 'python': ('https://docs.python.org/3', None), 59 | 'trio': ('https://trio.readthedocs.io/en/stable', None), 60 | } 61 | 62 | autodoc_member_order = 'bysource' 63 | 64 | # Add any paths that contain templates here, relative to this directory. 65 | templates_path = [] 66 | 67 | # The suffix(es) of source filenames. 68 | # You can specify multiple suffix as a list of string: 69 | # 70 | # source_suffix = ['.rst', '.md'] 71 | source_suffix = '.rst' 72 | 73 | # The master toctree document. 74 | master_doc = 'index' 75 | 76 | # General information about the project. 77 | project = 'outcome' 78 | copyright = 'The outcome authors' 79 | author = 'The outcome authors' 80 | 81 | # The version info for the project you're documenting, acts as replacement for 82 | # |version| and |release|, also used in various other places throughout the 83 | # built documents. 84 | # 85 | # The short X.Y version. 86 | import outcome 87 | 88 | version = outcome.__version__ 89 | # The full version, including alpha/beta/rc tags. 90 | release = version 91 | 92 | # The language for content autogenerated by Sphinx. Refer to documentation 93 | # for a list of supported languages. 94 | # 95 | # This is also used if you do content translation via gettext catalogs. 96 | # Usually you set 'language' from the command line for these cases. 97 | language = None 98 | 99 | # List of patterns, relative to source directory, that match files and 100 | # directories to ignore when looking for source files. 101 | # This patterns also effect to html_static_path and html_extra_path 102 | exclude_patterns = [] 103 | 104 | # The name of the Pygments (syntax highlighting) style to use. 105 | pygments_style = 'sphinx' 106 | 107 | # The default language for :: blocks 108 | highlight_language = 'python3' 109 | 110 | # If true, `todo` and `todoList` produce output, else they produce nothing. 111 | todo_include_todos = False 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | # The theme to use for HTML and HTML Help pages. See the documentation for 116 | # a list of builtin themes. 117 | # 118 | #html_theme = 'alabaster' 119 | 120 | # We have to set this ourselves, not only because it's useful for local 121 | # testing, but also because if we don't then RTD will throw away our 122 | # html_theme_options. 123 | import sphinx_rtd_theme 124 | 125 | html_theme = 'sphinx_rtd_theme' 126 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 127 | 128 | # Theme options are theme-specific and customize the look and feel of a theme 129 | # further. For a list of options available for each theme, see the 130 | # documentation. 131 | # 132 | html_theme_options = { 133 | # default is 2 134 | # show deeper nesting in the RTD theme's sidebar TOC 135 | # https://stackoverflow.com/questions/27669376/ 136 | # I'm not 100% sure this actually does anything with our current 137 | # versions/settings... 138 | 'navigation_depth': 4, 139 | 'logo_only': True, 140 | } 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named 'default.css' will overwrite the builtin 'default.css'. 145 | html_static_path = ['_static'] 146 | 147 | # -- Options for HTMLHelp output ------------------------------------------ 148 | 149 | # Output file base name for HTML help builder. 150 | htmlhelp_basename = 'outcomedoc' 151 | 152 | # -- Options for LaTeX output --------------------------------------------- 153 | 154 | latex_elements = { 155 | # The paper size ('letterpaper' or 'a4paper'). 156 | # 157 | # 'papersize': 'letterpaper', 158 | 159 | # The font size ('10pt', '11pt' or '12pt'). 160 | # 161 | # 'pointsize': '10pt', 162 | 163 | # Additional stuff for the LaTeX preamble. 164 | # 165 | # 'preamble': '', 166 | 167 | # Latex figure (float) alignment 168 | # 169 | # 'figure_align': 'htbp', 170 | } 171 | 172 | # Grouping the document tree into LaTeX files. List of tuples 173 | # (source start file, target name, title, 174 | # author, documentclass [howto, manual, or own class]). 175 | latex_documents = [ 176 | (master_doc, 'outcome.tex', 'Trio Documentation', author, 'manual'), 177 | ] 178 | 179 | # -- Options for manual page output --------------------------------------- 180 | 181 | # One entry per manual page. List of tuples 182 | # (source start file, name, description, authors, manual section). 183 | man_pages = [(master_doc, 'outcome', 'outcome Documentation', [author], 1)] 184 | 185 | # -- Options for Texinfo output ------------------------------------------- 186 | 187 | # Grouping the document tree into Texinfo files. List of tuples 188 | # (source start file, target name, title, author, 189 | # dir menu entry, description, category) 190 | texinfo_documents = [ 191 | ( 192 | master_doc, 'outcome', 'outcome Documentation', author, 'outcome', 193 | 'Capture the outcome of Python function call.', 'Miscellaneous' 194 | ), 195 | ] 196 | -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | Release history 2 | =============== 3 | 4 | .. currentmodule:: outcome 5 | 6 | .. towncrier release notes start 7 | 8 | Outcome 1.3.0 (2023-10-17) 9 | -------------------------- 10 | 11 | Features 12 | ~~~~~~~~ 13 | 14 | - Added type hints to the package. :py:class:`Value` and :py:class:`Outcome` are now generic. 15 | A type alias was also added (:py:data:`Maybe`) for the union of :py:class:`Value` 16 | and :py:class:`Error`. (`#36 `__) 17 | 18 | 19 | Outcome 1.2.0 (2022-06-14) 20 | -------------------------- 21 | 22 | Features 23 | ~~~~~~~~ 24 | 25 | - Add support for Python 3.9 and 3.10. (`#32 `__) 26 | 27 | 28 | Deprecations and Removals 29 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | 31 | - Drop support for Python 3.6. (`#32 `__) 32 | 33 | 34 | Outcome 1.1.0 (2020-11-16) 35 | -------------------------- 36 | 37 | Bugfixes 38 | ~~~~~~~~ 39 | 40 | - Tweaked the implementation of ``Error.unwrap`` to avoid creating a 41 | reference cycle between the exception object and the ``unwrap`` 42 | method's frame. This shouldn't affect most users, but it slightly 43 | reduces the amount of work that CPython's cycle collector has to do, 44 | and may reduce GC pauses in some cases. (`#29 `__) 45 | 46 | 47 | Deprecations and Removals 48 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 49 | 50 | - Drop support for Python 2.7, 3.4, and 3.5. (`#27 `__) 51 | 52 | 53 | Outcome 1.0.1 (2019-10-16) 54 | -------------------------- 55 | 56 | Upgrade to attrs 19.2.0. 57 | 58 | 59 | Outcome 1.0.0 (2018-09-12) 60 | -------------------------- 61 | 62 | Features 63 | ~~~~~~~~ 64 | 65 | - On Python 3, the exception frame generated within :func:`capture` and 66 | :func:`acapture` has been removed from the traceback. 67 | (`#21 `__) 68 | - Outcome is now tested using asyncio instead of trio, which outcome is a 69 | dependency of. This makes it easier for third parties to package up Outcome. 70 | (`#13 `__) 71 | 72 | 73 | Outcome 0.1.0 (2018-07-10) 74 | -------------------------- 75 | 76 | Features 77 | ~~~~~~~~ 78 | 79 | - An Outcome may only be unwrapped or sent once. 80 | 81 | Attempting to do so a second time will raise an :class:`AlreadyUsedError`. 82 | (`#7 `__) 83 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ====================================================== 2 | outcome: Capture the outcome of Python function calls. 3 | ====================================================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | tutorial.rst 9 | api.rst 10 | history.rst 11 | 12 | ==================== 13 | Indices and tables 14 | ==================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | * :ref:`glossary` 20 | -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Tutorial 3 | ======== 4 | 5 | .. currentmodule:: outcome 6 | 7 | Outcome provides a function for capturing the outcome of a Python 8 | function call, so that it can be passed around. The basic rule is:: 9 | 10 | result = outcome.capture(f, *args, **kwargs) 11 | x = result.unwrap() 12 | 13 | is the same as:: 14 | 15 | x = f(*args, **kwargs) 16 | 17 | even if ``f`` raises an error. 18 | 19 | There's also :func:`acapture`:: 20 | 21 | result = await outcome.acapture(f, *args, **kwargs) 22 | x = result.unwrap() 23 | 24 | which, like before, is the same as:: 25 | 26 | x = await f(*args, **kwargs) 27 | 28 | An Outcome object can only be unwrapped once. A second attempt would raise an 29 | :class:`AlreadyUsedError`. 30 | 31 | See the :ref:`api-reference` for the types involved. 32 | -------------------------------------------------------------------------------- /newsfragments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/outcome/03ed6218b08001877745bb1a9e180c8c5cf7c903/newsfragments/.gitkeep -------------------------------------------------------------------------------- /newsfragments/README.rst: -------------------------------------------------------------------------------- 1 | Adding newsfragments 2 | ==================== 3 | 4 | This directory collects "newsfragments": short files that each contain 5 | a snippet of ReST-formatted text that will be added to the next 6 | release notes. This should be a description of aspects of the change 7 | (if any) that are relevant to users. (This contrasts with your commit 8 | message and PR description, which are a description of the change as 9 | relevant to people working on the code itself.) 10 | 11 | Each file should be named like ``..rst``, where 12 | ```` is an issue numbers, and ```` is one of: 13 | 14 | * ``feature`` 15 | * ``bugfix`` 16 | * ``doc`` 17 | * ``removal`` 18 | * ``misc`` 19 | 20 | So for example: ``123.feature.rst``, ``456.bugfix.rst`` 21 | 22 | If your PR fixes an issue, use that number here. If there is no issue, 23 | then after you submit the PR and get the PR number you can add a 24 | newsfragment using that instead. 25 | 26 | Note that the ``towncrier`` tool will automatically 27 | reflow your text, so don't try to do any fancy formatting. You can 28 | install ``towncrier`` and then run ``towncrier --draft`` if you want 29 | to get a preview of how your change will look in the final release 30 | notes. 31 | 32 | 33 | Making releases 34 | =============== 35 | 36 | ``pip install towncrier``, then run ``towncrier``. (You can use 37 | ``towncrier --draft`` to get a preview of what this will do.) 38 | 39 | You can configure ``towncrier`` (for example: customizing the 40 | different types of changes) by modifying ``pyproject.toml``. 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 64"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name="outcome" 7 | description="Capture the outcome of Python function calls." 8 | authors = [{name = "Frazer McLean", email = "frazer@frazermclean.co.uk"}] 9 | license = {text = "MIT OR Apache-2.0"} 10 | keywords = [ 11 | "result", 12 | ] 13 | classifiers=[ 14 | "Development Status :: 5 - Production/Stable", 15 | "Framework :: Trio", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "License :: OSI Approved :: Apache Software License", 19 | "Operating System :: POSIX :: Linux", 20 | "Operating System :: MacOS :: MacOS X", 21 | "Operating System :: Microsoft :: Windows", 22 | "Programming Language :: Python :: Implementation :: CPython", 23 | "Programming Language :: Python :: Implementation :: PyPy", 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: 3.13", 31 | "Typing :: Typed", 32 | ] 33 | requires-python = ">=3.8" 34 | dependencies = [ 35 | # attrs 19.2.0 adds `eq` option to decorators 36 | "attrs>=19.2.0" 37 | ] 38 | dynamic = ["version"] 39 | 40 | [project.readme] 41 | file = "README.rst" 42 | content-type = "text/x-rst" 43 | 44 | [project.urls] 45 | Homepage = "https://github.com/python-trio/outcome" 46 | Documentation = "https://outcome.readthedocs.io/en/latest/" 47 | Changelog = "https://outcome.readthedocs.io/en/latest/history.html" 48 | Chat = "https://gitter.im/python-trio/general" 49 | 50 | [tool.setuptools] 51 | # This means, just install *everything* you see under outcome/, even if it 52 | # doesn't look like a source file, so long as it appears in MANIFEST.in: 53 | include-package-data = true 54 | 55 | [tool.setuptools.dynamic] 56 | version = {attr = "outcome._version.__version__"} 57 | 58 | [tool.towncrier] 59 | directory = "newsfragments" 60 | filename = "docs/source/history.rst" 61 | issue_format = "`#{issue} `__" 62 | # Usage: 63 | # - PRs should drop a file like "issuenumber.feature" in newsfragments 64 | # (or "bugfix", "doc", "removal", "misc"; misc gets no text, we can 65 | # customize this) 66 | # - At release time after bumping version number, run: towncrier 67 | # (or towncrier --draft) 68 | package = "outcome" 69 | package_dir = "src" 70 | underlines = ["-", "~", "^"] 71 | 72 | [[tool.towncrier.type]] 73 | directory = "feature" 74 | name = "Features" 75 | showcontent = true 76 | 77 | [[tool.towncrier.type]] 78 | directory = "bugfix" 79 | name = "Bugfixes" 80 | showcontent = true 81 | 82 | [[tool.towncrier.type]] 83 | directory = "doc" 84 | name = "Improved documentation" 85 | showcontent = true 86 | 87 | [[tool.towncrier.type]] 88 | directory = "removal" 89 | name = "Removals without deprecations" 90 | showcontent = true 91 | 92 | [[tool.towncrier.type]] 93 | directory = "misc" 94 | name = "Miscellaneous internal changes" 95 | showcontent = true 96 | 97 | [tool.coverage.run] 98 | branch = true 99 | source_pkgs = ["outcome", "tests"] 100 | omit = [ 101 | "tests/type_tests.py", 102 | ] 103 | 104 | [tool.coverage.report] 105 | precision = 1 106 | exclude_lines = [ 107 | "pragma: no cover", 108 | "abc.abstractmethod", 109 | "if TYPE_CHECKING.*:", 110 | "@overload", 111 | "raise NotImplementedError", 112 | ] 113 | partial_branches = [ 114 | "pragma: no branch", 115 | "if not TYPE_CHECKING:", 116 | "if .* or not TYPE_CHECKING:", 117 | ] 118 | 119 | [tool.isort] 120 | combine_as_imports = true 121 | profile = "black" 122 | skip_gitignore = true 123 | skip = ["./build", "./docs"] 124 | known_first_party = ["outcome"] 125 | 126 | [tool.mypy] 127 | python_version = "3.8" 128 | 129 | # Be strict about use of Mypy 130 | strict = true 131 | local_partial_types = true 132 | warn_unused_ignores = true 133 | warn_unused_configs = true 134 | warn_redundant_casts = true 135 | warn_no_return = true 136 | warn_unreachable = true 137 | warn_return_any = true 138 | 139 | # Avoid subtle backsliding 140 | disallow_incomplete_defs = true 141 | disallow_subclassing_any = true 142 | disallow_any_unimported = true 143 | disallow_any_generics = true 144 | disallow_any_explicit = false 145 | 146 | check_untyped_defs = true 147 | disallow_untyped_calls = true 148 | disallow_untyped_defs = true 149 | disallow_untyped_decorators = true 150 | 151 | # DO NOT use `ignore_errors`; it doesn't apply 152 | # downstream and users have to deal with them. 153 | 154 | [tool.pytest.ini_options] 155 | asyncio_mode = "strict" 156 | -------------------------------------------------------------------------------- /src/outcome/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for outcome.""" 2 | 3 | from ._impl import ( 4 | Error as Error, 5 | Maybe as Maybe, 6 | Outcome as Outcome, 7 | Value as Value, 8 | acapture as acapture, 9 | capture as capture, 10 | ) 11 | from ._util import AlreadyUsedError as AlreadyUsedError, fixup_module_metadata 12 | from ._version import __version__ as __version__ 13 | 14 | __all__ = ( 15 | 'Error', 'Outcome', 'Value', 'Maybe', 'acapture', 'capture', 16 | 'AlreadyUsedError' 17 | ) 18 | 19 | fixup_module_metadata(__name__, globals()) 20 | del fixup_module_metadata 21 | -------------------------------------------------------------------------------- /src/outcome/_impl.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import abc 4 | from typing import ( 5 | TYPE_CHECKING, 6 | AsyncGenerator, 7 | Awaitable, 8 | Callable, 9 | Generator, 10 | Generic, 11 | NoReturn, 12 | TypeVar, 13 | Union, 14 | overload, 15 | ) 16 | 17 | import attr 18 | 19 | from ._util import AlreadyUsedError, remove_tb_frames 20 | 21 | if TYPE_CHECKING: 22 | from typing_extensions import ParamSpec, final 23 | ArgsT = ParamSpec("ArgsT") 24 | else: 25 | 26 | def final(func): 27 | return func 28 | 29 | 30 | __all__ = ['Error', 'Outcome', 'Maybe', 'Value', 'acapture', 'capture'] 31 | 32 | ValueT = TypeVar("ValueT", covariant=True) 33 | ResultT = TypeVar("ResultT") 34 | 35 | 36 | @overload 37 | def capture( 38 | # NoReturn = raises exception, so we should get an error. 39 | sync_fn: Callable[ArgsT, NoReturn], 40 | *args: ArgsT.args, 41 | **kwargs: ArgsT.kwargs, 42 | ) -> Error: 43 | ... 44 | 45 | 46 | @overload 47 | def capture( 48 | sync_fn: Callable[ArgsT, ResultT], 49 | *args: ArgsT.args, 50 | **kwargs: ArgsT.kwargs, 51 | ) -> Value[ResultT] | Error: 52 | ... 53 | 54 | 55 | def capture( 56 | sync_fn: Callable[ArgsT, ResultT], 57 | *args: ArgsT.args, 58 | **kwargs: ArgsT.kwargs, 59 | ) -> Value[ResultT] | Error: 60 | """Run ``sync_fn(*args, **kwargs)`` and capture the result. 61 | 62 | Returns: 63 | Either a :class:`Value` or :class:`Error` as appropriate. 64 | 65 | """ 66 | try: 67 | return Value(sync_fn(*args, **kwargs)) 68 | except BaseException as exc: 69 | exc = remove_tb_frames(exc, 1) 70 | return Error(exc) 71 | 72 | 73 | @overload 74 | async def acapture( 75 | async_fn: Callable[ArgsT, Awaitable[NoReturn]], 76 | *args: ArgsT.args, 77 | **kwargs: ArgsT.kwargs, 78 | ) -> Error: 79 | ... 80 | 81 | 82 | @overload 83 | async def acapture( 84 | async_fn: Callable[ArgsT, Awaitable[ResultT]], 85 | *args: ArgsT.args, 86 | **kwargs: ArgsT.kwargs, 87 | ) -> Value[ResultT] | Error: 88 | ... 89 | 90 | 91 | async def acapture( 92 | async_fn: Callable[ArgsT, Awaitable[ResultT]], 93 | *args: ArgsT.args, 94 | **kwargs: ArgsT.kwargs, 95 | ) -> Value[ResultT] | Error: 96 | """Run ``await async_fn(*args, **kwargs)`` and capture the result. 97 | 98 | Returns: 99 | Either a :class:`Value` or :class:`Error` as appropriate. 100 | 101 | """ 102 | try: 103 | return Value(await async_fn(*args, **kwargs)) 104 | except BaseException as exc: 105 | exc = remove_tb_frames(exc, 1) 106 | return Error(exc) 107 | 108 | 109 | @attr.s(repr=False, init=False, slots=True) 110 | class Outcome(abc.ABC, Generic[ValueT]): 111 | """An abstract class representing the result of a Python computation. 112 | 113 | This class has two concrete subclasses: :class:`Value` representing a 114 | value, and :class:`Error` representing an exception. 115 | 116 | In addition to the methods described below, comparison operators on 117 | :class:`Value` and :class:`Error` objects (``==``, ``<``, etc.) check that 118 | the other object is also a :class:`Value` or :class:`Error` object 119 | respectively, and then compare the contained objects. 120 | 121 | :class:`Outcome` objects are hashable if the contained objects are 122 | hashable. 123 | 124 | """ 125 | _unwrapped: bool = attr.ib(default=False, eq=False, init=False) 126 | 127 | def _set_unwrapped(self) -> None: 128 | if self._unwrapped: 129 | raise AlreadyUsedError 130 | object.__setattr__(self, '_unwrapped', True) 131 | 132 | @abc.abstractmethod 133 | def unwrap(self) -> ValueT: 134 | """Return or raise the contained value or exception. 135 | 136 | These two lines of code are equivalent:: 137 | 138 | x = fn(*args) 139 | x = outcome.capture(fn, *args).unwrap() 140 | 141 | """ 142 | 143 | @abc.abstractmethod 144 | def send(self, gen: Generator[ResultT, ValueT, object]) -> ResultT: 145 | """Send or throw the contained value or exception into the given 146 | generator object. 147 | 148 | Args: 149 | gen: A generator object supporting ``.send()`` and ``.throw()`` 150 | methods. 151 | 152 | """ 153 | 154 | @abc.abstractmethod 155 | async def asend(self, agen: AsyncGenerator[ResultT, ValueT]) -> ResultT: 156 | """Send or throw the contained value or exception into the given async 157 | generator object. 158 | 159 | Args: 160 | agen: An async generator object supporting ``.asend()`` and 161 | ``.athrow()`` methods. 162 | 163 | """ 164 | 165 | 166 | @final 167 | @attr.s(frozen=True, repr=False, slots=True) 168 | class Value(Outcome[ValueT], Generic[ValueT]): 169 | """Concrete :class:`Outcome` subclass representing a regular value. 170 | 171 | """ 172 | 173 | value: ValueT = attr.ib() 174 | """The contained value.""" 175 | 176 | def __repr__(self) -> str: 177 | return f'Value({self.value!r})' 178 | 179 | def unwrap(self) -> ValueT: 180 | self._set_unwrapped() 181 | return self.value 182 | 183 | def send(self, gen: Generator[ResultT, ValueT, object]) -> ResultT: 184 | self._set_unwrapped() 185 | return gen.send(self.value) 186 | 187 | async def asend(self, agen: AsyncGenerator[ResultT, ValueT]) -> ResultT: 188 | self._set_unwrapped() 189 | return await agen.asend(self.value) 190 | 191 | 192 | @final 193 | @attr.s(frozen=True, repr=False, slots=True) 194 | class Error(Outcome[NoReturn]): 195 | """Concrete :class:`Outcome` subclass representing a raised exception. 196 | 197 | """ 198 | 199 | error: BaseException = attr.ib( 200 | validator=attr.validators.instance_of(BaseException) 201 | ) 202 | """The contained exception object.""" 203 | 204 | def __repr__(self) -> str: 205 | return f'Error({self.error!r})' 206 | 207 | def unwrap(self) -> NoReturn: 208 | self._set_unwrapped() 209 | # Tracebacks show the 'raise' line below out of context, so let's give 210 | # this variable a name that makes sense out of context. 211 | captured_error = self.error 212 | try: 213 | raise captured_error 214 | finally: 215 | # We want to avoid creating a reference cycle here. Python does 216 | # collect cycles just fine, so it wouldn't be the end of the world 217 | # if we did create a cycle, but the cyclic garbage collector adds 218 | # latency to Python programs, and the more cycles you create, the 219 | # more often it runs, so it's nicer to avoid creating them in the 220 | # first place. For more details see: 221 | # 222 | # https://github.com/python-trio/trio/issues/1770 223 | # 224 | # In particuar, by deleting this local variables from the 'unwrap' 225 | # methods frame, we avoid the 'captured_error' object's 226 | # __traceback__ from indirectly referencing 'captured_error'. 227 | del captured_error, self 228 | 229 | def send(self, gen: Generator[ResultT, NoReturn, object]) -> ResultT: 230 | self._set_unwrapped() 231 | return gen.throw(self.error) 232 | 233 | async def asend(self, agen: AsyncGenerator[ResultT, NoReturn]) -> ResultT: 234 | self._set_unwrapped() 235 | return await agen.athrow(self.error) 236 | 237 | 238 | # A convenience alias to a union of both results, allowing exhaustiveness checking. 239 | Maybe = Union[Value[ValueT], Error] 240 | -------------------------------------------------------------------------------- /src/outcome/_util.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | 4 | class AlreadyUsedError(RuntimeError): 5 | """An Outcome can only be unwrapped once.""" 6 | pass 7 | 8 | 9 | def fixup_module_metadata( 10 | module_name: str, 11 | namespace: Dict[str, object], 12 | ) -> None: 13 | def fix_one(obj: object) -> None: 14 | mod = getattr(obj, "__module__", None) 15 | if mod is not None and mod.startswith("outcome."): 16 | obj.__module__ = module_name 17 | if isinstance(obj, type): 18 | for attr_value in obj.__dict__.values(): 19 | fix_one(attr_value) 20 | 21 | all_list = namespace["__all__"] 22 | assert isinstance(all_list, (tuple, list)), repr(all_list) 23 | for objname in all_list: 24 | obj = namespace[objname] 25 | fix_one(obj) 26 | 27 | 28 | def remove_tb_frames(exc: BaseException, n: int) -> BaseException: 29 | tb = exc.__traceback__ 30 | for _ in range(n): 31 | assert tb is not None 32 | tb = tb.tb_next 33 | return exc.with_traceback(tb) 34 | -------------------------------------------------------------------------------- /src/outcome/_version.py: -------------------------------------------------------------------------------- 1 | # This file is imported from __init__.py and parsed by setuptools 2 | from typing import TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from typing_extensions import Final 6 | 7 | __version__: 'Final[str]' = "1.3.0.post0+dev" 8 | -------------------------------------------------------------------------------- /src/outcome/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/outcome/03ed6218b08001877745bb1a9e180c8c5cf7c903/src/outcome/py.typed -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-asyncio 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/outcome/03ed6218b08001877745bb1a9e180c8c5cf7c903/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import traceback 3 | 4 | import pytest 5 | 6 | import outcome 7 | from outcome import AlreadyUsedError, Error, Value 8 | 9 | pytestmark = pytest.mark.asyncio 10 | 11 | 12 | async def test_acapture(): 13 | async def add(x, y): 14 | await asyncio.sleep(0) 15 | return x + y 16 | 17 | v = await outcome.acapture(add, 3, y=4) 18 | assert v == Value(7) 19 | 20 | async def raise_ValueError(x): 21 | await asyncio.sleep(0) 22 | raise ValueError(x) 23 | 24 | e = await outcome.acapture(raise_ValueError, 9) 25 | assert type(e.error) is ValueError 26 | assert e.error.args == (9,) 27 | 28 | 29 | async def test_asend(): 30 | async def my_agen_func(): 31 | assert (yield 1) == "value" 32 | with pytest.raises(KeyError): 33 | yield 2 34 | yield 3 35 | 36 | my_agen = my_agen_func().__aiter__() 37 | v = Value("value") 38 | e = Error(KeyError()) 39 | assert (await my_agen.asend(None)) == 1 40 | assert (await v.asend(my_agen)) == 2 41 | with pytest.raises(AlreadyUsedError): 42 | await v.asend(my_agen) 43 | 44 | assert (await e.asend(my_agen)) == 3 45 | with pytest.raises(AlreadyUsedError): 46 | await e.asend(my_agen) 47 | with pytest.raises(StopAsyncIteration): 48 | await my_agen.asend(None) 49 | 50 | 51 | async def test_traceback_frame_removal(): 52 | async def raise_ValueError(x): 53 | raise ValueError(x) 54 | 55 | e = await outcome.acapture(raise_ValueError, 'abc') 56 | with pytest.raises(ValueError) as exc_info: 57 | e.unwrap() 58 | frames = traceback.extract_tb(exc_info.value.__traceback__) 59 | functions = [function for _, _, function, _ in frames] 60 | assert functions[-2:] == ['unwrap', 'raise_ValueError'] 61 | -------------------------------------------------------------------------------- /tests/test_sync.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | import pytest 5 | 6 | import outcome 7 | from outcome import AlreadyUsedError, Error, Value 8 | 9 | 10 | def test_Outcome(): 11 | v = Value(1) 12 | assert v.value == 1 13 | assert v.unwrap() == 1 14 | assert repr(v) == "Value(1)" 15 | 16 | with pytest.raises(AlreadyUsedError): 17 | v.unwrap() 18 | 19 | v = Value(1) 20 | 21 | exc = RuntimeError("oops") 22 | e = Error(exc) 23 | assert e.error is exc 24 | with pytest.raises(RuntimeError): 25 | e.unwrap() 26 | with pytest.raises(AlreadyUsedError): 27 | e.unwrap() 28 | assert repr(e) == f"Error({exc!r})" 29 | 30 | e = Error(exc) 31 | with pytest.raises(TypeError): 32 | Error("hello") 33 | with pytest.raises(TypeError): 34 | Error(RuntimeError) 35 | 36 | def expect_1(): 37 | assert (yield) == 1 38 | yield "ok" 39 | 40 | it = iter(expect_1()) 41 | next(it) 42 | assert v.send(it) == "ok" 43 | with pytest.raises(AlreadyUsedError): 44 | v.send(it) 45 | 46 | def expect_RuntimeError(): 47 | with pytest.raises(RuntimeError): 48 | yield 49 | yield "ok" 50 | 51 | it = iter(expect_RuntimeError()) 52 | next(it) 53 | assert e.send(it) == "ok" 54 | with pytest.raises(AlreadyUsedError): 55 | e.send(it) 56 | 57 | 58 | def test_Outcome_eq_hash(): 59 | v1 = Value(["hello"]) 60 | v2 = Value(["hello"]) 61 | v3 = Value("hello") 62 | v4 = Value("hello") 63 | assert v1 == v2 64 | assert v1 != v3 65 | with pytest.raises(TypeError): 66 | {v1} 67 | assert {v3, v4} == {v3} 68 | 69 | # exceptions in general compare by identity 70 | exc1 = RuntimeError("oops") 71 | exc2 = KeyError("foo") 72 | e1 = Error(exc1) 73 | e2 = Error(exc1) 74 | e3 = Error(exc2) 75 | e4 = Error(exc2) 76 | assert e1 == e2 77 | assert e3 == e4 78 | assert e1 != e3 79 | assert {e1, e2, e3, e4} == {e1, e3} 80 | 81 | 82 | def test_Value_compare(): 83 | assert Value(1) < Value(2) 84 | assert not Value(3) < Value(2) 85 | with pytest.raises(TypeError): 86 | Value(1) < Value("foo") 87 | 88 | 89 | def test_capture(): 90 | def add(x, y): 91 | return x + y 92 | 93 | v = outcome.capture(add, 2, y=3) 94 | assert type(v) == Value 95 | assert v.unwrap() == 5 96 | 97 | def raise_ValueError(x): 98 | raise ValueError(x) 99 | 100 | e = outcome.capture(raise_ValueError, "two") 101 | assert type(e) == Error 102 | assert type(e.error) is ValueError 103 | assert e.error.args == ("two",) 104 | 105 | 106 | def test_inheritance(): 107 | assert issubclass(Value, outcome.Outcome) 108 | assert issubclass(Error, outcome.Outcome) 109 | 110 | 111 | def test_traceback_frame_removal(): 112 | def raise_ValueError(x): 113 | raise ValueError(x) 114 | 115 | e = outcome.capture(raise_ValueError, 'abc') 116 | with pytest.raises(ValueError) as exc_info: 117 | e.unwrap() 118 | frames = traceback.extract_tb(exc_info.value.__traceback__) 119 | functions = [function for _, _, function, _ in frames] 120 | assert functions[-2:] == ['unwrap', 'raise_ValueError'] 121 | 122 | 123 | def test_Error_unwrap_does_not_create_reference_cycles(): 124 | # See comment in Error.unwrap for why reference cycles are tricky 125 | exc = ValueError() 126 | err = Error(exc) 127 | try: 128 | err.unwrap() 129 | except ValueError: 130 | pass 131 | # Top frame in the traceback is the current test function; we don't care 132 | # about its references 133 | assert exc.__traceback__.tb_frame is sys._getframe() 134 | # The next frame down is the 'unwrap' frame; we want to make sure it 135 | # doesn't reference the exception (or anything else for that matter, just 136 | # to be thorough) 137 | unwrap_frame = exc.__traceback__.tb_next.tb_frame 138 | assert unwrap_frame.f_code.co_name == "unwrap" 139 | assert unwrap_frame.f_locals == {} 140 | -------------------------------------------------------------------------------- /tests/type_tests.py: -------------------------------------------------------------------------------- 1 | """Verify type hints behave corectly. 2 | 3 | This doesn't have the test_ prefix, since runtime testing isn't particularly useful. 4 | """ 5 | from collections.abc import AsyncGenerator, Generator 6 | from typing import List, NoReturn, Union 7 | 8 | from typing_extensions import assert_never, assert_type 9 | 10 | import outcome 11 | from outcome import Error, Maybe, Outcome, Value, acapture, capture 12 | 13 | 14 | class Super: 15 | """A superclass, inheriting ultimately from object.""" 16 | 17 | 18 | class Sub(Super): 19 | """A subclass.""" 20 | 21 | 22 | maybe: Maybe[float] = capture(len, []) 23 | assert_type(maybe, Union[Value[float], Error]) 24 | 25 | # Check that this is immutable. 26 | outcome.__version__ = 'dev' # type: ignore[misc] 27 | 28 | 29 | def maybe_test_val_first(maybe: Maybe[float]) -> None: 30 | """Check behaviour of the Maybe annotation, when checking Value first.""" 31 | assert_type(maybe, Union[Value[float], Error]) 32 | # Check narrowing. 33 | if isinstance(maybe, Value): 34 | assert_type(maybe, Value[float]) 35 | assert_type(maybe.value, float) 36 | maybe.error # type: ignore[attr-defined] 37 | else: 38 | assert_type(maybe, Error) 39 | if isinstance(maybe, Error): 40 | assert_type(maybe, Error) 41 | assert_type(maybe.error, BaseException) 42 | maybe.value # type: ignore[attr-defined] 43 | else: 44 | assert_never(maybe) 45 | 46 | 47 | def maybe_test_err_first(maybe: Maybe[float]) -> None: 48 | """Check behaviour of the Maybe annotation, when checking Error first.""" 49 | assert_type(maybe, Union[Value[float], Error]) 50 | # Check narrowing. 51 | if isinstance(maybe, Error): 52 | assert_type(maybe, Error) 53 | assert_type(maybe.error, BaseException) 54 | maybe.value # type: ignore[attr-defined] 55 | else: 56 | assert_type(maybe, Value[float]) 57 | if isinstance(maybe, Value): 58 | assert_type(maybe, Value[float]) 59 | assert_type(maybe.value, float) 60 | maybe.error # type: ignore[attr-defined] 61 | else: 62 | assert_never(maybe) 63 | 64 | 65 | def value_variance_test() -> None: 66 | """Check variance behaves as expected.""" 67 | value: Value[Super] 68 | value_sub: Value[Sub] = Value(Sub()) 69 | value_super: Value[object] = Value(None) 70 | value = value_sub # Is covariant. 71 | value = value_super # type: ignore[assignment] 72 | 73 | 74 | def outcome_test() -> None: 75 | """Test assigning either type to the base class.""" 76 | value: Value[List[str]] = Value(['a', 'b', 'c']) 77 | bad_value: Value[float] = Value(48.3) 78 | error: Error = Error(Exception()) 79 | 80 | outcome_good: Outcome[List[str]] = value 81 | outcome_mismatch: Outcome[bool] = value # type: ignore[assignment] 82 | outcome_err: Outcome[List[str]] = error 83 | 84 | assert_type(outcome_good.unwrap(), List[str]) 85 | assert_type(outcome_err.unwrap(), List[str]) 86 | assert_type(value.unwrap(), List[str]) 87 | assert_type(error.unwrap(), NoReturn) 88 | 89 | 90 | def sync_raises() -> NoReturn: 91 | raise NotImplementedError 92 | 93 | 94 | def sync_generator_one() -> Generator[int, str, List[str]]: 95 | word: str = (yield 5) 96 | assert len(word) == 3 97 | return ['a', 'b', 'c'] 98 | 99 | 100 | def sync_generator_two() -> Generator[str, int, List[str]]: 101 | three: int = (yield 'house') 102 | assert three.bit_length() == 2 103 | return ['a', 'b', 'c'] 104 | 105 | 106 | def sync_none() -> bool: 107 | return True 108 | 109 | 110 | def sync_one(param: float) -> int: 111 | return round(param) 112 | 113 | 114 | def sync_capture_test() -> None: 115 | """Test synchronous behaviour.""" 116 | assert_type(capture(sync_none), Union[Value[bool], Error]) 117 | assert_type(capture(sync_one, 3.14), Union[Value[int], Error]) 118 | assert_type(capture(sync_one, param=3.14), Union[Value[int], Error]) 119 | assert_type(capture(sync_raises), Error) 120 | capture(sync_one) # type: ignore[call-overload] 121 | capture(sync_none, 1, 2) # type: ignore[call-overload] 122 | 123 | 124 | async def sync_gen_test() -> None: 125 | """Check send methods.""" 126 | value_one: Value[str] = Value('abc') 127 | value_two: Value[int] = Value(3) 128 | error: Error = Error(Exception()) 129 | outcome_one: Outcome[str] = [error, value_one][1] 130 | outcome_two: Outcome[int] = [error, value_two][1] 131 | 132 | assert_type(outcome_one.send(sync_generator_one()), int) 133 | assert_type(outcome_two.send(sync_generator_two()), str) 134 | outcome_one.send(sync_generator_two()) # type: ignore[arg-type] 135 | outcome_two.send(sync_generator_one()) # type: ignore[arg-type] 136 | 137 | assert_type(value_one.send(sync_generator_one()), int) 138 | assert_type(value_two.send(sync_generator_two()), str) 139 | value_one.send(sync_generator_two()) # type: ignore[arg-type] 140 | value_two.send(sync_generator_one()) # type: ignore[arg-type] 141 | 142 | # Error doesn't care. 143 | assert_type(error.send(sync_generator_one()), int) 144 | assert_type(error.send(sync_generator_two()), str) 145 | 146 | 147 | async def async_none() -> bool: 148 | return True 149 | 150 | 151 | async def async_raises() -> NoReturn: 152 | raise NotImplementedError 153 | 154 | 155 | async def async_one(param: float) -> int: 156 | return round(param) 157 | 158 | 159 | async def async_generator_one() -> AsyncGenerator[int, str]: 160 | assert_type((yield 5), str) 161 | 162 | 163 | async def async_generator_two() -> AsyncGenerator[str, int]: 164 | assert_type((yield ''), int) 165 | 166 | 167 | async def async_capture_test() -> None: 168 | """Test asynchronous behaviour.""" 169 | assert_type(await acapture(async_none), Union[Value[bool], Error]) 170 | assert_type(await acapture(async_one, 3.14), Union[Value[int], Error]) 171 | assert_type( 172 | await acapture(async_one, param=3.14), Union[Value[int], Error] 173 | ) 174 | assert_type(await acapture(async_raises), Error) 175 | capture(async_one) # type: ignore[call-overload] 176 | capture(async_none, 1, 2) # type: ignore[call-overload] 177 | 178 | 179 | async def async_gen_test() -> None: 180 | value_one: Value[str] = Value('abc') 181 | value_two: Value[int] = Value(3) 182 | error: Error = Error(Exception()) 183 | outcome_one: Outcome[str] = [error, value_one][1] 184 | outcome_two: Outcome[int] = [error, value_two][1] 185 | 186 | assert_type(await outcome_one.asend(async_generator_one()), int) 187 | assert_type(await outcome_two.asend(async_generator_two()), str) 188 | await outcome_one.asend(async_generator_two()) # type: ignore[arg-type] 189 | await outcome_two.asend(async_generator_one()) # type: ignore[arg-type] 190 | 191 | assert_type(await value_one.asend(async_generator_one()), int) 192 | assert_type(await value_two.asend(async_generator_two()), str) 193 | await value_one.asend(async_generator_two()) # type: ignore[arg-type] 194 | await value_two.asend(async_generator_one()) # type: ignore[arg-type] 195 | 196 | # Error doesn't care. 197 | assert_type(await error.asend(async_generator_one()), int) 198 | assert_type(await error.asend(async_generator_two()), str) 199 | --------------------------------------------------------------------------------