├── .coveragerc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .readthedocs.yml ├── .style.yapf ├── CHEATSHEET.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.APACHE2 ├── LICENSE.MIT ├── MANIFEST.in ├── Makefile ├── README.rst ├── ci.sh ├── debian ├── .gitignore ├── changelog ├── compat ├── control ├── copyright ├── rules ├── source │ └── format └── watch ├── docs-requirements.txt ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── .gitkeep │ ├── conf.py │ ├── history.rst │ ├── index.rst │ ├── principles.rst │ └── usage.rst ├── newsfragments ├── ###.misc.rst ├── .gitkeep └── README.rst ├── pyproject.toml ├── setup.py ├── test-requirements.txt ├── tests ├── __init__.py ├── aiotest │ ├── __init__.py │ ├── conftest.py │ ├── test_add_reader.py │ ├── test_callback.py │ ├── test_coroutine.py │ ├── test_network.py │ ├── test_thread.py │ └── test_timer.py ├── conftest.py ├── interop │ ├── __init__.py │ ├── test_adapter.py │ └── test_calls.py ├── module_with_deprecations.py ├── python │ └── __init__.py ├── scripts │ ├── echo.py │ ├── echo2.py │ └── echo3.py ├── test_aio_subprocess.py ├── test_concurrent.py ├── test_deprecate.py ├── test_misc.py ├── test_sync.py ├── test_trio_asyncio.py └── utils.py ├── tox.ini └── trio_asyncio ├── __init__.py ├── _adapter.py ├── _async.py ├── _base.py ├── _child.py ├── _deprecate.py ├── _handles.py ├── _loop.py ├── _sync.py ├── _util.py └── _version.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch=True 3 | source=trio_asyncio 4 | 5 | [report] 6 | precision = 1 7 | exclude_lines = 8 | pragma: no cover 9 | abc.abstractmethod 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | pull_request: 8 | 9 | jobs: 10 | Windows: 11 | name: 'Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }})' 12 | timeout-minutes: 30 13 | runs-on: 'windows-latest' 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python: ['3.9', '3.10', '3.11', '3.12', '3.13'] 18 | arch: ['x86', 'x64'] 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | - name: Setup python 23 | uses: actions/setup-python@v4 24 | with: 25 | # This allows the matrix to specify just the major.minor version while still 26 | # expanding it to get the latest patch version including alpha releases. 27 | # This avoids the need to update for each new alpha, beta, release candidate, 28 | # and then finally an actual release version. actions/setup-python doesn't 29 | # support this for PyPy presently so we get no help there. 30 | # 31 | # CPython -> 3.9.0-alpha - 3.9.X 32 | # PyPy -> pypy-3.7 33 | python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} 34 | architecture: '${{ matrix.arch }}' 35 | cache: pip 36 | cache-dependency-path: test-requirements.txt 37 | - name: Run tests 38 | run: ./ci.sh 39 | shell: bash 40 | - if: always() 41 | uses: codecov/codecov-action@v3 42 | with: 43 | directory: empty 44 | name: Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }}) 45 | flags: Windows,${{ matrix.python }} 46 | 47 | Ubuntu: 48 | name: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' 49 | timeout-minutes: 20 50 | runs-on: 'ubuntu-latest' 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | python: ['pypy-3.9', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.9-nightly'] 55 | check_formatting: ['0'] 56 | extra_name: [''] 57 | include: 58 | - python: '3.11' 59 | check_formatting: '1' 60 | extra_name: ', check formatting' 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v3 64 | - name: Setup python 65 | uses: actions/setup-python@v4 66 | if: "!endsWith(matrix.python, '-dev')" 67 | with: 68 | python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} 69 | cache: pip 70 | cache-dependency-path: test-requirements.txt 71 | - name: Setup python (dev) 72 | uses: deadsnakes/action@v2.0.2 73 | if: endsWith(matrix.python, '-dev') 74 | with: 75 | python-version: '${{ matrix.python }}' 76 | - name: Install python testsuite 77 | if: endsWith(matrix.python, '-dev') 78 | run: | 79 | version=$(echo $PYVERSION | sed 's/-dev//') 80 | sudo apt-get install -y --no-install-recommends libpython${version}-testsuite 81 | env: 82 | PYVERSION: '${{ matrix.python }}' 83 | - name: Run tests 84 | run: ./ci.sh 85 | env: 86 | CHECK_FORMATTING: '${{ matrix.check_formatting }}' 87 | - if: always() 88 | uses: codecov/codecov-action@v3 89 | with: 90 | directory: empty 91 | name: Ubuntu (${{ matrix.python }}${{ matrix.extra_name }}) 92 | flags: Ubuntu,${{ matrix.python }} 93 | 94 | macOS: 95 | name: 'macOS (${{ matrix.python }})' 96 | timeout-minutes: 20 97 | runs-on: 'macos-latest' 98 | strategy: 99 | fail-fast: false 100 | matrix: 101 | python: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.9-nightly'] 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@v3 105 | - name: Setup python 106 | uses: actions/setup-python@v4 107 | with: 108 | python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} 109 | cache: pip 110 | cache-dependency-path: test-requirements.txt 111 | - name: Run tests 112 | run: ./ci.sh 113 | - if: always() 114 | uses: codecov/codecov-action@v3 115 | with: 116 | directory: empty 117 | name: macOS (${{ matrix.python }}) 118 | flags: macOS,${{ matrix.python }} 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | 3 | /.pytest_cache/ 4 | /.coverage 5 | /.cache/ 6 | /.eggs/ 7 | /.pybuild/ 8 | /build/ 9 | /docs/build/ 10 | __pycache__/ 11 | /trio_asyncio.egg-info/ 12 | /.pybuild/ 13 | /dist 14 | /empty 15 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/latest/yaml-config.html 2 | version: 2 3 | 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: docs-requirements.txt 16 | 17 | sphinx: 18 | configuration: docs/source/conf.py 19 | fail_on_warning: true 20 | -------------------------------------------------------------------------------- /.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=99 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 ci/test-requirements.txt`` 8 | (possibly in a virtualenv) 9 | 10 | * Actually run the tests: ``PYTHONPATH=. pytest-3 tests`` 11 | 12 | 13 | To run yapf 14 | ----------- 15 | 16 | * Show what changes yapf wants to make: 17 | ``yapf3 -rpd setup.py trio_asyncio tests`` 18 | 19 | * Apply all changes directly to the source tree: 20 | ``yapf -rpi setup.py trio_asyncio tests`` 21 | 22 | * Find semantic problems: ``flake8 setup.py trio_asyncio tests`` 23 | 24 | 25 | To make a release 26 | ----------------- 27 | 28 | * Update the version in ``trio_asyncio/_version.py`` 29 | 30 | * Run ``towncrier`` to collect your release notes. 31 | 32 | * Review your release notes. 33 | 34 | * Check everything in. 35 | 36 | * Double-check it all works, docs build, etc. 37 | 38 | * Upload to PyPI: ``make upload`` 39 | 40 | * Don't forget to ``git push --tags``. 41 | 42 | -------------------------------------------------------------------------------- /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 3 | trio are 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 LICENSE* CODE_OF_CONDUCT* CONTRIBUTING* 2 | include .coveragerc 3 | include ci/test-requirements.txt 4 | recursive-include tests *.py 5 | recursive-exclude tests *.pyc 6 | prune tests/.pytest_cache 7 | recursive-include docs * 8 | prune docs/build 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | .PHONY: doc test update all tag pypi upload 4 | 5 | all: 6 | @echo "Please use 'python setup.py'." 7 | @exit 1 8 | 9 | # need to use python3 sphinx-build 10 | PATH := /usr/share/sphinx/scripts/python3:${PATH} 11 | 12 | PACKAGE = trio_asyncio 13 | PYTHON ?= python3 14 | export PYTHONPATH=$(shell pwd) 15 | 16 | PYTEST ?= ${PYTHON} $(shell which pytest-3) 17 | TEST_OPTIONS ?= -xvvv --full-trace 18 | PYLINT_RC ?= .pylintrc 19 | 20 | BUILD_DIR ?= build 21 | INPUT_DIR ?= docs/source 22 | 23 | # Sphinx options (are passed to build_docs, which passes them to sphinx-build) 24 | # -W : turn warning into errors 25 | # -a : write all files 26 | # -b html : use html builder 27 | # -i [pat] : ignore pattern 28 | 29 | SPHINXOPTS ?= -a -W -b html 30 | AUTOSPHINXOPTS := -i *~ -i *.sw* -i Makefile* 31 | 32 | SPHINXBUILDDIR ?= $(BUILD_DIR)/sphinx/html 33 | ALLSPHINXOPTS ?= -d $(BUILD_DIR)/sphinx/doctrees $(SPHINXOPTS) docs 34 | 35 | doc: 36 | sphinx3-build -a $(INPUT_DIR) $(BUILD_DIR) 37 | 38 | livehtml: docs 39 | sphinx-autobuild $(AUTOSPHINXOPTS) $(ALLSPHINXOPTS) $(SPHINXBUILDDIR) 40 | 41 | test: 42 | $(PYTEST) $(PACKAGE) $(TEST_OPTIONS) 43 | 44 | 45 | tag: 46 | @-git tag v$(shell python3 setup.py -V) 47 | 48 | pypi: tag 49 | @if python3 setup.py -V 2>/dev/null | grep -qs + >/dev/null 2>&1 ; \ 50 | then echo "You need a clean, tagged tree" >&2; exit 1 ; fi 51 | python3 setup.py sdist upload 52 | ## version depends on tag, so re-tagging doesn't make sense 53 | 54 | upload: pypi 55 | git push-all --tags 56 | 57 | update: 58 | pip install -r ci/test-requirements.txt 59 | 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/trio-asyncio.svg 2 | :target: https://pypi.org/project/trio-asyncio 3 | :alt: Latest PyPI version 4 | 5 | .. image:: https://img.shields.io/badge/chat-join%20now-blue.svg 6 | :target: https://gitter.im/python-trio/general 7 | :alt: Join chatroom 8 | 9 | .. image:: https://img.shields.io/badge/docs-read%20now-blue.svg 10 | :target: https://trio-asyncio.readthedocs.io/en/latest/?badge=latest 11 | :alt: Documentation status 12 | 13 | .. image:: https://github.com/python-trio/trio-asyncio/actions/workflows/ci.yml/badge.svg 14 | :target: https://github.com/python-trio/trio-asyncio/actions/workflows/ci.yml 15 | :alt: Automated test status 16 | 17 | .. image:: https://codecov.io/gh/python-trio/trio-asyncio/branch/master/graph/badge.svg 18 | :target: https://codecov.io/gh/python-trio/trio-asyncio 19 | :alt: Test coverage 20 | 21 | 22 | ============== 23 | trio-asyncio 24 | ============== 25 | 26 | **trio-asyncio** is a re-implementation of the ``asyncio`` mainloop on top of 27 | Trio. 28 | 29 | trio-asyncio requires at least Python 3.9. It is tested on recent versions of 30 | 3.9 through 3.13. 31 | 32 | +++++++++++ 33 | Rationale 34 | +++++++++++ 35 | 36 | Trio has native concepts of tasks and task cancellation. Asyncio is based 37 | on callbacks and chaining Futures, albeit with nicer syntax, making 38 | handling failures and timeouts fundamentally less reliable, especially in 39 | larger programs. Thus, you *really* want to base your async project on Trio. 40 | 41 | On the other hand, there are quite a few asyncio-enhanced libraries. You 42 | *really* don't want to re-invent any wheels in your project. 43 | 44 | Thus, being able to use asyncio libraries from Trio is useful. 45 | trio-asyncio enables you to do that and more. 46 | 47 | -------------------------------------- 48 | Transparent vs. explicit translation 49 | -------------------------------------- 50 | 51 | ``trio_asyncio`` does not try to magically allow calling ``await 52 | trio_code()`` from asyncio or vice versa. There are multiple reasons for 53 | this; the executive summary is that cross-domain calls can't be made to 54 | work correctly, and any such call is likely to result in an irrecoverable 55 | error. You need to keep your code's ``asyncio`` and ``trio`` domains 56 | rigidly separate. 57 | 58 | Fortunately, this is not difficult. 59 | 60 | +++++++ 61 | Usage 62 | +++++++ 63 | 64 | Trio-Asyncio's documentation is too large for a README. 65 | 66 | For further information, `see the manual on readthedocs `_. 67 | 68 | ++++++++++++++ 69 | Contributing 70 | ++++++++++++++ 71 | 72 | Like Trio, trio-asyncio is licensed under both the MIT and Apache licenses. 73 | Submitting a patch or pull request implies your acceptance of these licenses. 74 | 75 | Testing is done with ``pytest``. Test coverage is pretty thorough; please 76 | keep it that way when adding new code. 77 | 78 | See the `Trio contributor guide 79 | `__ for much 80 | more detail on how to get involved. 81 | 82 | Contributors are requested to follow our `code of conduct 83 | `__ in all 84 | project spaces. 85 | 86 | ++++++++ 87 | Author 88 | ++++++++ 89 | 90 | Matthias Urlichs originally wrote trio-asyncio. 91 | It is now maintained by the `Trio project `_. 92 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex -o pipefail 4 | 5 | # Log some general info about the environment 6 | uname -a 7 | env | sort 8 | 9 | if [ "$JOB_NAME" = "" ]; then 10 | JOB_NAME="${TRAVIS_OS_NAME}-${TRAVIS_PYTHON_VERSION:-unknown}" 11 | fi 12 | 13 | # Curl's built-in retry system is not very robust; it gives up on lots of 14 | # network errors that we want to retry on. Wget might work better, but it's 15 | # not installed on azure pipelines's windows boxes. So... let's try some good 16 | # old-fashioned brute force. (This is also a convenient place to put options 17 | # we always want, like -f to tell curl to give an error if the server sends an 18 | # error response, and -L to follow redirects.) 19 | function curl-harder() { 20 | for BACKOFF in 0 1 2 4 8 15 15 15 15; do 21 | sleep $BACKOFF 22 | if curl -fL --connect-timeout 5 "$@"; then 23 | return 0 24 | fi 25 | done 26 | return 1 27 | } 28 | 29 | ################################################################ 30 | # We have a Python environment! 31 | ################################################################ 32 | 33 | python -c "import sys, struct, ssl; print('#' * 70); print('python:', sys.version); print('version_info:', sys.version_info); print('bits:', struct.calcsize('P') * 8); print('openssl:', ssl.OPENSSL_VERSION, ssl.OPENSSL_VERSION_INFO); print('#' * 70)" 34 | 35 | python -m pip install -U pip setuptools wheel 36 | python -m pip --version 37 | 38 | python -m pip install . 39 | 40 | BLACK_VERSION=24.1.0 41 | 42 | if [ "$CHECK_FORMATTING" = "1" ]; then 43 | pip install black==${BLACK_VERSION} 44 | if ! black --check setup.py tests trio_asyncio; then 45 | black --diff setup.py tests trio_asyncio 46 | cat < Mon, 25 Feb 2019 14:38:47 +0100 6 | 7 | trio-asyncio (0.10.0-3) unstable; urgency=medium 8 | 9 | * Merged trio deprecation fix 10 | 11 | -- Matthias Urlichs Mon, 25 Feb 2019 14:33:54 +0100 12 | 13 | trio-asyncio (0.10.0-2) unstable; urgency=medium 14 | 15 | * Require trio 0.11 16 | 17 | -- Matthias Urlichs Mon, 25 Feb 2019 12:37:16 +0100 18 | 19 | trio-asyncio (0.10.0-1) unstable; urgency=medium 20 | 21 | * New release 22 | * Fix deprecation warnings 23 | * Fix cross-references in doc 24 | 25 | -- Matthias Urlichs Sun, 09 Dec 2018 10:20:44 +0100 26 | 27 | trio-asyncio (0.8.4-1) unstable; urgency=medium 28 | 29 | * Fix python_requires 30 | 31 | -- Matthias Urlichs Sat, 25 Aug 2018 20:38:16 +0200 32 | 33 | trio-asyncio (0.8.3-1) unstable; urgency=medium 34 | 35 | * sniffio fixes 36 | 37 | -- Matthias Urlichs Sat, 25 Aug 2018 20:36:01 +0200 38 | 39 | trio-asyncio (0.8.0-2) unstable; urgency=medium 40 | 41 | * Test fixes 42 | 43 | -- Matthias Urlichs Thu, 09 Aug 2018 14:50:49 +0200 44 | 45 | trio-asyncio (0.8.0-1) unstable; urgency=medium 46 | 47 | * Fix Trio dependency 48 | * Override asyncio's process-global loop policy handling 49 | 50 | -- Matthias Urlichs Fri, 03 Aug 2018 13:37:28 +0200 51 | 52 | trio-asyncio (0.7.5-3) unstable; urgency=medium 53 | 54 | * depend on outcome 55 | 56 | -- Matthias Urlichs Mon, 23 Jul 2018 22:30:18 +0200 57 | 58 | trio-asyncio (0.7.5-2) unstable; urgency=medium 59 | 60 | * Test and .gitignore fix-ups 61 | 62 | -- Matthias Urlichs Mon, 23 Jul 2018 21:58:50 +0200 63 | 64 | trio-asyncio (0.7.5-1) unstable; urgency=medium 65 | 66 | * trio.hazmat.Result is deprecated. 67 | 68 | -- Matthias Urlichs Mon, 23 Jul 2018 20:54:55 +0200 69 | 70 | trio-asyncio (0.7.4-5) unstable; urgency=medium 71 | 72 | * Debianization 73 | 74 | -- Matthias Urlichs Tue, 29 May 2018 13:43:52 +0200 75 | 76 | trio-asyncio (0.7.4-4) unstable; urgency=medium 77 | 78 | * Merge larger-queue patch (for now) 79 | 80 | -- Matthias Urlichs Tue, 29 May 2018 13:38:02 +0200 81 | 82 | trio-asyncio (0.7.4-3) unstable; urgency=medium 83 | 84 | * Don't depend on pytest-trio 85 | 86 | -- Matthias Urlichs Tue, 29 May 2018 03:23:44 +0200 87 | 88 | trio-asyncio (0.7.4-2) unstable; urgency=medium 89 | 90 | * fix packaging 91 | 92 | -- Matthias Urlichs Fri, 25 May 2018 03:58:46 +0200 93 | 94 | trio-asyncio (0.7.4-1) unstable; urgency=medium 95 | 96 | * Depend on Trio 0.4 97 | 98 | -- Matthias Urlichs Sat, 14 Apr 2018 12:28:42 +0200 99 | 100 | trio-asyncio (0.7.3-1) unstable; urgency=medium 101 | 102 | * Use contextvars instead of TaskLocal 103 | 104 | -- Matthias Urlichs Wed, 04 Apr 2018 12:02:03 +0200 105 | 106 | trio-asyncio (0.7.2-1) unstable; urgency=medium 107 | 108 | * TrioChildWatcher needs to descend from AbstractChildWatcher 109 | (if not Windows) 110 | 111 | -- Matthias Urlichs Tue, 03 Apr 2018 09:39:35 +0200 112 | 113 | trio-asyncio (0.7.1-1) unstable; urgency=medium 114 | 115 | * Handle errors in async generators 116 | 117 | -- Matthias Urlichs Thu, 29 Mar 2018 09:11:53 +0200 118 | 119 | trio-asyncio (0.7.0-1) unstable; urgency=medium 120 | 121 | * Enable Python 3.5 compatibility 122 | * allow parallel and nested native asyncio loops 123 | * minor bug fixes 124 | 125 | -- Matthias Urlichs Tue, 27 Mar 2018 17:03:35 +0200 126 | 127 | trio-asyncio (0.6.0-1) unstable; urgency=medium 128 | 129 | * Export loop.synchronize() 130 | * Concurrency bug fixes 131 | 132 | -- Matthias Urlichs Sun, 11 Mar 2018 21:57:13 +0100 133 | 134 | trio-asyncio (0.5.0-2) unstable; urgency=medium 135 | 136 | * Version 0.5 (beta) 137 | 138 | -- Matthias Urlichs Tue, 20 Feb 2018 13:52:29 +0100 139 | 140 | trio-asyncio (0.4.6-1) unstable; urgency=medium 141 | 142 | * Updated, Travis tests work (mostly) 143 | 144 | -- Matthias Urlichs Sun, 18 Feb 2018 06:08:48 +0100 145 | 146 | trio-asyncio (0.4.5-1) unstable; urgency=medium 147 | 148 | * Updated version 149 | 150 | -- Matthias Urlichs Fri, 16 Feb 2018 10:24:55 +0100 151 | 152 | trio-asyncio (0.4.2-1) unstable; urgency=medium 153 | 154 | * Skip a flaky testcase 155 | 156 | -- Matthias Urlichs Mon, 12 Feb 2018 21:23:13 +0100 157 | 158 | trio-asyncio (0.4.1-2) unstable; urgency=medium 159 | 160 | * … and updated the version number. 161 | 162 | -- Matthias Urlichs Mon, 12 Feb 2018 21:20:29 +0100 163 | 164 | trio-asyncio (0.4.1-1) unstable; urgency=medium 165 | 166 | * Fixed a race condition 167 | 168 | -- Matthias Urlichs Thu, 08 Feb 2018 04:12:38 +0100 169 | 170 | trio-asyncio (0.4-1) unstable; urgency=medium 171 | 172 | * Pyhon 3.6.4 asyncio tests work 173 | 174 | -- Matthias Urlichs Wed, 07 Feb 2018 20:26:48 +0100 175 | 176 | trio-asyncio (0.3-2) unstable; urgency=medium 177 | 178 | * source package automatically created by stdeb 0.8.5 179 | 180 | -- Matthias Urlichs Thu, 25 Jan 2018 12:00:10 +0100 181 | 182 | trio-asyncio (0.3-1) unstable; urgency=medium 183 | 184 | * Version 0.3 185 | * added trio2aio and aio2trio decorators 186 | 187 | -- Matthias Urlichs Tue, 17 Oct 2017 12:57:23 +0200 188 | 189 | trio-asyncio (0.2-2) unstable; urgency=medium 190 | 191 | * ignore .pybuild 192 | 193 | -- Matthias Urlichs Wed, 11 Oct 2017 11:37:32 +0200 194 | 195 | trio-asyncio (0.2-1) unstable; urgency=low 196 | 197 | * initial packaging 198 | 199 | -- Matthias Urlichs Mon, 09 Oct 2017 11:36:30 +0200 200 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: trio-asyncio 2 | Maintainer: Matthias Urlichs 3 | Section: python 4 | Priority: optional 5 | Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), 6 | python3-trio (>= 0.11.0), 7 | python3-pytest-trio, 8 | python3-pytest-runner, 9 | python3-outcome, 10 | Standards-Version: 3.9.6 11 | Homepage: https://github.com/python-trio/trio-asyncio 12 | 13 | Package: python3-trio-asyncio 14 | Architecture: all 15 | Depends: ${misc:Depends}, ${python3:Depends}, 16 | python3-trio (>= 0.11.0), 17 | python3-outcome, 18 | Description: Re-implementation of the asyncio mainloop on top of Trio 19 | trio_asyncio allows you to call asyncio code from within Trio, 20 | or vice versa. 21 | . 22 | While there's a compatibility mode which allows you to use a standard 23 | asyncio mainloop, trio-asyncio works best when used with a Trio mainloop. 24 | Examples how to convert one to the other are included in the documentation. 25 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: Trio 3 | Upstream-Contact: Nathaniel J. Smith 4 | Source: https://github.com/python-trio/trio 5 | 6 | Files: * 7 | Copyright: 2017-2018 Nathaniel J. Smith and contributors 8 | License: MIT or Apache-2.0 9 | 10 | License: MIT 11 | The MIT License (MIT) 12 | . 13 | Permission is hereby granted, free of charge, to any person obtaining 14 | a copy of this software and associated documentation files (the 15 | "Software"), to deal in the Software without restriction, including 16 | without limitation the rights to use, copy, modify, merge, publish, 17 | distribute, sublicense, and/or sell copies of the Software, and to 18 | permit persons to whom the Software is furnished to do so, subject to 19 | the following conditions: 20 | . 21 | The above copyright notice and this permission notice shall be 22 | included in all copies or substantial portions of the Software. 23 | . 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 27 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 28 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 29 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 30 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | 32 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # This file was automatically generated by stdeb 0.8.5 at 4 | # Mon, 09 Oct 2017 11:36:30 +0200 5 | export PYBUILD_NAME=trio-asyncio 6 | export PYBUILD_TEST_PYTEST=1 7 | export PYTEST_ADDOPTS=-sxv 8 | %: 9 | dh $@ --with python3 --buildsystem=pybuild 10 | 11 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | # please also check http://pypi.debian.net/trio_asyncio/watch 2 | version=3 3 | opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ 4 | http://pypi.debian.net/trio_asyncio/trio_asyncio-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) -------------------------------------------------------------------------------- /docs-requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx >= 1.7.0 2 | sphinx_rtd_theme 3 | sphinxcontrib-trio 4 | towncrier 5 | trio >= 0.25.0 6 | outcome 7 | attrs 8 | greenlet 9 | -------------------------------------------------------------------------------- /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 = trio-asyncio 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=trio-asyncio 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/trio-asyncio/8d6744baaab324c7fdfb3258ea485296548036e9/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Jan 21 19:11:14 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | # So autodoc can import our package 23 | sys.path.insert(0, os.path.abspath('../..')) 24 | 25 | # https://docs.readthedocs.io/en/stable/builds.html#build-environment 26 | if "READTHEDOCS" in os.environ: 27 | import glob 28 | if glob.glob("../../newsfragments/*.*.rst"): 29 | print("-- Found newsfragments; running towncrier --", flush=True) 30 | import subprocess 31 | subprocess.run( 32 | ["towncrier", "--yes", "--date", "not released yet"], 33 | cwd="../..", 34 | check=True, 35 | ) 36 | 37 | # Warn about all references to unknown targets 38 | nitpicky = True 39 | # Except for these ones, which we expect to point to unknown targets: 40 | nitpick_ignore = [ 41 | # Format is ("sphinx reference type", "string"), e.g.: 42 | ("py:class", "CapacityLimiter-like object"), 43 | ("py:class", "bytes-like"), 44 | ("py:class", "None"), 45 | ] 46 | 47 | autodoc_inherit_docstrings = False 48 | default_role = "obj" 49 | 50 | # -- General configuration ------------------------------------------------ 51 | 52 | # If your documentation needs a minimal Sphinx version, state it here. 53 | # 54 | # needs_sphinx = '1.0' 55 | 56 | # Add any Sphinx extension module names here, as strings. They can be 57 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 58 | # ones. 59 | extensions = [ 60 | 'sphinx.ext.autodoc', 61 | 'sphinx.ext.intersphinx', 62 | 'sphinx.ext.coverage', 63 | 'sphinx.ext.napoleon', 64 | 'sphinxcontrib_trio', 65 | ] 66 | 67 | intersphinx_mapping = { 68 | "python": ('https://docs.python.org/3', None), 69 | "trio": ('https://trio.readthedocs.io/en/stable', None), 70 | "sniffio": ('https://sniffio.readthedocs.io/en/latest', None), 71 | } 72 | 73 | autodoc_member_order = "bysource" 74 | 75 | # Add any paths that contain templates here, relative to this directory. 76 | templates_path = [] 77 | 78 | # The suffix(es) of source filenames. 79 | # You can specify multiple suffix as a list of string: 80 | # 81 | # source_suffix = ['.rst', '.md'] 82 | source_suffix = '.rst' 83 | 84 | # The master toctree document. 85 | master_doc = 'index' 86 | 87 | # General information about the project. 88 | project = 'trio-asyncio' 89 | copyright = '© 2018, Matthias Urlichs' 90 | author = 'Matthias Urlichs' 91 | 92 | # The version info for the project you're documenting, acts as replacement for 93 | # |version| and |release|, also used in various other places throughout the 94 | # built documents. 95 | # 96 | # The short X.Y version. 97 | import trio_asyncio 98 | version = trio_asyncio.__version__ 99 | # The full version, including alpha/beta/rc tags. 100 | release = version 101 | 102 | # The language for content autogenerated by Sphinx. Refer to documentation 103 | # for a list of supported languages. 104 | # 105 | # This is also used if you do content translation via gettext catalogs. 106 | # Usually you set "language" from the command line for these cases. 107 | language = "en" 108 | 109 | # List of patterns, relative to source directory, that match files and 110 | # directories to ignore when looking for source files. 111 | # This patterns also effect to html_static_path and html_extra_path 112 | exclude_patterns = [] 113 | 114 | # The name of the Pygments (syntax highlighting) style to use. 115 | pygments_style = 'sphinx' 116 | 117 | # The default language for :: blocks 118 | highlight_language = 'python3' 119 | 120 | # If true, `todo` and `todoList` produce output, else they produce nothing. 121 | todo_include_todos = False 122 | 123 | # -- Options for HTML output ---------------------------------------------- 124 | 125 | # The theme to use for HTML and HTML Help pages. See the documentation for 126 | # a list of builtin themes. 127 | # 128 | #html_theme = 'alabaster' 129 | 130 | # We have to set this ourselves, not only because it's useful for local 131 | # testing, but also because if we don't then RTD will throw away our 132 | # html_theme_options. 133 | import sphinx_rtd_theme 134 | html_theme = 'sphinx_rtd_theme' 135 | 136 | # Theme options are theme-specific and customize the look and feel of a theme 137 | # further. For a list of options available for each theme, see the 138 | # documentation. 139 | # 140 | html_theme_options = { 141 | # default is 2 142 | # show deeper nesting in the RTD theme's sidebar TOC 143 | # https://stackoverflow.com/questions/27669376/ 144 | # I'm not 100% sure this actually does anything with our current 145 | # versions/settings... 146 | "navigation_depth": 4, 147 | "logo_only": True, 148 | } 149 | 150 | # Add any paths that contain custom static files (such as style sheets) here, 151 | # relative to this directory. They are copied after the builtin static files, 152 | # so a file named "default.css" will overwrite the builtin "default.css". 153 | html_static_path = ['_static'] 154 | 155 | # -- Options for HTMLHelp output ------------------------------------------ 156 | 157 | # Output file base name for HTML help builder. 158 | htmlhelp_basename = 'trio-asyncio-doc' 159 | 160 | # -- Options for LaTeX output --------------------------------------------- 161 | 162 | latex_elements = { 163 | # The paper size ('letterpaper' or 'a4paper'). 164 | # 165 | # 'papersize': 'letterpaper', 166 | 167 | # The font size ('10pt', '11pt' or '12pt'). 168 | # 169 | # 'pointsize': '10pt', 170 | 171 | # Additional stuff for the LaTeX preamble. 172 | # 173 | # 'preamble': '', 174 | 175 | # Latex figure (float) alignment 176 | # 177 | # 'figure_align': 'htbp', 178 | } 179 | 180 | # Grouping the document tree into LaTeX files. List of tuples 181 | # (source start file, target name, title, 182 | # author, documentclass [howto, manual, or own class]). 183 | latex_documents = [ 184 | (master_doc, 'trio-asyncio.tex', 'Trio Documentation', author, 'manual'), 185 | ] 186 | 187 | # -- Options for manual page output --------------------------------------- 188 | 189 | # One entry per manual page. List of tuples 190 | # (source start file, name, description, authors, manual section). 191 | man_pages = [(master_doc, 'trio-asyncio', 'trio-asyncio Documentation', [author], 1)] 192 | 193 | # -- Options for Texinfo output ------------------------------------------- 194 | 195 | # Grouping the document tree into Texinfo files. List of tuples 196 | # (source start file, target name, title, author, 197 | # dir menu entry, description, category) 198 | texinfo_documents = [ 199 | ( 200 | master_doc, 'trio-asyncio', 'trio-asyncio Documentation', author, 'trio-asyncio', 201 | 'A re-implementation of the asyncio mainloop on top of Trio', 'Miscellaneous' 202 | ), 203 | ] 204 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. documentation master file, created by 2 | sphinx-quickstart on Sat Jan 21 19:11:14 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | ======================================================================== 8 | trio-asyncio: A re-implementation of the asyncio mainloop on top of Trio 9 | ======================================================================== 10 | 11 | trio-asyncio is the library of choice for a Python program that 12 | contains both `Trio `__ and 13 | `asyncio` code. 14 | 15 | Trio has native concepts of tasks and task cancellation. Asyncio is based 16 | on callbacks and chaining Futures, albeit with nicer syntax, which make 17 | handling of failures and timeouts fundamentally less reliable, especially in 18 | larger programs. Thus, you really want to use Trio in your project. 19 | 20 | On the other hand, there are quite a few robust libraries that have been 21 | implemented using asyncio, while Trio's ecosystem is relatively younger. You 22 | really don't want to re-invent any wheels in your project. 23 | 24 | Thus, being able to use asyncio libraries from Trio is useful. 25 | trio-asyncio enables you to do that, and more. 26 | 27 | With trio-asyncio, you can: 28 | 29 | * Incrementally convert an asyncio application to Trio. Start with a 30 | Trio mainloop, call your existing asyncio code, then successively 31 | convert procedures to Trio calling conventions. 32 | 33 | * Use any asyncio-capable library in a Trio application. 34 | 35 | * Use trio-asyncio as a building block for convincing other async-ish 36 | libraries (Twisted, Promise, …) to be compatible with Trio. 37 | 38 | trio-asyncio is tested against the complete asyncio test suite as 39 | shipped with each supported version of Python, although a small number of tests 40 | fail due to our limited support for starting and stopping the same event 41 | loop multiple times. It has also passed the test suite of some complex 42 | asyncio libraries such as ``home-assistant``. 43 | 44 | .. note:: trio-asyncio is most useful for *applications*: it works best 45 | when you control the code that starts the event loop (such as 46 | the call to :func:`asyncio.run`). If you're writing a *library* and 47 | want to adopt a Trio-ish worldview without sacrificing asyncio 48 | compatibility, you might find `anyio `__ 49 | helpful. 50 | 51 | Helpful facts: 52 | 53 | * Supported environments: Linux, MacOS, or Windows running some kind of Python 54 | 3.7-or-better (either CPython or PyPy3 is fine). \*BSD and illumOS likely 55 | work too, but are untested. 56 | 57 | * Install: ``python3 -m pip install -U trio-asyncio`` (or on Windows, maybe 58 | ``py -3 -m pip install -U trio-asyncio``). No compiler needed. 59 | 60 | * Tutorial and reference manual: https://trio-asyncio.readthedocs.io 61 | 62 | * Bug tracker and source code: https://github.com/python-trio/trio-asyncio 63 | 64 | Inherited from `Trio `__: 65 | 66 | * Real-time chat: https://gitter.im/python-trio/general 67 | 68 | * License: MIT or Apache 2, your choice 69 | 70 | * Contributor guide: https://trio.readthedocs.io/en/latest/contributing.html 71 | 72 | * Code of conduct: Contributors are requested to follow our `code of 73 | conduct `_ 74 | in all project spaces. 75 | 76 | ===================== 77 | trio-asyncio manual 78 | ===================== 79 | 80 | .. toctree:: 81 | :maxdepth: 2 82 | 83 | principles.rst 84 | usage.rst 85 | history.rst 86 | 87 | ==================== 88 | Indices and tables 89 | ==================== 90 | 91 | * :ref:`genindex` 92 | * :ref:`modindex` 93 | * :ref:`search` 94 | * :ref:`glossary` 95 | -------------------------------------------------------------------------------- /docs/source/principles.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: trio_asyncio 2 | 3 | ++++++++++++ 4 | Principles 5 | ++++++++++++ 6 | 7 | -------------------------------------- 8 | Async function "flavors" 9 | -------------------------------------- 10 | 11 | As you might recall from the discussion of async "sandwiches" in the 12 | `Trio tutorial 13 | `__, 14 | every async function ultimately must do its useful work by directly or 15 | indirectly calling back into the same async library (such as asyncio 16 | or Trio) that's managing the currently running event loop. If a 17 | function invoked within a :func:`trio.run` calls 18 | :func:`asyncio.sleep`, or a function invoked within an 19 | :func:`asyncio.run` calls :func:`trio.sleep`, the sleep function will 20 | send a message to the event loop that the event loop doesn't know how 21 | to handle, and some sort of error will result. 22 | 23 | In a program that uses trio-asyncio, you probably have some async 24 | functions implemented in terms of Trio calls and some implemented in 25 | terms of asyncio calls. In order to keep track of which is which, 26 | we'll call these "Trio-flavored" and "asyncio-flavored" functions, 27 | respectively. It is critical that you understand which functions in 28 | your program are Trio-flavored and which are asyncio-flavored, just 29 | like it's critical that you understand which functions are synchronous 30 | and which ones are async. Unfortunately, there's no syntactic marker 31 | for flavor: both Trio-flavored and asyncio-flavored functions are 32 | defined with ``async def fn()`` and call other async functions with 33 | ``await other_fn()``. You'll have to keep track of it some other 34 | way. To help you out, every function in trio-asyncio documents its 35 | flavor, and we recommend that you follow this convention in your own 36 | programs too. 37 | 38 | The general rules that determine flavor are as follows: 39 | 40 | * Every async function in the ``trio`` module is Trio-flavored. 41 | Every async function in the ``asyncio`` module is asyncio-flavored. 42 | 43 | * Flavor is transitive: if async function ``foo()`` calls ``await 44 | bar()``, then ``foo()`` has ``bar()``'s flavor. (If ``foo()`` 45 | calls ``await baz()`` too, then ``bar()`` and ``baz()`` had better 46 | have the same flavor.) 47 | 48 | * trio-asyncio gives you the ability to call functions whose flavor is 49 | different than your own, but you must be explicit about it. 50 | :func:`trio_asyncio.aio_as_trio` takes an asyncio-flavored function 51 | and returns a Trio-flavored wrapper for it; 52 | :func:`trio_as_aio` takes a Trio-flavored function and 53 | returns an asyncio-flavored wrapper for it. 54 | 55 | If you don't keep track of your function flavors correctly, you might 56 | get exceptions like the following: 57 | 58 | * If you call a Trio function where an asyncio function is expected: ``RuntimeError: 59 | Task got bad yield:`` followed by either ``WaitTaskRescheduled(abort_func=...)`` 60 | or ```` 61 | 62 | * If you call an asyncio function where a Trio function is expected: ``TypeError: 63 | trio.run received unrecognized yield message .`` 64 | 65 | Other errors are possible too. 66 | 67 | ----------------------- 68 | Flavor versus context 69 | ----------------------- 70 | 71 | The concept of function flavor is distinct from the concept of 72 | "asyncio context" or "Trio context". You're in Trio context if you're 73 | (indirectly) inside a call to :func:`trio.run`. You're in asyncio 74 | context if :func:`asyncio.get_running_loop` returns a valid event 75 | loop. In a trio-asyncio program, you will frequently be in both Trio 76 | context and asyncio context at the same time, but each async function is 77 | either Trio-flavored or asyncio-flavored (not both). 78 | 79 | Most *synchronous* asyncio or Trio functions (:meth:`trio.Event.set`, 80 | :meth:`asyncio.StreamWriter.close`, etc) only require you to be in 81 | asyncio or Trio context, and work equally well regardless of the 82 | flavor of function calling them. The exceptions are functions that 83 | access the current task (:func:`asyncio.current_task`, 84 | :func:`trio.lowlevel.current_task`, and anything that calls them), 85 | because there's only a meaningful concept of the current *foo* task 86 | when a *foo*-flavored function is executing. For example, this means 87 | context managers that set a timeout on their body (``with 88 | async_timeout.timeout(N):``, ``with trio.move_on_after(N):``) must be 89 | run from within the correct flavor of function. 90 | 91 | --------------------------------- 92 | Flavor transitions are explicit 93 | --------------------------------- 94 | 95 | As mentioned above, trio-asyncio does not generally allow you to 96 | transparently call ``await trio.something()`` from asyncio code, nor 97 | vice versa; you need to use :func:`aio_as_trio` or 98 | :func:`trio_as_aio` when calling a function whose flavor is 99 | different than yours. This is certainly more frustrating than having 100 | it "just work". Unfortunately, semantic differences between Trio and 101 | asyncio (such as how to signal cancellation) need to be resolved at 102 | each boundary between asyncio and Trio, and we haven't found a way to 103 | do this with acceptable performance and robustness unless those 104 | boundaries are marked. 105 | 106 | If you insist on living on the wild side, trio-asyncio does provide 107 | :func:`allow_asyncio` which allows *limited, 108 | experimental, and slow* mixing of Trio-flavored and asyncio-flavored 109 | calls in the same Trio-flavored function. 110 | 111 | ------------------------------------------- 112 | trio-asyncio's place in the asyncio stack 113 | ------------------------------------------- 114 | 115 | At its base, asyncio doesn't know anything about futures or 116 | coroutines, nor does it have any concept of a task. All of these 117 | features are built on top of the simpler interfaces provided by the 118 | event loop. The event loop itself has little functionality beyond executing 119 | synchronous functions submitted with :meth:`~asyncio.loop.call_soon` 120 | and :meth:`~asyncio.loop.call_later` 121 | and invoking I/O availability callbacks registered using 122 | :meth:`~asyncio.loop.add_reader` and :meth:`~asyncio.loop.add_writer` 123 | at the appropriate times. 124 | 125 | trio-asyncio provides an asyncio event loop implementation which 126 | performs these basic operations using Trio APIs. Everything else in 127 | asyncio (futures, tasks, cancellation, and so on) is ultimately 128 | implemented in terms of calls to event loop methods, and thus works 129 | "magically" with Trio once the trio-asyncio event loop is 130 | installed. This strategy provides a high level of compatibility with 131 | asyncio libraries, but it also means that asyncio-flavored code 132 | running under trio-asyncio doesn't benefit much from Trio's more 133 | structured approach to concurrent programming: cancellation, 134 | causality, and exception propagation in asyncio-flavored code are just 135 | as error-prone under trio-asyncio as they are under the default 136 | asyncio event loop. (Of course, your Trio-flavored code will still 137 | benefit from all the usual Trio guarantees.) 138 | 139 | If you look at a Trio task tree, you'll see only one Trio task for the 140 | entire asyncio event loop. The distinctions between different asyncio 141 | tasks are erased, because they've all been merged into a single pot of 142 | callback soup by the time they get to trio-asyncio. Similarly, context 143 | variables will only work properly in asyncio-flavored code when 144 | running Python 3.7 or later (where they're supported natively), even 145 | though Trio supports them on earlier Pythons using a backport package. 146 | 147 | ---------------------------- 148 | Event loop implementations 149 | ---------------------------- 150 | 151 | A stock asyncio event loop may be interrupted and restarted at any 152 | time, simply by making repeated calls to :meth:`run_until_complete() 153 | `. 154 | Trio, however, requires one long-running main loop. trio-asyncio bridges 155 | this gap by providing two event loop implementations. 156 | 157 | * The preferred option is to use an "async loop": inside a 158 | Trio-flavored async function, write ``async with 159 | trio_asyncio.open_loop() as loop:``. Within the ``async with`` 160 | block (and anything it calls, and any tasks it starts, and so on), 161 | :func:`asyncio.get_event_loop` and :func:`asyncio.get_running_loop` 162 | will return *loop*. You can't manually start and stop an async 163 | loop. Instead, it starts when you enter the ``async with`` block and 164 | stops when you exit the block. 165 | 166 | * The other option is a "sync loop". 167 | If you've imported trio-asyncio but aren't in Trio context, and you haven't 168 | installed a custom event loop policy, calling :func:`asyncio.new_event_loop` 169 | (including the implicit call made by the first :func:`asyncio.get_event_loop` 170 | in the main thread) will give you an event loop that transparently runs 171 | in a separate greenlet in order to support multiple 172 | calls to :meth:`~asyncio.loop.run_until_complete`, 173 | :meth:`~asyncio.loop.run_forever`, and :meth:`~asyncio.loop.stop`. 174 | Sync loops are intended to allow trio-asyncio to run the existing 175 | test suites of large asyncio libraries, which often call 176 | :meth:`~asyncio.loop.run_until_complete` on the same loop multiple times. 177 | Using them for other purposes is not recommended (it is better to refactor 178 | so you can use an async loop) but will probably work. 179 | -------------------------------------------------------------------------------- /newsfragments/###.misc.rst: -------------------------------------------------------------------------------- 1 | Python 3.13 is now supported. Python 3.8 is no longer supported. 2 | -------------------------------------------------------------------------------- /newsfragments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/trio-asyncio/8d6744baaab324c7fdfb3258ea485296548036e9/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 | [tool.pytest.ini_options] 2 | addopts = ["-p", "no:asyncio"] 3 | filterwarnings = [ 4 | "error", 5 | "ignore:The loop argument is deprecated since Python 3.8:DeprecationWarning", 6 | 'ignore:"@coroutine" decorator is deprecated since Python 3.8:DeprecationWarning', 7 | "default:Tried to add marker .* but that test doesn't exist.:RuntimeWarning", 8 | "ignore:the imp module is deprecated in favour of importlib.*:DeprecationWarning", 9 | "ignore:'AbstractChildWatcher' is deprecated.*:DeprecationWarning" 10 | ] 11 | junit_family = "xunit2" 12 | xfail_strict = true 13 | 14 | [tool.flake8] 15 | max-line-length = 99 16 | extend-ignore = ['D', 'E402', 'E731', 'E127', 'E502', 'E123', 'W503'] 17 | 18 | [tool.towncrier] 19 | package = "trio_asyncio" 20 | title_format = "trio-asyncio {version} ({project_date})" 21 | filename = "docs/source/history.rst" 22 | directory = "newsfragments" 23 | underlines = ["-", "~", "^"] 24 | issue_format = "`#{issue} `__" 25 | 26 | [[tool.towncrier.type]] 27 | directory = "feature" 28 | name = "Features" 29 | showcontent = true 30 | 31 | [[tool.towncrier.type]] 32 | directory = "bugfix" 33 | name = "Bugfixes" 34 | showcontent = true 35 | 36 | [[tool.towncrier.type]] 37 | directory = "doc" 38 | name = "Improved documentation" 39 | showcontent = true 40 | 41 | [[tool.towncrier.type]] 42 | directory = "removal" 43 | name = "Deprecations and removals" 44 | showcontent = true 45 | 46 | [[tool.towncrier.type]] 47 | directory = "misc" 48 | name = "Miscellaneous" 49 | showcontent = true 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys 3 | 4 | exec(open("trio_asyncio/_version.py", encoding="utf-8").read()) 5 | 6 | LONG_DESC = """\ 7 | ``trio-asyncio`` is a re-implementation of the ``asyncio`` mainloop on top of 8 | Trio. 9 | 10 | Rationale 11 | ========= 12 | 13 | There are quite a few asyncio-compatible libraries. 14 | 15 | On the other hand, Trio has native concepts of tasks and task cancellation. 16 | Asyncio, on the other hand, is based on chaining Future objects, albeit 17 | with nicer syntax. 18 | 19 | Thus, being able to use asyncio libraries from Trio is useful. 20 | 21 | Principle of operation 22 | ====================== 23 | 24 | The core of the "normal" asyncio main loop is the repeated execution of 25 | synchronous code that's submitted to ``call_soon`` or 26 | ``add_reader``/``add_writer``. 27 | 28 | Everything else within ``asyncio``, i.e. Futures and ``async``/``await``, 29 | is just syntactic sugar. There is no concept of a task; while a Future can 30 | be cancelled, that in itself doesn't affect the code responsible for 31 | fulfilling it. 32 | 33 | On the other hand, trio has genuine tasks with no separation between 34 | returning a value asynchronously, and the code responsible for providing 35 | that value. 36 | 37 | ``trio_asyncio`` implements a task which runs (its own version of) the 38 | asyncio main loop. It also contains shim code which translates between these 39 | concepts as transparently and correctly as possible, and it supplants a few 40 | of the standard loop's key functions. 41 | 42 | This works rather well: ``trio_asyncio`` consists of just ~700 lines of 43 | code (asyncio: ~8000) but passes the complete Python 3.6 test suite with no 44 | errors. 45 | 46 | ``trio_asyncio`` requires Python 3.9 or later. 47 | 48 | Author 49 | ====== 50 | 51 | Matthias Urlichs 52 | 53 | """ 54 | 55 | setup( 56 | name="trio_asyncio", 57 | version=__version__, # noqa: F821 58 | description="A re-implementation of the asyncio mainloop on top of Trio", 59 | long_description=LONG_DESC, 60 | author="Matthias Urlichs", 61 | author_email="matthias@urlichs.de", 62 | url="https://github.com/python-trio/trio-asyncio", 63 | license="MIT -or- Apache License 2.0", 64 | packages=["trio_asyncio"], 65 | install_requires=[ 66 | "trio >= 0.22.0", 67 | "outcome", 68 | "sniffio >= 1.3.0", 69 | "exceptiongroup >= 1.0.0; python_version < '3.11'", 70 | "greenlet", 71 | ], 72 | # This means, just install *everything* you see under trio/, even if it 73 | # doesn't look like a source file, so long as it appears in MANIFEST.in: 74 | include_package_data=True, 75 | python_requires=">=3.9", 76 | keywords=["async", "io", "trio", "asyncio", "trio-asyncio"], 77 | setup_requires=["pytest-runner"], 78 | tests_require=["pytest >= 5.4", "pytest-trio >= 0.6", "outcome"], 79 | classifiers=[ 80 | "Development Status :: 4 - Beta", 81 | "Intended Audience :: Developers", 82 | "License :: OSI Approved :: MIT License", 83 | "License :: OSI Approved :: Apache Software License", 84 | "Operating System :: POSIX :: Linux", 85 | "Operating System :: MacOS :: MacOS X", 86 | "Operating System :: POSIX :: BSD", 87 | "Operating System :: Microsoft :: Windows", 88 | "Programming Language :: Python :: Implementation :: CPython", 89 | "Programming Language :: Python :: Implementation :: PyPy", 90 | "Programming Language :: Python :: 3 :: Only", 91 | "Programming Language :: Python :: 3.9", 92 | "Programming Language :: Python :: 3.10", 93 | "Programming Language :: Python :: 3.11", 94 | "Programming Language :: Python :: 3.12", 95 | "Programming Language :: Python :: 3.13", 96 | "Topic :: System :: Networking", 97 | "Framework :: Trio", 98 | "Framework :: AsyncIO", 99 | ], 100 | ) 101 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest >= 5.4 # for the Node.from_parent() transition 2 | pytest-cov 3 | pytest-trio 4 | outcome 5 | pytest-timeout 6 | trio >= 0.26.0 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # asyncio.loop logs some things we might need to see. 2 | import logging 3 | 4 | logging.basicConfig() 5 | del logging 6 | 7 | # asyncio runs many error messages through reprlib, 8 | # which defaults to fairly short strings, 9 | # which is a major PITA. 10 | from reprlib import aRepr 11 | 12 | aRepr.maxstring = 9999 13 | aRepr.maxother = 9999 14 | aRepr.maxlong = 9999 15 | del aRepr 16 | -------------------------------------------------------------------------------- /tests/aiotest/__init__.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import time 4 | 5 | import asyncio as _asyncio 6 | 7 | socketpair = socket.socketpair 8 | 9 | 10 | class TestConfig: 11 | def __init__(self): 12 | # Stop on first fail or error 13 | self.fail_fast = False 14 | 15 | # Run tests forever to catch sporadic errors 16 | self.forever = False 17 | 18 | # Verbosity 0..4: 0=less messages (CRITICAL), 4=more messages (DEBUG) 19 | self.verbosity = 0 20 | 21 | # List of test names to include, empty list means that all tests 22 | # are included 23 | self.includes = [] 24 | 25 | # List of test names to exclude, empty list means that no test is 26 | # excluded 27 | self.excludes = [] 28 | 29 | # modules 30 | self.asyncio = _asyncio 31 | self.socket = socket 32 | self.threading = threading 33 | 34 | # functions 35 | self.socketpair = socketpair 36 | self.sleep = time.sleep 37 | 38 | # features of the implementations 39 | 40 | # The event loop can be run in a thread different than the main thread? 41 | self.support_threads = True 42 | 43 | # http://bugs.python.org/issue22922 44 | # call_soon() now raises an exception when the event loop is closed 45 | self.call_soon_check_closed = True 46 | 47 | # http://bugs.python.org/issue25593 48 | # Change semantics of EventLoop.stop(). Replace _StopError exception 49 | # with a new stopping attribute. 50 | self.stopping = True 51 | 52 | def prepare(self, testcase): 53 | # import pdb;pdb.set_trace() 54 | # policy = self.new_event_pool_policy() 55 | # self.asyncio.set_event_loop_policy(policy) 56 | testcase.addCleanup(self.asyncio.set_event_loop_policy, None) 57 | 58 | testcase.loop = self.asyncio.get_event_loop() 59 | # testcase.addCleanup(testcase.loop.close) 60 | # testcase.addCleanup(self.asyncio.set_event_loop, None) 61 | 62 | 63 | class TestCase: 64 | pass 65 | 66 | 67 | # @classmethod 68 | # def setUpClass(cls): 69 | # cls.config = config 70 | # 71 | # def setUp(self): 72 | # self.config.prepare(self) 73 | -------------------------------------------------------------------------------- /tests/aiotest/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import TestConfig 3 | 4 | 5 | @pytest.fixture 6 | def config(): 7 | return TestConfig() 8 | -------------------------------------------------------------------------------- /tests/aiotest/test_add_reader.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from tests.aiotest import socketpair 3 | import pytest 4 | import trio 5 | 6 | 7 | class TestAddReader: 8 | @pytest.mark.trio 9 | async def test_add_reader(self, loop): 10 | result = {"received": None} 11 | rsock, wsock = socketpair() 12 | ready = trio.Event() 13 | try: 14 | 15 | def reader(): 16 | data = rsock.recv(100) 17 | result["received"] = data 18 | loop.remove_reader(rsock) 19 | ready.set() 20 | 21 | def writer(): 22 | loop.remove_writer(wsock) 23 | loop.call_soon(wsock.send, b"abc") 24 | 25 | loop.add_reader(rsock, reader) 26 | loop.add_writer(wsock, writer) 27 | 28 | await ready.wait() 29 | assert result["received"] == b"abc" 30 | 31 | finally: 32 | rsock.close() 33 | wsock.close() 34 | 35 | async def check_add_replace(self, event, loop, config): 36 | socket = config.socket 37 | 38 | selector = loop._selector 39 | if event == "reader": 40 | add_sock = loop.add_reader 41 | remove_sock = loop.remove_reader 42 | 43 | def get_handle(fileobj): 44 | return selector.get_key(fileobj).data[0] 45 | 46 | else: 47 | add_sock = loop.add_writer 48 | remove_sock = loop.remove_writer 49 | 50 | def get_handle(fileobj): 51 | return selector.get_key(fileobj).data[1] 52 | 53 | sock = socket.socket() 54 | try: 55 | 56 | def func(): 57 | pass 58 | 59 | def func2(): 60 | pass 61 | 62 | with pytest.raises(KeyError): 63 | get_handle(sock) 64 | 65 | add_sock(sock, func) 66 | handle1 = get_handle(sock) 67 | assert not handle1._cancelled 68 | 69 | add_sock(sock, func2) 70 | handle2 = get_handle(sock) 71 | assert handle1 is not handle2 72 | assert handle1._cancelled 73 | assert not handle2._cancelled 74 | 75 | removed = remove_sock(sock) 76 | assert removed 77 | assert handle2._cancelled 78 | 79 | removed = remove_sock(sock) 80 | assert not removed 81 | finally: 82 | sock.close() 83 | 84 | @pytest.mark.trio 85 | async def test_add_reader_replace(self, loop, config): 86 | await self.check_add_replace("reader", loop, config) 87 | 88 | @pytest.mark.trio 89 | async def test_add_writer_replace(self, loop, config): 90 | await self.check_add_replace("writer", loop, config) 91 | -------------------------------------------------------------------------------- /tests/aiotest/test_callback.py: -------------------------------------------------------------------------------- 1 | from tests import aiotest 2 | import signal 3 | import pytest 4 | 5 | 6 | class TestCallback(aiotest.TestCase): 7 | @pytest.mark.trio 8 | async def test_call_soon(self, loop): 9 | result = [] 10 | 11 | def hello_world(loop): 12 | result.append("Hello World") 13 | loop.stop() 14 | 15 | loop.call_soon(hello_world, loop) 16 | await loop.stop().wait() 17 | assert result == ["Hello World"] 18 | 19 | @pytest.mark.trio 20 | async def test_call_soon_control(self, loop): 21 | result = [] 22 | 23 | def func(result, loop): 24 | loop.call_soon(append_result, loop, result, "yes") 25 | result.append(str(result)) 26 | 27 | def append_result(loop, result, value): 28 | result.append(value) 29 | loop.stop() 30 | 31 | loop.call_soon(func, result, loop) 32 | await loop.wait_stopped() 33 | # http://bugs.python.org/issue22875: Ensure that call_soon() does not 34 | # call append_result() immediately, but when control returns to the 35 | # event loop, when func() is done. 36 | assert result == ["[]", "yes"] 37 | 38 | @pytest.mark.trio 39 | async def test_close(self, loop, config): 40 | if not config.call_soon_check_closed: 41 | # http://bugs.python.org/issue22922 not implemented 42 | self.skipTest("call_soon() doesn't raise if the event loop is closed") 43 | 44 | await loop.stop().wait() 45 | loop.close() 46 | 47 | async def test(): 48 | pass 49 | 50 | func = lambda: False 51 | coro = test() 52 | try: 53 | with pytest.raises(RuntimeError, match="not a sync loop"): 54 | loop.run_until_complete(None) 55 | with pytest.raises(RuntimeError): 56 | loop.run_forever() 57 | with pytest.raises(RuntimeError, match="Event loop is closed"): 58 | loop.call_soon(func) 59 | with pytest.raises(RuntimeError, match="Event loop is closed"): 60 | loop.call_soon_threadsafe(func) 61 | with pytest.raises(RuntimeError, match="Event loop is closed"): 62 | loop.call_later(1.0, func) 63 | with pytest.raises(RuntimeError, match="Event loop is closed"): 64 | loop.call_at(loop.time() + 0.0, func) 65 | with pytest.raises(RuntimeError, match="Event loop is closed"): 66 | loop.run_in_executor(None, func) 67 | with pytest.raises(RuntimeError, match="Event loop is closed"): 68 | await loop.run_aio_coroutine(coro) 69 | with pytest.raises(RuntimeError, match="Event loop is closed"): 70 | loop.add_signal_handler(signal.SIGTERM, func) 71 | finally: 72 | coro.close() 73 | -------------------------------------------------------------------------------- /tests/aiotest/test_coroutine.py: -------------------------------------------------------------------------------- 1 | from tests import aiotest 2 | import trio_asyncio 3 | import pytest 4 | 5 | 6 | async def hello_world(asyncio, result, delay, loop): 7 | result.append("Hello") 8 | # retrieve the event loop from the policy 9 | await asyncio.sleep(delay) 10 | result.append("World") 11 | return "." 12 | 13 | 14 | class TestCoroutine(aiotest.TestCase): 15 | @pytest.mark.trio 16 | async def test_hello_world(self, loop, config): 17 | result = [] 18 | coro = hello_world(config.asyncio, result, 0.001, loop) 19 | await loop.run_aio_coroutine(config.asyncio.ensure_future(coro)) 20 | assert result == ["Hello", "World"] 21 | 22 | @pytest.mark.trio 23 | async def test_waiter(self, loop, config): 24 | async def waiter(asyncio, hello_world, result): 25 | fut = asyncio.Future() 26 | loop.call_soon(fut.set_result, "Future") 27 | 28 | value = await fut 29 | result.append(value) 30 | 31 | value = await hello_world(asyncio, result, 0.001, loop) 32 | result.append(value) 33 | 34 | result = [] 35 | await trio_asyncio.aio_as_trio(waiter)(config.asyncio, hello_world, result) 36 | assert result == ["Future", "Hello", "World", "."] 37 | -------------------------------------------------------------------------------- /tests/aiotest/test_network.py: -------------------------------------------------------------------------------- 1 | from tests import aiotest 2 | import pytest 3 | 4 | 5 | def create_classes(config): 6 | asyncio = config.asyncio 7 | socket = config.socket 8 | threading = config.threading 9 | 10 | class TcpEchoClientProtocol(asyncio.Protocol): 11 | def __init__(self, message, loop): 12 | self.message = message 13 | self.loop = loop 14 | self.state = "new" 15 | self.received = None 16 | 17 | def connection_made(self, transport): 18 | self.state = "ping" 19 | transport.write(self.message) 20 | 21 | def data_received(self, data): 22 | self.state = "pong" 23 | self.received = data 24 | 25 | def connection_lost(self, exc): 26 | self.state = "closed" 27 | self.loop.stop() 28 | 29 | class TcpServer(threading.Thread): 30 | def __init__(self, host, port, event): 31 | super(TcpServer, self).__init__() 32 | self.host = host 33 | self.port = port 34 | self.event = event 35 | self.sock = None 36 | self.client = None 37 | 38 | def run(self): 39 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 40 | self.sock = sock 41 | try: 42 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 43 | sock.bind((self.host, self.port)) 44 | sock.listen(1) 45 | 46 | self.event.set() 47 | client, addr = sock.accept() 48 | self.client = client 49 | try: 50 | message = client.recv(100) 51 | client.sendall(message) 52 | finally: 53 | client.close() 54 | self.client = None 55 | finally: 56 | sock.close() 57 | self.sock = None 58 | 59 | def stop(self): 60 | self.join() 61 | 62 | return TcpEchoClientProtocol, TcpServer 63 | 64 | 65 | class TestNetwork(aiotest.TestCase): 66 | @pytest.mark.trio 67 | async def test_tcp_hello(self, loop, config): 68 | return 69 | port = 8888 70 | host = "127.0.0.1" 71 | message = b"Hello World!" 72 | 73 | event = config.threading.Event() 74 | TcpEchoClientProtocol, TcpServer = create_classes(config) 75 | server = TcpServer(host, port, event) 76 | server.start() 77 | self.addCleanup(server.stop) 78 | event.wait() 79 | 80 | proto = TcpEchoClientProtocol(message, loop) 81 | coro = loop.create_connection(lambda: proto, host, port) 82 | await loop.run_aio_coroutine(coro) 83 | assert proto.state != "new" 84 | 85 | await loop.stop().wait()() 86 | assert proto.state == "closed" 87 | assert proto.received == message 88 | -------------------------------------------------------------------------------- /tests/aiotest/test_thread.py: -------------------------------------------------------------------------------- 1 | from tests import aiotest 2 | import pytest 3 | import trio 4 | import trio_asyncio 5 | 6 | 7 | class TestThread(aiotest.TestCase): 8 | @pytest.mark.trio 9 | async def test_ident(self, loop, config): 10 | threading = config.threading 11 | try: 12 | get_ident = threading.get_ident # Python 3 13 | except AttributeError: 14 | get_ident = threading._get_ident # Python 2 15 | 16 | result = {"ident": None} 17 | 18 | def work(): 19 | result["ident"] = get_ident() 20 | 21 | fut = loop.run_in_executor(None, work) 22 | await loop.run_aio_coroutine(fut) 23 | 24 | # ensure that work() was executed in a different thread 25 | work_ident = result["ident"] 26 | assert work_ident is not None 27 | assert work_ident != get_ident() 28 | 29 | @pytest.mark.trio 30 | async def test_run_twice(self, loop): 31 | result = [] 32 | 33 | def work(): 34 | result.append("run") 35 | 36 | fut = loop.run_in_executor(None, work) 37 | await loop.run_aio_future(fut) 38 | assert result == ["run"] 39 | 40 | # ensure that run_in_executor() can be called twice 41 | fut = loop.run_in_executor(None, work) 42 | await loop.run_aio_future(fut) 43 | assert result == ["run", "run"] 44 | 45 | @pytest.mark.trio 46 | async def test_policy(self, loop, config): 47 | asyncio = config.asyncio 48 | result = {"loop": "not set"} # sentinel, different than None 49 | 50 | def work(): 51 | try: 52 | result["loop"] = asyncio.get_event_loop() 53 | except Exception as exc: 54 | result["loop"] = exc 55 | 56 | # get_event_loop() must return None in a different thread 57 | fut = loop.run_in_executor(None, work) 58 | await loop.run_aio_future(fut) 59 | assert isinstance(result["loop"], (AssertionError, RuntimeError)) 60 | 61 | @pytest.mark.trio 62 | async def test_run_in_thread(self, config): 63 | threading = config.threading 64 | 65 | class LoopThread(threading.Thread): 66 | def __init__(self, event): 67 | super(LoopThread, self).__init__() 68 | self.loop = None 69 | self.event = event 70 | 71 | async def _run(self): 72 | async with trio_asyncio.open_loop() as loop: 73 | self.loop = loop 74 | loop.set_debug(True) 75 | 76 | self.event.set() 77 | await loop.wait_stopped() 78 | 79 | def run(self): 80 | trio.run(self._run) 81 | 82 | result = [] 83 | 84 | # start an event loop in a thread 85 | event = threading.Event() 86 | thread = LoopThread(event) 87 | thread.start() 88 | event.wait() 89 | loop = thread.loop 90 | 91 | def func(loop): 92 | result.append(threading.current_thread().ident) 93 | loop.stop() 94 | 95 | # call func() in a different thread using the event loop 96 | tid = thread.ident 97 | loop.call_soon_threadsafe(func, loop) 98 | 99 | # wait for the other thread's event loop to terminate 100 | thread.join() 101 | assert result == [tid] 102 | -------------------------------------------------------------------------------- /tests/aiotest/test_timer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from tests import aiotest 3 | import pytest 4 | import trio 5 | 6 | 7 | class TestTimer(aiotest.TestCase): 8 | @pytest.mark.trio 9 | async def test_display_date(self, loop): 10 | result = [] 11 | delay = 0.2 12 | count = 3 13 | h = trio.Event() 14 | 15 | def display_date(end_time, loop): 16 | if not end_time: 17 | end_time.append(loop.time() + delay * count) 18 | result.append(datetime.datetime.now()) 19 | if (loop.time() + delay * 1.5) < end_time[0]: 20 | loop.call_later(delay, display_date, end_time, loop) 21 | else: 22 | loop.stop(h) 23 | 24 | loop.call_soon(display_date, [], loop) 25 | await h.wait() 26 | 27 | assert 2 <= len(result) <= 3 28 | assert all( 29 | later - earlier >= datetime.timedelta(microseconds=150000) 30 | for earlier, later in zip(result[:-1], result[1:]) 31 | ) 32 | 33 | @pytest.mark.trio 34 | async def test_later_stop_later(self, loop): 35 | result = [] 36 | 37 | def hello(): 38 | result.append("Hello") 39 | 40 | def world(loop): 41 | result.append("World") 42 | loop.stop() 43 | 44 | loop.call_later(0.1, hello) 45 | loop.call_later(0.5, world, loop) 46 | 47 | await trio.sleep(0.3) 48 | assert result == ["Hello"] 49 | 50 | await loop.wait_stopped() 51 | assert result == ["Hello", "World"] 52 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | from pathlib import Path 4 | import warnings 5 | import pytest 6 | import asyncio 7 | import trio_asyncio 8 | import inspect 9 | import unittest 10 | 11 | 12 | @pytest.fixture 13 | async def loop(): 14 | async with trio_asyncio.open_loop() as loop: 15 | yield loop 16 | 17 | 18 | # auto-trio-ize all async functions 19 | @pytest.hookimpl(tryfirst=True) 20 | def pytest_pyfunc_call(pyfuncitem): 21 | if inspect.iscoroutinefunction(pyfuncitem.obj): 22 | pyfuncitem.obj = pytest.mark.trio(pyfuncitem.obj) 23 | 24 | 25 | # Map collection of the python/ submodule to python's asyncio unittests 26 | try: 27 | # the global 'test' package installed with Python 28 | from test import test_asyncio 29 | except ImportError: 30 | warnings.warn( 31 | "Can't run the Python asyncio tests because they're not installed. " 32 | "On a Debian/Ubuntu system, you might need to install the " 33 | "libpython{}.{}-testsuite package.".format(*sys.version_info[:2]), 34 | RuntimeWarning, 35 | ) 36 | else: 37 | # threading_cleanup() causes a 5-second or so delay (100 38 | # gc.collect()'s) and warning on any test that holds a reference 39 | # to the event loop in the testsuite class, because 40 | # SyncTrioEventLoop spawns a thread that only exits when the 41 | # loop is closed. Nerf it. 42 | try: 43 | from test.support import threading_cleanup 44 | except ImportError: 45 | # Python 3.10+ 46 | from test.support.threading_helper import threading_cleanup 47 | 48 | def threading_no_cleanup(*original_values): 49 | pass 50 | 51 | threading_cleanup.__code__ = threading_no_cleanup.__code__ 52 | 53 | asyncio_test_dir = Path(test_asyncio.__path__[0]) 54 | 55 | def aio_test_nodeid(path): 56 | try: 57 | relpath = path.relative_to(asyncio_test_dir) 58 | except ValueError: 59 | return None 60 | else: 61 | return "/Python-{}.{}/test_asyncio/{}".format( 62 | *sys.version_info[:2], relpath 63 | ) 64 | 65 | # A pytest.Module that will only collect unittest.TestCase 66 | # classes, so that we don't get spurious warnings about things 67 | # like TestSelector and TestEventLoop (which are fakes used by the 68 | # tests, directly containing no tests themselves) not being collectable. 69 | class UnittestOnlyModule(pytest.Module): 70 | def istestclass(self, obj, name): 71 | return isinstance(obj, unittest.TestCase) 72 | 73 | # A pytest.Package whose only purpose is to mark that its children should 74 | # become UnittestOnlyModules (or other UnittestOnlyPackages). 75 | class UnittestOnlyPackage(pytest.Package): 76 | def collect(self): 77 | for node in super().collect(): 78 | if isinstance(node, pytest.Package): 79 | node.__class__ = UnittestOnlyPackage 80 | elif isinstance(node, pytest.Module): 81 | node.__class__ = UnittestOnlyModule 82 | node._nodeid = node._nodeid.replace("/__init__.py::", "/") 83 | node._nodeid = node._nodeid.replace("/.::", "/") 84 | yield node 85 | 86 | @pytest.hookimpl(tryfirst=True) 87 | def pytest_collect_directory(path, parent): 88 | from . import python 89 | 90 | candidate = str(path.resolve()) 91 | expected = os.path.realpath(os.path.dirname(python.__file__)) 92 | if candidate == expected: 93 | fwd_path = Path(os.path.dirname(test_asyncio.__file__)) 94 | node = UnittestOnlyPackage.from_parent(parent, path=fwd_path) 95 | # This keeps all test names from showing as "." 96 | node._nodeid = aio_test_nodeid(fwd_path) 97 | return node 98 | 99 | def pytest_collection_modifyitems(items): 100 | by_id = {item.nodeid: item for item in items} 101 | aio_test_root = aio_test_nodeid(asyncio_test_dir / "foo")[:-3] 102 | 103 | def mark(marker, rel_id, may_be_absent=False): 104 | try: 105 | by_id[aio_test_root + rel_id].add_marker(marker) 106 | except KeyError: 107 | if may_be_absent: 108 | return 109 | warnings.warn( 110 | "Tried to add marker {} to {}, but that test doesn't exist.".format( 111 | marker, rel_id 112 | ), 113 | RuntimeWarning, 114 | stacklevel=3, 115 | ) 116 | 117 | def xfail(rel_id): 118 | mark(pytest.mark.xfail, rel_id) 119 | 120 | def skip(rel_id): 121 | mark(pytest.mark.skip, rel_id) 122 | 123 | # Remainder of these have unclear issues 124 | if sys.version_info < (3, 8): 125 | xfail( 126 | "test_base_events.py::BaseEventLoopWithSelectorTests::" 127 | "test_log_slow_callbacks" 128 | ) 129 | if sys.version_info >= (3, 8): 130 | xfail( 131 | "test_tasks.py::RunCoroutineThreadsafeTests::" 132 | "test_run_coroutine_threadsafe_task_cancelled" 133 | ) 134 | if sys.version_info < (3, 11): 135 | xfail( 136 | "test_tasks.py::RunCoroutineThreadsafeTests::" 137 | "test_run_coroutine_threadsafe_with_timeout" 138 | ) 139 | if sys.platform == "win32": 140 | # hangs on 3.11+, fails without hanging on 3.8-3.10 141 | skip("test_windows_events.py::ProactorLoopCtrlC::test_ctrl_c") 142 | 143 | if sys.implementation.name == "pypy": 144 | # This test depends on the C implementation of asyncio.Future, and 145 | # unlike most such tests it is not configured to be skipped if 146 | # the C implementation is not available 147 | xfail( 148 | "test_futures.py::CFutureInheritanceTests::" 149 | "test_inherit_without_calling_super_init" 150 | ) 151 | if sys.version_info < (3, 8): 152 | # These tests assume CPython-style immediate finalization of 153 | # objects when they become unreferenced 154 | for test in ( 155 | "test_create_connection_memory_leak", 156 | "test_handshake_timeout", 157 | "test_start_tls_client_reg_proto_1", 158 | ): 159 | xfail("test_sslproto.py::SelectorStartTLSTests::{}".format(test)) 160 | 161 | # This test depends on the name of the loopback interface. On Github Actions 162 | # it fails on macOS always, and on Linux/Windows except on 3.8. 163 | skip( 164 | "test_base_events.py::BaseEventLoopWithSelectorTests::" 165 | "test_create_connection_ipv6_scope" 166 | ) 167 | 168 | if sys.platform == "darwin": 169 | # https://foss.heptapod.net/pypy/pypy/-/issues/3964 causes infinite loops 170 | for nodeid, item in by_id.items(): 171 | if "sendfile" in nodeid: 172 | item.add_marker(pytest.mark.skip) 173 | 174 | if sys.version_info >= (3, 11): 175 | # This tries to use a mock ChildWatcher that does something unlikely. 176 | # We don't support it because we don't actually use the ChildWatcher 177 | # to manage subprocesses. 178 | xfail( 179 | "test_subprocess.py::GenericWatcherTests::" 180 | "test_create_subprocess_fails_with_inactive_watcher" 181 | ) 182 | 183 | # This forks a child process and tries to run a new event loop there, 184 | # but Trio isn't fork-safe -- it hangs nondeterministically. 185 | skip("test_events.py::TestPyGetEventLoop::test_get_event_loop_new_process") 186 | skip("test_events.py::TestCGetEventLoop::test_get_event_loop_new_process") 187 | 188 | # This test attempts to stop the event loop from within a 189 | # run_until_complete coroutine, which hangs on our implementation. 190 | # Only present on releases post November 2023 191 | mark( 192 | pytest.mark.skip, 193 | "test_streams.py::StreamTests::test_loop_is_closed_resource_warnings", 194 | may_be_absent=True, 195 | ) 196 | 197 | if sys.version_info >= (3, 12): 198 | # These tests assume asyncio.sleep(0) is sufficient to run all pending tasks 199 | xfail( 200 | "test_futures2.py::PyFutureTests::test_task_exc_handler_correct_context" 201 | ) 202 | xfail( 203 | "test_futures2.py::CFutureTests::test_task_exc_handler_correct_context" 204 | ) 205 | 206 | # This test assumes that get_event_loop_policy().get_event_loop() doesn't 207 | # automatically return the running loop 208 | skip( 209 | "test_subprocess.py::GenericWatcherTests::test_create_subprocess_with_pidfd" 210 | ) 211 | -------------------------------------------------------------------------------- /tests/interop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/trio-asyncio/8d6744baaab324c7fdfb3258ea485296548036e9/tests/interop/__init__.py -------------------------------------------------------------------------------- /tests/interop/test_adapter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from trio_asyncio import aio_as_trio, trio_as_aio, allow_asyncio 3 | import asyncio 4 | import trio 5 | import sniffio 6 | from tests import aiotest 7 | import sys 8 | import warnings 9 | from contextlib import asynccontextmanager 10 | from trio_asyncio import TrioAsyncioDeprecationWarning 11 | 12 | 13 | def de_deprecate_converter(func): 14 | def wrapper(proc): 15 | with warnings.catch_warnings(): 16 | warnings.simplefilter("ignore", TrioAsyncioDeprecationWarning) 17 | return func(proc) 18 | 19 | return wrapper 20 | 21 | 22 | class SomeThing: 23 | flag = 0 24 | 25 | def __init__(self, loop): 26 | self.loop = loop 27 | 28 | async def dly_trio(self): 29 | assert sniffio.current_async_library() == "trio" 30 | await trio.sleep(0.01) 31 | self.flag |= 2 32 | return 8 33 | 34 | @trio_as_aio 35 | async def dly_trio_adapted(self): 36 | assert sniffio.current_async_library() == "trio" 37 | await trio.sleep(0.01) 38 | self.flag |= 2 39 | return 8 40 | 41 | @aio_as_trio 42 | async def dly_asyncio_adapted(self): 43 | assert sniffio.current_async_library() == "asyncio" 44 | await asyncio.sleep(0.01) 45 | self.flag |= 1 46 | return 4 47 | 48 | async def dly_asyncio(self, do_test=True): 49 | if do_test: 50 | assert sniffio.current_async_library() == "asyncio" 51 | await asyncio.sleep(0.01) 52 | self.flag |= 1 53 | return 4 54 | 55 | async def iter_asyncio(self, do_test=True): 56 | if do_test: 57 | assert sniffio.current_async_library() == "asyncio" 58 | await asyncio.sleep(0.01) 59 | yield 1 60 | await asyncio.sleep(0.01) 61 | yield 2 62 | await asyncio.sleep(0.01) 63 | self.flag |= 1 64 | 65 | async def iter_trio(self): 66 | assert sniffio.current_async_library() == "trio" 67 | await trio.sleep(0.01) 68 | yield 1 69 | await trio.sleep(0.01) 70 | yield 2 71 | await trio.sleep(0.01) 72 | self.flag |= 1 73 | 74 | @asynccontextmanager 75 | async def ctx_asyncio(self): 76 | await asyncio.sleep(0.01) 77 | self.flag |= 1 78 | yield self 79 | await asyncio.sleep(0.01) 80 | self.flag |= 2 81 | 82 | @asynccontextmanager 83 | async def ctx_trio(self): 84 | await trio.sleep(0.01) 85 | self.flag |= 1 86 | yield self 87 | await trio.sleep(0.01) 88 | self.flag |= 2 89 | 90 | 91 | class TestAdapt(aiotest.TestCase): 92 | @pytest.mark.trio 93 | async def test_asyncio_trio_adapted(self, loop): 94 | """Call asyncio from trio""" 95 | 96 | sth = SomeThing(loop) 97 | res = await aio_as_trio(sth.dly_trio_adapted, loop=loop)() 98 | assert res == 8 99 | assert sth.flag == 2 100 | 101 | @pytest.mark.trio 102 | async def test_asyncio_trio_adapted_no_call(self, loop): 103 | """Call asyncio from trio""" 104 | 105 | sth = SomeThing(loop) 106 | res = await aio_as_trio(sth.dly_trio_adapted) 107 | assert res == 8 108 | assert sth.flag == 2 109 | 110 | @pytest.mark.trio 111 | async def test_trio_asyncio_adapted(self, loop): 112 | sth = SomeThing(loop) 113 | res = await sth.dly_asyncio_adapted() 114 | assert res == 4 115 | assert sth.flag == 1 116 | 117 | @pytest.mark.trio 118 | async def test_trio_asyncio(self, loop): 119 | sth = SomeThing(loop) 120 | res = await aio_as_trio(sth.dly_asyncio)() 121 | assert res == 4 122 | assert sth.flag == 1 123 | 124 | @pytest.mark.trio 125 | async def test_trio_asyncio_awaitable(self, loop): 126 | sth = SomeThing(loop) 127 | res = await aio_as_trio(sth.dly_asyncio()) 128 | assert res == 4 129 | assert sth.flag == 1 130 | 131 | @pytest.mark.trio 132 | async def test_trio_asyncio_future(self, loop): 133 | sth = SomeThing(loop) 134 | f = sth.dly_asyncio(do_test=False) 135 | f = asyncio.ensure_future(f) 136 | res = await aio_as_trio(f) 137 | assert res == 4 138 | assert sth.flag == 1 139 | 140 | @pytest.mark.trio 141 | async def test_trio_asyncio_iter(self, loop): 142 | sth = SomeThing(loop) 143 | n = 0 144 | assert sniffio.current_async_library() == "trio" 145 | async for x in aio_as_trio(sth.iter_asyncio()): 146 | n += 1 147 | assert x == n 148 | assert n == 2 149 | assert sth.flag == 1 150 | 151 | async def run_asyncio_trio_iter(self, loop): 152 | sth = SomeThing(loop) 153 | n = 0 154 | assert sniffio.current_async_library() == "asyncio" 155 | async for x in trio_as_aio(sth.iter_trio()): 156 | n += 1 157 | assert x == n 158 | assert n == 2 159 | assert sth.flag == 1 160 | 161 | @pytest.mark.trio 162 | async def test_asyncio_trio_iter(self, loop): 163 | await aio_as_trio(self.run_asyncio_trio_iter)(loop) 164 | 165 | @pytest.mark.trio 166 | async def test_trio_asyncio_ctx(self, loop): 167 | sth = SomeThing(loop) 168 | async with aio_as_trio(sth.ctx_asyncio()): 169 | assert sth.flag == 1 170 | assert sth.flag == 3 171 | 172 | async def run_asyncio_trio_ctx(self, loop): 173 | sth = SomeThing(loop) 174 | async with trio_as_aio(sth.ctx_trio()): 175 | assert sth.flag == 1 176 | assert sth.flag == 3 177 | 178 | @pytest.mark.trio 179 | async def test_asyncio_trio_ctx(self, loop): 180 | await aio_as_trio(self.run_asyncio_trio_ctx)(loop) 181 | 182 | 183 | class TestAllow(aiotest.TestCase): 184 | async def run_asyncio_trio(self, loop): 185 | """Call asyncio from trio""" 186 | sth = SomeThing(loop) 187 | res = await trio_as_aio(sth.dly_trio, loop=loop)() 188 | assert res == 8 189 | assert sth.flag == 2 190 | 191 | @pytest.mark.trio 192 | async def test_asyncio_trio(self, loop): 193 | await allow_asyncio(self.run_asyncio_trio, loop) 194 | 195 | async def run_trio_asyncio(self, loop): 196 | sth = SomeThing(loop) 197 | res = await sth.dly_asyncio(do_test=False) 198 | assert res == 4 199 | assert sth.flag == 1 200 | 201 | @pytest.mark.trio 202 | async def test_trio_asyncio(self, loop): 203 | await allow_asyncio(self.run_trio_asyncio, loop) 204 | 205 | async def run_trio_asyncio_future(self, loop): 206 | sth = SomeThing(loop) 207 | f = sth.dly_asyncio(do_test=False) 208 | f = asyncio.ensure_future(f) 209 | res = await f 210 | assert res == 4 211 | assert sth.flag == 1 212 | 213 | @pytest.mark.trio 214 | async def test_trio_asyncio_future(self, loop): 215 | await allow_asyncio(self.run_trio_asyncio_future, loop) 216 | 217 | def get_asyncio_future(self, loop, sth): 218 | async def set_result(future, sth): 219 | await asyncio.sleep(0.01) 220 | sth.flag |= 1 221 | future.set_result(4) 222 | 223 | f = loop.create_future() 224 | loop.create_task(set_result(f, sth)) 225 | return f 226 | 227 | @pytest.mark.trio 228 | async def test_trio_asyncio_future_getter(self, loop): 229 | sth = SomeThing(loop) 230 | res = await allow_asyncio(self.get_asyncio_future, loop, sth) 231 | assert res == 4 232 | assert sth.flag == 1 233 | 234 | async def run_trio_asyncio_adapted(self, loop): 235 | sth = SomeThing(loop) 236 | res = await sth.dly_asyncio_adapted() 237 | assert res == 4 238 | assert sth.flag == 1 239 | 240 | @pytest.mark.trio 241 | async def test_trio_asyncio_adapted(self, loop): 242 | await allow_asyncio(self.run_trio_asyncio_adapted, loop) 243 | 244 | async def run_trio_asyncio_iter(self, loop): 245 | sth = SomeThing(loop) 246 | n = 0 247 | async for x in sth.iter_asyncio(do_test=False): 248 | n += 1 249 | assert x == n 250 | assert n == 2 251 | assert sth.flag == 1 252 | 253 | @pytest.mark.trio 254 | async def test_trio_asyncio_iter(self, loop): 255 | await allow_asyncio(self.run_trio_asyncio_iter, loop) 256 | 257 | async def run_trio_asyncio_ctx(self, loop): 258 | sth = SomeThing(loop) 259 | async with sth.ctx_asyncio(): 260 | assert sth.flag == 1 261 | assert sth.flag == 3 262 | 263 | @pytest.mark.trio 264 | async def test_trio_asyncio_ctx(self, loop): 265 | await allow_asyncio(self.run_trio_asyncio_ctx, loop) 266 | -------------------------------------------------------------------------------- /tests/interop/test_calls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | import trio 4 | import trio.testing 5 | import sniffio 6 | from trio_asyncio import aio_as_trio, trio_as_aio, run_aio_generator 7 | from tests import aiotest 8 | from functools import partial 9 | import sys 10 | from .. import utils as test_utils 11 | 12 | 13 | class Seen: 14 | flag = 0 15 | 16 | 17 | async def async_gen_to_list(generator): 18 | result = [] 19 | async for item in generator: 20 | result.append(item) 21 | return result 22 | 23 | 24 | class TrioContext: 25 | def __init__(self, parent): 26 | self.parent = parent 27 | 28 | async def __aenter__(self): 29 | assert self.parent.did_it == 0 30 | self.parent.did_it = 1 31 | assert sniffio.current_async_library() == "trio" 32 | await trio.sleep(0.01) 33 | self.parent.did_it = 2 34 | return self 35 | 36 | async def __aexit__(self, *tb): 37 | assert self.parent.did_it == 3 38 | self.parent.did_it = 4 39 | 40 | 41 | class AioContext: 42 | def __init__(self, parent, loop): 43 | self.parent = parent 44 | self.loop = loop 45 | 46 | async def __aenter__(self): 47 | assert self.parent.did_it == 0 48 | self.parent.did_it = 1 49 | assert sniffio.current_async_library() == "asyncio" 50 | await asyncio.sleep(0.01) 51 | self.parent.did_it = 2 52 | return self 53 | 54 | async def __aexit__(self, *tb): 55 | assert self.parent.did_it == 3 56 | self.parent.did_it = 4 57 | 58 | 59 | class TestCalls(aiotest.TestCase): 60 | async def call_t_a(self, proc, *args, loop=None): 61 | """called from Trio""" 62 | return await aio_as_trio(proc, loop=loop)(*args) 63 | 64 | async def call_a_t(self, proc, *args, loop=None): 65 | """call from asyncio to an async trio function""" 66 | return await trio_as_aio(proc, loop=loop)(*args) 67 | 68 | async def call_a_ts(self, proc, *args, loop=None): 69 | """called from asyncio to a sync trio function""" 70 | return proc(*args) 71 | 72 | @pytest.mark.trio 73 | async def test_call_at(self, loop): 74 | async def delay(t): 75 | done = asyncio.Event() 76 | loop.call_at(t, done.set) 77 | await done.wait() 78 | 79 | t = loop.time() + 0.1 80 | await aio_as_trio(delay, loop=loop)(t) 81 | 82 | @pytest.mark.trio 83 | async def test_asyncio_trio(self, loop): 84 | """Call asyncio from trio""" 85 | 86 | async def dly_trio(seen): 87 | await trio.sleep(0.01) 88 | seen.flag |= 2 89 | return 8 90 | 91 | seen = Seen() 92 | res = await aio_as_trio(partial(self.call_a_t, loop=loop), loop=loop)( 93 | dly_trio, seen 94 | ) 95 | assert res == 8 96 | assert seen.flag == 2 97 | 98 | @pytest.mark.trio 99 | async def test_call_asyncio_ctx(self, loop): 100 | self.did_it = 0 101 | async with aio_as_trio(AioContext(self, loop), loop=loop) as ctx: 102 | assert ctx.parent is self 103 | assert self.did_it == 2 104 | self.did_it = 3 105 | assert self.did_it == 4 106 | 107 | @pytest.mark.trio 108 | async def test_call_trio_ctx(self, loop): 109 | async def _call_trio_ctx(): 110 | self.did_it = 0 111 | async with trio_as_aio(TrioContext(self)) as ctx: 112 | assert ctx.parent is self 113 | assert self.did_it == 2 114 | self.did_it = 3 115 | assert self.did_it == 4 116 | 117 | await aio_as_trio(_call_trio_ctx, loop=loop)() 118 | 119 | @pytest.mark.trio 120 | async def test_asyncio_trio_sync(self, loop): 121 | """Call asyncio from trio""" 122 | 123 | def dly_trio(seen): 124 | seen.flag |= 2 125 | return 8 126 | 127 | seen = Seen() 128 | res = await aio_as_trio(partial(self.call_a_ts, loop=loop), loop=loop)( 129 | dly_trio, seen 130 | ) 131 | assert res == 8 132 | assert seen.flag == 2 133 | 134 | @pytest.mark.trio 135 | async def test_trio_asyncio(self, loop): 136 | async def dly_asyncio(seen): 137 | await asyncio.sleep(0.01) 138 | seen.flag |= 1 139 | return 4 140 | 141 | seen = Seen() 142 | res = await self.call_t_a(dly_asyncio, seen, loop=loop) 143 | assert res == 4 144 | assert seen.flag == 1 145 | 146 | @pytest.mark.trio 147 | async def test_asyncio_trio_error(self, loop): 148 | async def err_trio(): 149 | await trio.sleep(0.01) 150 | raise RuntimeError("I has another owie") 151 | 152 | with pytest.raises(RuntimeError) as err: 153 | await aio_as_trio(partial(self.call_a_t, loop=loop), loop=loop)(err_trio) 154 | assert err.value.args[0] == "I has another owie" 155 | 156 | @pytest.mark.trio 157 | async def test_asyncio_trio_sync_error(self, loop): 158 | def err_trio_sync(): 159 | loop.time() # verify that the loop is running 160 | raise RuntimeError("I has more owie") 161 | 162 | with pytest.raises(RuntimeError) as err: 163 | await aio_as_trio(partial(self.call_a_ts, loop=loop), loop=loop)( 164 | err_trio_sync 165 | ) 166 | assert err.value.args[0] == "I has more owie" 167 | 168 | @pytest.mark.trio 169 | async def test_trio_asyncio_error(self, loop): 170 | async def err_asyncio(): 171 | await asyncio.sleep(0.01) 172 | raise RuntimeError("I has an owie") 173 | 174 | with pytest.raises(RuntimeError) as err: 175 | await self.call_t_a(err_asyncio, loop=loop) 176 | assert err.value.args[0] == "I has an owie" 177 | 178 | @pytest.mark.trio 179 | async def test_asyncio_trio_cancel_out(self, loop): 180 | async def cancelled_trio(seen): 181 | seen.flag |= 1 182 | await trio.sleep(0.01) 183 | scope = trio.lowlevel.current_task()._cancel_status._scope 184 | scope.cancel() 185 | seen.flag |= 2 186 | await trio.sleep(0.01) 187 | seen.flag |= 4 188 | 189 | seen = Seen() 190 | with pytest.raises(asyncio.CancelledError): 191 | await aio_as_trio(partial(self.call_a_t, loop=loop), loop=loop)( 192 | cancelled_trio, seen 193 | ) 194 | assert seen.flag == 3 195 | 196 | @pytest.mark.trio 197 | async def test_trio_asyncio_cancel_out(self, loop): 198 | async def cancelled_asyncio(seen): 199 | seen.flag |= 1 200 | await asyncio.sleep(0.01) 201 | f = asyncio.Future() 202 | f.cancel() 203 | return f.result() # raises error 204 | 205 | def cancelled_future(seen): 206 | seen.flag |= 1 207 | f = asyncio.Future() 208 | f.cancel() 209 | return f # contains error 210 | 211 | async def check_cancel(proc, seen): 212 | with trio.CancelScope() as scope: 213 | with pytest.raises(asyncio.CancelledError): 214 | await self.call_t_a(proc, seen, loop=loop) 215 | assert not scope.cancel_called 216 | seen.flag |= 4 217 | 218 | seen = Seen() 219 | await check_cancel(cancelled_future, seen) 220 | assert seen.flag == 1 | 4 221 | 222 | seen = Seen() 223 | await check_cancel(cancelled_asyncio, seen) 224 | assert seen.flag == 1 | 4 225 | 226 | @pytest.mark.trio 227 | async def test_asyncio_trio_cancel_in(self, loop): 228 | async def in_trio(started, seen): 229 | started.set() 230 | try: 231 | await trio.sleep(1) 232 | except trio.Cancelled: 233 | seen.flag |= 1 234 | raise 235 | else: 236 | seen.flag |= 4 237 | finally: 238 | seen.flag |= 2 239 | 240 | async def cancel_asyncio(seen): 241 | started = asyncio.Event() 242 | f = asyncio.ensure_future(self.call_a_t(in_trio, started, seen, loop=loop)) 243 | await started.wait() 244 | f.cancel() 245 | with pytest.raises(asyncio.CancelledError): 246 | await f 247 | seen.flag |= 8 248 | 249 | seen = Seen() 250 | await aio_as_trio(cancel_asyncio, loop=loop)(seen) 251 | assert seen.flag == 1 | 2 | 8 252 | 253 | @pytest.mark.trio 254 | async def test_trio_asyncio_cancel_in(self, loop): 255 | async def in_asyncio(started, seen): 256 | started.set() 257 | try: 258 | await asyncio.sleep(9999) 259 | except asyncio.CancelledError: 260 | seen.flag |= 1 261 | except trio.Cancelled: 262 | seen.flag |= 16 263 | else: 264 | seen.flag |= 4 265 | finally: 266 | seen.flag |= 2 267 | 268 | async def cancel_trio(seen): 269 | started = trio.Event() 270 | async with trio.open_nursery() as nursery: 271 | nursery.start_soon( 272 | partial(self.call_t_a, loop=loop), in_asyncio, started, seen 273 | ) 274 | await started.wait() 275 | nursery.cancel_scope.cancel() 276 | seen.flag |= 8 277 | 278 | seen = Seen() 279 | await cancel_trio(seen) 280 | assert seen.flag == 1 | 2 | 8 281 | 282 | @pytest.mark.trio 283 | async def test_trio_asyncio_cancel_direct(self, loop): 284 | def in_asyncio(started, seen): 285 | # This is intentionally not async 286 | seen.flag |= 1 287 | raise asyncio.CancelledError() 288 | 289 | async def cancel_trio(seen): 290 | started = trio.Event() 291 | try: 292 | async with trio.open_nursery() as nursery: 293 | nursery.start_soon( 294 | partial(self.call_t_a, loop=loop), in_asyncio, started, seen 295 | ) 296 | await started.wait() 297 | nursery.cancel_scope.cancel() 298 | finally: 299 | seen.flag |= 8 300 | 301 | seen = Seen() 302 | with trio.testing.RaisesGroup(asyncio.CancelledError): 303 | await cancel_trio(seen) 304 | assert seen.flag == 1 | 8 305 | 306 | @pytest.mark.trio 307 | async def test_trio_asyncio_error_direct(self, loop): 308 | def err_asyncio(): 309 | # This is intentionally not async 310 | raise RuntimeError("I has an owie") 311 | 312 | with pytest.raises(RuntimeError) as err: 313 | await self.call_t_a(err_asyncio, loop=loop) 314 | assert err.value.args[0] == "I has an owie" 315 | 316 | @pytest.mark.trio 317 | async def test_trio_asyncio_generator(self, loop): 318 | async def dly_asyncio(): 319 | yield 1 320 | await asyncio.sleep(0.01) 321 | yield 2 322 | 323 | res = await async_gen_to_list(run_aio_generator(loop, dly_asyncio())) 324 | assert res == [1, 2] 325 | 326 | @pytest.mark.trio 327 | async def test_trio_asyncio_generator_with_error(self, loop): 328 | async def dly_asyncio(): 329 | yield 1 330 | raise RuntimeError("I has an owie") 331 | yield 2 332 | 333 | with pytest.raises(RuntimeError) as err: 334 | await async_gen_to_list(run_aio_generator(loop, dly_asyncio())) 335 | assert err.value.args[0] == "I has an owie" 336 | 337 | @pytest.mark.trio 338 | async def test_trio_asyncio_generator_with_cancellation(self, loop): 339 | async def dly_asyncio(hold, seen): 340 | yield 1 341 | seen.flag |= 1 342 | await hold.wait() 343 | 344 | hold = asyncio.Event() 345 | seen = Seen() 346 | 347 | async with trio.open_nursery() as nursery: 348 | nursery.start_soon( 349 | async_gen_to_list, run_aio_generator(loop, dly_asyncio(hold, seen)) 350 | ) 351 | await trio.testing.wait_all_tasks_blocked() 352 | nursery.cancel_scope.cancel() 353 | assert nursery.cancel_scope.cancel_called 354 | assert seen.flag == 1 355 | 356 | @pytest.mark.trio 357 | async def test_trio_asyncio_iterator(self, loop): 358 | async def slow_nums(): 359 | for n in range(1, 6): 360 | await asyncio.sleep(0.01) 361 | yield n 362 | 363 | sum = 0 364 | async for n in aio_as_trio(slow_nums()): 365 | sum += n 366 | assert sum == 15 367 | 368 | @pytest.mark.trio 369 | async def test_trio_asyncio_iterator_depr(self, loop): 370 | async def slow_nums(): 371 | for n in range(1, 6): 372 | await asyncio.sleep(0.01) 373 | yield n 374 | 375 | sum = 0 376 | # with test_utils.deprecate(self): ## not yet 377 | async for n in aio_as_trio(slow_nums(), loop=loop): 378 | sum += n 379 | assert sum == 15 380 | -------------------------------------------------------------------------------- /tests/module_with_deprecations.py: -------------------------------------------------------------------------------- 1 | regular = "hi" 2 | 3 | from trio_asyncio import _deprecate 4 | 5 | _deprecate.enable_attribute_deprecations(__name__) 6 | 7 | # Make sure that we don't trigger infinite recursion when accessing module 8 | # attributes in between calling enable_attribute_deprecations and defining 9 | # __deprecated_attributes__: 10 | import sys 11 | 12 | this_mod = sys.modules[__name__] 13 | assert this_mod.regular == "hi" 14 | assert not hasattr(this_mod, "dep1") 15 | 16 | __deprecated_attributes__ = { 17 | "dep1": _deprecate.DeprecatedAttribute("value1", "1.1", issue=1), 18 | "dep2": _deprecate.DeprecatedAttribute( 19 | "value2", "1.2", issue=1, instead="instead-string" 20 | ), 21 | } 22 | -------------------------------------------------------------------------------- /tests/python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/trio-asyncio/8d6744baaab324c7fdfb3258ea485296548036e9/tests/python/__init__.py -------------------------------------------------------------------------------- /tests/scripts/echo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if __name__ == "__main__": 4 | while True: 5 | buf = os.read(0, 1024) 6 | if not buf: 7 | break 8 | os.write(1, buf) 9 | -------------------------------------------------------------------------------- /tests/scripts/echo2.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if __name__ == "__main__": 4 | buf = os.read(0, 1024) 5 | os.write(1, b"OUT:" + buf) 6 | os.write(2, b"ERR:" + buf) 7 | -------------------------------------------------------------------------------- /tests/scripts/echo3.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if __name__ == "__main__": 4 | while True: 5 | buf = os.read(0, 1024) 6 | if not buf: 7 | break 8 | try: 9 | os.write(1, b"OUT:" + buf) 10 | except OSError as ex: 11 | os.write(2, b"ERR:" + ex.__class__.__name__.encode("ascii")) 12 | -------------------------------------------------------------------------------- /tests/test_concurrent.py: -------------------------------------------------------------------------------- 1 | import trio 2 | import trio_asyncio 3 | import asyncio 4 | import pytest 5 | from trio_asyncio._loop import TrioPolicy 6 | 7 | # Tests for concurrent or nested loops 8 | 9 | 10 | @pytest.mark.trio 11 | async def test_parallel(): 12 | loops = [None, None] 13 | async with trio.open_nursery() as n: 14 | 15 | async def gen_loop(i, task_status=trio.TASK_STATUS_IGNORED): 16 | task_status.started() 17 | async with trio_asyncio.open_loop() as loop: 18 | loops[i] = loop 19 | 20 | assert not isinstance(asyncio._get_running_loop(), trio_asyncio.TrioEventLoop) 21 | await n.start(gen_loop, 0) 22 | await n.start(gen_loop, 1) 23 | 24 | assert isinstance(loops[0], trio_asyncio.TrioEventLoop) 25 | assert isinstance(loops[1], trio_asyncio.TrioEventLoop) 26 | assert loops[0] is not loops[1] 27 | 28 | 29 | @pytest.mark.trio 30 | async def test_nested(): 31 | loops = [None, None] 32 | async with trio.open_nursery() as n: 33 | 34 | async def gen_loop(i, task_status=trio.TASK_STATUS_IGNORED): 35 | task_status.started() 36 | async with trio_asyncio.open_loop() as loop: 37 | loops[i] = loop 38 | if i > 0: 39 | await n.start(gen_loop, i - 1) 40 | 41 | assert not isinstance(asyncio._get_running_loop(), trio_asyncio.TrioEventLoop) 42 | await n.start(gen_loop, 1) 43 | assert not isinstance(asyncio._get_running_loop(), trio_asyncio.TrioEventLoop) 44 | assert isinstance(loops[0], trio_asyncio.TrioEventLoop) 45 | assert isinstance(loops[1], trio_asyncio.TrioEventLoop) 46 | assert loops[0] is not loops[1] 47 | 48 | 49 | async def _test_same_task(): 50 | assert isinstance(asyncio.get_event_loop_policy(), TrioPolicy) 51 | 52 | def get_loop(i, loop, policy): 53 | assert loop == asyncio.get_event_loop() 54 | assert policy == asyncio.get_event_loop_policy() 55 | 56 | async with trio.open_nursery(): 57 | async with trio_asyncio.open_loop() as loop1: 58 | policy = asyncio.get_event_loop_policy() 59 | assert isinstance(policy, TrioPolicy) 60 | async with trio_asyncio.open_loop() as loop2: 61 | p2 = asyncio.get_event_loop_policy() 62 | assert policy is p2, (policy, p2) 63 | loop1.call_later(0.1, get_loop, 0, loop1, policy) 64 | loop2.call_later(0.1, get_loop, 1, loop2, policy) 65 | await trio.sleep(0.2) 66 | 67 | assert isinstance(asyncio.get_event_loop_policy(), TrioPolicy) 68 | assert asyncio._get_running_loop() is None 69 | 70 | 71 | def test_same_task(): 72 | assert not isinstance(asyncio.get_event_loop_policy(), TrioPolicy) 73 | trio.run(_test_same_task) 74 | -------------------------------------------------------------------------------- /tests/test_deprecate.py: -------------------------------------------------------------------------------- 1 | # Mostly copied from trio.tests.test_deprecate. 2 | 3 | import pytest 4 | 5 | import inspect 6 | import warnings 7 | 8 | from trio_asyncio._deprecate import ( 9 | TrioAsyncioDeprecationWarning, 10 | warn_deprecated, 11 | deprecated, 12 | deprecated_alias, 13 | ) 14 | 15 | from . import module_with_deprecations 16 | 17 | 18 | @pytest.fixture 19 | def recwarn_always(recwarn): 20 | warnings.simplefilter("always") 21 | # ResourceWarnings about unclosed sockets can occur nondeterministically 22 | # (during GC) which throws off the tests in this file 23 | warnings.simplefilter("ignore", ResourceWarning) 24 | return recwarn 25 | 26 | 27 | def _here(): 28 | info = inspect.getframeinfo(inspect.currentframe().f_back) 29 | return (info.filename, info.lineno) 30 | 31 | 32 | def test_warn_deprecated(recwarn_always): 33 | def deprecated_thing(): 34 | warn_deprecated("ice", "1.2", issue=1, instead="water") 35 | 36 | deprecated_thing() 37 | filename, lineno = _here() 38 | assert len(recwarn_always) == 1 39 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 40 | assert "ice is deprecated" in got.message.args[0] 41 | assert "trio-asyncio 1.2" in got.message.args[0] 42 | assert "water instead" in got.message.args[0] 43 | assert "/issues/1" in got.message.args[0] 44 | assert got.filename == filename 45 | assert got.lineno == lineno - 1 46 | 47 | 48 | def test_warn_deprecated_no_instead_or_issue(recwarn_always): 49 | # Explicitly no instead or issue 50 | warn_deprecated("water", "1.3", issue=None, instead=None) 51 | assert len(recwarn_always) == 1 52 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 53 | assert "water is deprecated" in got.message.args[0] 54 | assert "no replacement" in got.message.args[0] 55 | assert "trio-asyncio 1.3" in got.message.args[0] 56 | 57 | 58 | def test_warn_deprecated_stacklevel(recwarn_always): 59 | def nested1(): 60 | nested2() 61 | 62 | def nested2(): 63 | warn_deprecated("x", "1.3", issue=7, instead="y", stacklevel=3) 64 | 65 | filename, lineno = _here() 66 | nested1() 67 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 68 | assert got.filename == filename 69 | assert got.lineno == lineno + 1 70 | 71 | 72 | def old(): # pragma: no cover 73 | pass 74 | 75 | 76 | def new(): # pragma: no cover 77 | pass 78 | 79 | 80 | def test_warn_deprecated_formatting(recwarn_always): 81 | warn_deprecated(old, "1.0", issue=1, instead=new) 82 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 83 | assert "test_deprecate.old is deprecated" in got.message.args[0] 84 | assert "test_deprecate.new instead" in got.message.args[0] 85 | 86 | 87 | @deprecated("1.5", issue=123, instead=new) 88 | def deprecated_old(): 89 | return 3 90 | 91 | 92 | def test_deprecated_decorator(recwarn_always): 93 | assert deprecated_old() == 3 94 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 95 | assert "test_deprecate.deprecated_old is deprecated" in got.message.args[0] 96 | assert "1.5" in got.message.args[0] 97 | assert "test_deprecate.new" in got.message.args[0] 98 | assert "issues/123" in got.message.args[0] 99 | 100 | 101 | class Foo: 102 | @deprecated("1.0", issue=123, instead="crying") 103 | def method(self): 104 | return 7 105 | 106 | 107 | def test_deprecated_decorator_method(recwarn_always): 108 | f = Foo() 109 | assert f.method() == 7 110 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 111 | assert "test_deprecate.Foo.method is deprecated" in got.message.args[0] 112 | 113 | 114 | @deprecated("1.2", thing="the thing", issue=None, instead=None) 115 | def deprecated_with_thing(): 116 | return 72 117 | 118 | 119 | def test_deprecated_decorator_with_explicit_thing(recwarn_always): 120 | assert deprecated_with_thing() == 72 121 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 122 | assert "the thing is deprecated" in got.message.args[0] 123 | 124 | 125 | def new_hotness(): 126 | return "new hotness" 127 | 128 | 129 | old_hotness = deprecated_alias("old_hotness", new_hotness, "1.23", issue=1) 130 | 131 | 132 | def test_deprecated_alias(recwarn_always): 133 | assert old_hotness() == "new hotness" 134 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 135 | assert "test_deprecate.old_hotness is deprecated" in got.message.args[0] 136 | assert "1.23" in got.message.args[0] 137 | assert "test_deprecate.new_hotness instead" in got.message.args[0] 138 | assert "issues/1" in got.message.args[0] 139 | 140 | assert ".. deprecated:: 1.23" in old_hotness.__doc__ 141 | assert "test_deprecate.new_hotness instead" in old_hotness.__doc__ 142 | assert "issues/1>`__" in old_hotness.__doc__ 143 | 144 | 145 | class Alias: 146 | def new_hotness_method(self): 147 | return "new hotness method" 148 | 149 | old_hotness_method = deprecated_alias( 150 | "Alias.old_hotness_method", new_hotness_method, "3.21", issue=1 151 | ) 152 | 153 | 154 | def test_deprecated_alias_method(recwarn_always): 155 | obj = Alias() 156 | assert obj.old_hotness_method() == "new hotness method" 157 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 158 | msg = got.message.args[0] 159 | assert "test_deprecate.Alias.old_hotness_method is deprecated" in msg 160 | assert "test_deprecate.Alias.new_hotness_method instead" in msg 161 | 162 | 163 | @deprecated("2.1", issue=1, instead="hi") 164 | def docstring_test1(): # pragma: no cover 165 | """Hello!""" 166 | 167 | 168 | @deprecated("2.1", issue=None, instead="hi") 169 | def docstring_test2(): # pragma: no cover 170 | """Hello!""" 171 | 172 | 173 | @deprecated("2.1", issue=1, instead=None) 174 | def docstring_test3(): # pragma: no cover 175 | """Hello!""" 176 | 177 | 178 | @deprecated("2.1", issue=None, instead=None) 179 | def docstring_test4(): # pragma: no cover 180 | """Hello!""" 181 | 182 | 183 | def test_deprecated_docstring_munging(): 184 | assert ( 185 | docstring_test1.__doc__ 186 | == """Hello! 187 | 188 | .. deprecated:: 2.1 189 | Use hi instead. 190 | For details, see `issue #1 `__. 191 | 192 | """ 193 | ) 194 | 195 | assert ( 196 | docstring_test2.__doc__ 197 | == """Hello! 198 | 199 | .. deprecated:: 2.1 200 | Use hi instead. 201 | 202 | """ 203 | ) 204 | 205 | assert ( 206 | docstring_test3.__doc__ 207 | == """Hello! 208 | 209 | .. deprecated:: 2.1 210 | For details, see `issue #1 `__. 211 | 212 | """ 213 | ) 214 | 215 | assert ( 216 | docstring_test4.__doc__ 217 | == """Hello! 218 | 219 | .. deprecated:: 2.1 220 | 221 | """ 222 | ) 223 | 224 | 225 | def test_module_with_deprecations(recwarn_always): 226 | assert module_with_deprecations.regular == "hi" 227 | assert len(recwarn_always) == 0 228 | 229 | filename, lineno = _here() 230 | assert module_with_deprecations.dep1 == "value1" 231 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 232 | assert got.filename == filename 233 | assert got.lineno == lineno + 1 234 | 235 | assert "module_with_deprecations.dep1" in got.message.args[0] 236 | assert "trio-asyncio 1.1" in got.message.args[0] 237 | assert "/issues/1" in got.message.args[0] 238 | assert "value1 instead" in got.message.args[0] 239 | 240 | assert module_with_deprecations.dep2 == "value2" 241 | got = recwarn_always.pop(TrioAsyncioDeprecationWarning) 242 | assert "instead-string instead" in got.message.args[0] 243 | 244 | with pytest.raises(AttributeError): 245 | module_with_deprecations.asdf 246 | -------------------------------------------------------------------------------- /tests/test_misc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import trio_asyncio 3 | import asyncio 4 | import trio 5 | import sys 6 | 7 | if sys.version_info < (3, 11): 8 | from exceptiongroup import ExceptionGroup, BaseExceptionGroup 9 | 10 | 11 | class Seen: 12 | flag = 0 13 | 14 | 15 | class TestMisc: 16 | @pytest.mark.trio 17 | async def test_close_no_stop(self): 18 | async with trio_asyncio.open_loop() as loop: 19 | triggered = trio.Event() 20 | 21 | def close_no_stop(): 22 | with pytest.raises(RuntimeError): 23 | loop.close() 24 | triggered.set() 25 | 26 | loop.call_soon(close_no_stop) 27 | await triggered.wait() 28 | 29 | @pytest.mark.trio 30 | async def test_too_many_stops(self): 31 | with trio.move_on_after(1) as scope: 32 | async with trio_asyncio.open_loop() as loop: 33 | await trio.lowlevel.checkpoint() 34 | loop.stop() 35 | assert ( 36 | not scope.cancelled_caught 37 | ), "Possible deadlock after manual call to loop.stop" 38 | 39 | @pytest.mark.trio 40 | async def test_err1(self, loop): 41 | async def raise_err(): 42 | raise RuntimeError("Foo") 43 | 44 | with pytest.raises(RuntimeError) as err: 45 | await trio_asyncio.aio_as_trio(raise_err, loop=loop)() 46 | assert err.value.args[0] == "Foo" 47 | 48 | @pytest.mark.trio 49 | async def test_err3(self, loop): 50 | owch = 0 51 | 52 | async def nest(): 53 | nonlocal owch 54 | owch = 1 55 | raise RuntimeError("Hello") 56 | 57 | async def call_nested(): 58 | with pytest.raises(RuntimeError) as err: 59 | await trio_asyncio.trio_as_aio(nest, loop=loop)() 60 | assert err.value.args[0] == "Hello" 61 | 62 | await trio_asyncio.aio_as_trio(call_nested, loop=loop)() 63 | assert owch 64 | 65 | @pytest.mark.trio 66 | async def test_run(self, loop): 67 | owch = 0 68 | 69 | async def nest(): 70 | await trio.sleep(0.01) 71 | nonlocal owch 72 | owch = 1 73 | 74 | async def call_nested(): 75 | await trio_asyncio.trio_as_aio(nest, loop=loop)() 76 | 77 | await trio_asyncio.aio_as_trio(call_nested, loop=loop)() 78 | assert owch 79 | 80 | async def _test_run(self): 81 | owch = 0 82 | 83 | async def nest(): 84 | await trio.sleep(0.01) 85 | nonlocal owch 86 | owch = 1 87 | 88 | async def call_nested(): 89 | await trio_asyncio.trio_as_aio(nest)() 90 | 91 | await trio_asyncio.aio_as_trio(call_nested)() 92 | assert owch 93 | 94 | def test_run2(self): 95 | trio_asyncio.run(self._test_run) 96 | 97 | @pytest.mark.trio 98 | async def test_run_task(self): 99 | owch = 0 100 | 101 | async def nest(x): 102 | nonlocal owch 103 | owch += x 104 | 105 | with pytest.raises(RuntimeError): 106 | trio_asyncio.run_trio_task(nest, 100) 107 | 108 | with pytest.raises((AttributeError, RuntimeError, TypeError)): 109 | with trio_asyncio.open_loop(): 110 | nest(1000) 111 | 112 | async with trio_asyncio.open_loop(): 113 | trio_asyncio.run_trio_task(nest, 1) 114 | await trio.sleep(0.05) 115 | assert owch == 1 116 | 117 | @pytest.mark.trio 118 | async def test_err2(self, loop): 119 | owch = 0 120 | 121 | async def nest(): 122 | nonlocal owch 123 | owch = 1 124 | raise RuntimeError("Hello") 125 | 126 | async def call_nested(): 127 | await trio_asyncio.aio_as_trio(nest, loop=loop)() 128 | 129 | async def call_more_nested(): 130 | with pytest.raises(RuntimeError) as err: 131 | await trio_asyncio.trio_as_aio(call_nested, loop=loop)() 132 | assert err.value.args[0] == "Hello" 133 | 134 | await trio_asyncio.aio_as_trio(call_more_nested, loop=loop)() 135 | assert owch 136 | 137 | @pytest.mark.trio 138 | async def test_run3(self, loop): 139 | owch = 0 140 | 141 | async def nest(): 142 | nonlocal owch 143 | owch = 1 144 | 145 | async def call_nested(): 146 | await trio_asyncio.aio_as_trio(nest, loop=loop)() 147 | 148 | async def call_more_nested(): 149 | await trio_asyncio.trio_as_aio(call_nested, loop=loop)() 150 | 151 | await trio_asyncio.aio_as_trio(call_more_nested, loop=loop)() 152 | assert owch 153 | 154 | @pytest.mark.trio 155 | async def test_cancel_sleep(self, loop): 156 | owch = 0 157 | 158 | def do_not_run(): 159 | nonlocal owch 160 | owch = 1 161 | 162 | async def cancel_sleep(): 163 | h = loop.call_later(0.2, do_not_run) 164 | await asyncio.sleep(0.01) 165 | h.cancel() 166 | await asyncio.sleep(0.3) 167 | 168 | await trio_asyncio.aio_as_trio(cancel_sleep, loop=loop)() 169 | assert owch == 0 170 | 171 | 172 | @pytest.mark.trio 173 | async def test_wrong_context_manager_order(): 174 | take_down = trio.Event() 175 | 176 | async def work_in_asyncio(): 177 | await asyncio.sleep(0) 178 | 179 | async def runner(*, task_status=trio.TASK_STATUS_IGNORED): 180 | await trio_asyncio.aio_as_trio(work_in_asyncio)() 181 | try: 182 | task_status.started() 183 | await take_down.wait() 184 | finally: 185 | await trio_asyncio.aio_as_trio(work_in_asyncio)() 186 | 187 | async with trio.open_nursery() as nursery: 188 | async with trio_asyncio.open_loop(): 189 | await nursery.start(runner) 190 | take_down.set() 191 | 192 | 193 | @pytest.mark.trio 194 | @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows") 195 | async def test_keyboard_interrupt_teardown(): 196 | asyncio_loop_closed = trio.Event() 197 | 198 | async def work_in_trio_no_matter_what(*, task_status=trio.TASK_STATUS_IGNORED): 199 | await trio_asyncio.aio_as_trio(work_in_asyncio)() 200 | try: 201 | # KeyboardInterrupt won't cancel this coroutine thanks to the shield 202 | with trio.CancelScope(shield=True): 203 | task_status.started() 204 | await asyncio_loop_closed.wait() 205 | finally: 206 | # Hence this call will be exceuted after run_asyncio_loop is cancelled 207 | with pytest.raises(RuntimeError): 208 | await trio_asyncio.aio_as_trio(work_in_asyncio)() 209 | 210 | async def work_in_asyncio(): 211 | await asyncio.sleep(0) 212 | 213 | async def run_asyncio_loop(nursery, *, task_status=trio.TASK_STATUS_IGNORED): 214 | with trio.CancelScope() as cancel_scope: 215 | try: 216 | async with trio_asyncio.open_loop(): 217 | # Starting a coroutine from here make it inherit the access 218 | # to the asyncio loop context manager 219 | await nursery.start(work_in_trio_no_matter_what) 220 | task_status.started(cancel_scope) 221 | await trio.sleep_forever() 222 | finally: 223 | asyncio_loop_closed.set() 224 | 225 | import signal 226 | import threading 227 | 228 | with trio.testing.RaisesGroup(KeyboardInterrupt): 229 | async with trio.open_nursery() as nursery: 230 | await nursery.start(run_asyncio_loop, nursery) 231 | # Trigger KeyboardInterrupt that should propagate accross the coroutines 232 | signal.pthread_kill(threading.get_ident(), signal.SIGINT) 233 | 234 | 235 | @pytest.mark.trio 236 | @pytest.mark.parametrize("throw_another", [False, True]) 237 | async def test_cancel_loop(throw_another): 238 | """Regression test for #76: ensure that cancelling a trio-asyncio loop 239 | does not cause any of the tasks running within it to yield a 240 | result of Cancelled. 241 | """ 242 | 243 | async def manage_loop(task_status): 244 | try: 245 | with trio.CancelScope() as scope: 246 | async with trio_asyncio.open_loop() as loop: 247 | task_status.started((loop, scope)) 248 | await trio.sleep_forever() 249 | finally: 250 | assert scope.cancelled_caught 251 | 252 | # Trio-flavored async function. Runs as a trio-aio loop task 253 | # and gets cancelled when the loop does. 254 | async def trio_task(): 255 | async with trio.open_nursery() as nursery: 256 | nursery.start_soon(trio.sleep_forever) 257 | try: 258 | await trio.sleep_forever() 259 | except trio.Cancelled: 260 | if throw_another: 261 | # This will combine with the Cancelled from the 262 | # background sleep_forever task to create an 263 | # ExceptionGroup escaping from trio_task 264 | raise ValueError("hi") 265 | 266 | async with trio.open_nursery() as nursery: 267 | loop, scope = await nursery.start(manage_loop) 268 | fut = loop.trio_as_future(trio_task) 269 | await trio.testing.wait_all_tasks_blocked() 270 | scope.cancel() 271 | assert fut.done() 272 | if throw_another: 273 | with trio.testing.RaisesGroup(trio.testing.Matcher(ValueError, match="hi")): 274 | fut.result() 275 | else: 276 | assert fut.cancelled() 277 | 278 | 279 | @pytest.mark.trio 280 | async def test_trio_as_fut_throws_after_cancelled(): 281 | """If a trio_as_future() future is cancelled, any exception 282 | thrown by the Trio task as it unwinds is still propagated. 283 | """ 284 | 285 | async def trio_task(): 286 | try: 287 | await trio.sleep_forever() 288 | finally: 289 | raise ValueError("hi") 290 | 291 | async with trio_asyncio.open_loop() as loop: 292 | fut = loop.trio_as_future(trio_task) 293 | await trio.testing.wait_all_tasks_blocked() 294 | fut.cancel() 295 | with pytest.raises(ValueError): 296 | await trio_asyncio.run_aio_future(fut) 297 | 298 | 299 | @pytest.mark.trio 300 | async def test_run_trio_task_errors(monkeypatch): 301 | async with trio_asyncio.open_loop() as loop: 302 | # Test never getting to start the task 303 | handle = loop.run_trio_task(trio.sleep_forever) 304 | handle.cancel() 305 | 306 | # Test cancelling the task 307 | handle = loop.run_trio_task(trio.sleep_forever) 308 | await trio.testing.wait_all_tasks_blocked() 309 | handle.cancel() 310 | 311 | # Helper for the rest of this test, which covers cases where 312 | # the Trio task raises an exception 313 | async def raise_in_aio_loop(exc): 314 | async def raise_it(): 315 | raise exc 316 | 317 | async with trio_asyncio.open_loop() as loop: 318 | loop.run_trio_task(raise_it) 319 | 320 | # We temporarily modify the default exception handler to collect 321 | # the exceptions instead of logging or raising them 322 | 323 | exceptions = [] 324 | 325 | def collect_exceptions(loop, context): 326 | if context.get("exception"): 327 | exceptions.append(context["exception"]) 328 | else: 329 | exceptions.append(RuntimeError(context.get("message") or "unknown")) 330 | 331 | monkeypatch.setattr( 332 | trio_asyncio.TrioEventLoop, "default_exception_handler", collect_exceptions 333 | ) 334 | expected = [ValueError("hi"), ValueError("lo"), KeyError(), IndexError()] 335 | await raise_in_aio_loop(expected[0]) 336 | with trio.testing.RaisesGroup(SystemExit, flatten_subgroups=True): 337 | await raise_in_aio_loop(SystemExit(0)) 338 | with trio.testing.RaisesGroup(SystemExit, flatten_subgroups=True) as result: 339 | await raise_in_aio_loop(BaseExceptionGroup("", [expected[1], SystemExit()])) 340 | 341 | assert len(result.value.exceptions) == 1 342 | 343 | def innermost_exception(item): 344 | if isinstance(item, BaseExceptionGroup): 345 | return innermost_exception(item.exceptions[0]) 346 | return item 347 | 348 | assert isinstance(innermost_exception(result.value), SystemExit) 349 | await raise_in_aio_loop(ExceptionGroup("", expected[2:])) 350 | 351 | assert len(exceptions) == 3 352 | assert exceptions[0] is expected[0] 353 | assert isinstance(exceptions[1], ExceptionGroup) 354 | assert exceptions[1].exceptions == (expected[1],) 355 | assert isinstance(exceptions[2], ExceptionGroup) 356 | assert exceptions[2].exceptions == tuple(expected[2:]) 357 | 358 | 359 | @pytest.mark.trio 360 | async def test_contextvars(): 361 | import contextvars 362 | 363 | cvar = contextvars.ContextVar("test_cvar") 364 | cvar.set("outer") 365 | 366 | async def fudge_in_aio(): 367 | assert cvar.get() == "outer" 368 | cvar.set("middle") 369 | await trio_asyncio.trio_as_aio(fudge_in_trio)() 370 | assert cvar.get() == "middle" 371 | 372 | async def fudge_in_trio(): 373 | assert cvar.get() == "middle" 374 | cvar.set("inner") 375 | 376 | async with trio_asyncio.open_loop() as loop: 377 | await trio_asyncio.aio_as_trio(fudge_in_aio)() 378 | assert cvar.get() == "outer" 379 | -------------------------------------------------------------------------------- /tests/test_sync.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | class TestSync: 5 | def test_explicit_mainloop(self): 6 | async def foo(): 7 | return "bar" 8 | 9 | async def bar(): 10 | return "baz" 11 | 12 | loop = asyncio.new_event_loop() 13 | res = loop.run_until_complete(foo()) 14 | assert res == "bar" 15 | res = loop.run_until_complete(bar()) 16 | assert res == "baz" 17 | loop.close() 18 | -------------------------------------------------------------------------------- /tests/test_trio_asyncio.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | import types 4 | import asyncio 5 | import trio 6 | import trio.testing 7 | import trio_asyncio 8 | import contextlib 9 | import gc 10 | 11 | 12 | async def use_asyncio(): 13 | await trio_asyncio.aio_as_trio(asyncio.sleep)(0) 14 | 15 | 16 | @pytest.fixture() 17 | async def asyncio_fixture_with_fixtured_loop(loop): 18 | await use_asyncio() 19 | yield None 20 | 21 | 22 | @pytest.fixture() 23 | async def asyncio_fixture_own_loop(): 24 | async with trio_asyncio.open_loop(): 25 | await use_asyncio() 26 | yield None 27 | 28 | 29 | @pytest.mark.trio 30 | async def test_no_fixture(): 31 | async with trio_asyncio.open_loop(): 32 | await use_asyncio() 33 | 34 | 35 | @pytest.mark.trio 36 | async def test_half_fixtured_asyncpg_conn(asyncio_fixture_own_loop): 37 | await use_asyncio() 38 | 39 | 40 | @pytest.mark.trio 41 | async def test_fixtured_asyncpg_conn(asyncio_fixture_with_fixtured_loop): 42 | await use_asyncio() 43 | 44 | 45 | @pytest.mark.trio 46 | async def test_get_running_loop(): 47 | async with trio_asyncio.open_loop() as loop: 48 | assert asyncio.get_running_loop() == loop 49 | 50 | 51 | @pytest.mark.trio 52 | async def test_exception_after_closed(caplog): 53 | async with trio_asyncio.open_loop() as loop: 54 | pass 55 | loop.call_exception_handler({"message": "Test exception after loop closed"}) 56 | assert len(caplog.records) == 1 57 | assert caplog.records[0].message == "Test exception after loop closed" 58 | 59 | 60 | @pytest.mark.trio 61 | async def test_tasks_get_cancelled(): 62 | record = [] 63 | tasks = [] 64 | 65 | @types.coroutine 66 | def aio_yield(): 67 | yield 68 | 69 | async def aio_sleeper(key): 70 | try: 71 | await asyncio.sleep(10) 72 | record.append("expired") 73 | finally: 74 | try: 75 | # Prove that we're still running in the aio loop, not 76 | # some GC pass 77 | await aio_yield() 78 | finally: 79 | record.append(key) 80 | if "early" in key: 81 | tasks.append(asyncio.ensure_future(aio_sleeper("aio late"))) 82 | asyncio.get_event_loop().run_trio_task(trio_sleeper, "trio late") 83 | 84 | async def trio_sleeper(key): 85 | try: 86 | await trio.sleep_forever() 87 | finally: 88 | await trio.lowlevel.cancel_shielded_checkpoint() 89 | record.append(key) 90 | 91 | async with trio_asyncio.open_loop() as loop: 92 | tasks.append(asyncio.ensure_future(aio_sleeper("aio early"))) 93 | loop.run_trio_task(trio_sleeper, "trio early") 94 | 95 | assert set(record) == {"aio early", "trio early", "trio late"} 96 | assert len(tasks) == 2 and tasks[0].done() and not tasks[1].done() 97 | 98 | # Suppress "Task was destroyed but it was pending!" message 99 | tasks[1]._log_traceback = False 100 | tasks[1]._log_destroy_pending = False 101 | 102 | # Suppress the "coroutine ignored GeneratorExit" message 103 | while True: 104 | try: 105 | tasks[1]._coro.throw(SystemExit) 106 | except SystemExit: 107 | break 108 | 109 | 110 | @pytest.mark.trio 111 | async def test_cancel_loop(autojump_clock): 112 | with trio.move_on_after(1) as scope: 113 | async with trio_asyncio.open_loop(): 114 | await trio.sleep_forever() 115 | assert trio.current_time() == 1 116 | assert scope.cancelled_caught 117 | 118 | 119 | @pytest.mark.trio 120 | @pytest.mark.parametrize("shield", (False, True)) 121 | @pytest.mark.parametrize("body_raises", (False, True)) 122 | async def test_cancel_loop_with_tasks(autojump_clock, shield, body_raises): 123 | record = [] 124 | 125 | if body_raises: 126 | catcher = trio.testing.RaisesGroup( 127 | trio.testing.Matcher(ValueError, match="hi"), flatten_subgroups=True 128 | ) 129 | else: 130 | catcher = contextlib.nullcontext() 131 | 132 | with catcher, trio.move_on_after(1.25) as scope: 133 | async with trio_asyncio.open_loop(): 134 | 135 | async def trio_task(): 136 | try: 137 | with trio.CancelScope(shield=shield): 138 | await trio.sleep(1) 139 | finally: 140 | record.append("trio_task done at") 141 | record.append(trio.current_time()) 142 | 143 | async def aio_task(): 144 | await asyncio.sleep(1) 145 | try: 146 | await trio_asyncio.trio_as_aio(trio_task)() 147 | except asyncio.CancelledError: 148 | assert not shield 149 | raise 150 | except trio.Cancelled: 151 | assert False 152 | else: 153 | assert shield 154 | finally: 155 | record.append("aio_task done") 156 | 157 | try: 158 | async with trio.open_nursery() as nursery: 159 | nursery.cancel_scope.shield = True 160 | 161 | @nursery.start_soon 162 | async def unshield_later(): 163 | await trio.sleep(1.5) 164 | nursery.cancel_scope.shield = False 165 | 166 | nursery.start_soon(trio_asyncio.aio_as_trio(aio_task)) 167 | if body_raises: 168 | try: 169 | await trio.sleep_forever() 170 | finally: 171 | raise ValueError("hi") 172 | finally: 173 | record.append("toplevel done") 174 | 175 | assert record == [ 176 | "trio_task done at", 177 | trio.current_time(), 178 | "aio_task done", 179 | "toplevel done", 180 | ] 181 | assert trio.current_time() == 1.5 + (shield * 0.5) 182 | assert scope.cancelled_caught == (not shield) 183 | 184 | 185 | @pytest.mark.trio 186 | async def test_executor_limiter_deadlock(): 187 | def noop(): 188 | pass 189 | 190 | # capacity of 1 to catch a double-acquire 191 | limiter = trio.CapacityLimiter(1) 192 | executor = trio_asyncio.TrioExecutor(limiter=limiter) 193 | async with trio_asyncio.open_loop() as loop: 194 | with trio.move_on_after(1) as scope: 195 | await trio_asyncio.aio_as_trio(loop.run_in_executor)(executor, noop) 196 | 197 | assert not scope.cancelled_caught 198 | 199 | 200 | def test_system_exit(): 201 | async def main(): 202 | raise SystemExit(42) 203 | 204 | with pytest.raises(SystemExit) as scope: 205 | asyncio.run(main()) 206 | 207 | assert scope.value.code == 42 208 | 209 | 210 | @pytest.mark.trio 211 | @pytest.mark.parametrize("alive_on_exit", (False, True)) 212 | @pytest.mark.parametrize("slow_finalizer", (False, True)) 213 | @pytest.mark.parametrize("loop_timeout", (0, 1, 20)) 214 | async def test_asyncgens(alive_on_exit, slow_finalizer, loop_timeout, autojump_clock): 215 | import sniffio 216 | 217 | record = set() 218 | holder = [] 219 | 220 | async def agen(label, extra): 221 | assert sniffio.current_async_library() == label 222 | if label == "asyncio": 223 | loop = asyncio.get_running_loop() 224 | try: 225 | yield 1 226 | finally: 227 | library = sniffio.current_async_library() 228 | if label == "asyncio": 229 | assert loop is asyncio.get_running_loop() 230 | try: 231 | await sys.modules[library].sleep(5 if slow_finalizer else 0) 232 | except (trio.Cancelled, asyncio.CancelledError): 233 | pass 234 | record.add((label + extra, library)) 235 | 236 | async def iterate_one(label, extra=""): 237 | ag = agen(label, extra) 238 | await ag.asend(None) 239 | if alive_on_exit: 240 | holder.append(ag) 241 | else: 242 | del ag 243 | 244 | sys.unraisablehook, prev_hook = sys.__unraisablehook__, sys.unraisablehook 245 | try: 246 | before_hooks = sys.get_asyncgen_hooks() 247 | 248 | start_time = trio.current_time() 249 | with trio.move_on_after(loop_timeout) as scope: 250 | if loop_timeout == 0: 251 | scope.cancel() 252 | async with trio_asyncio.open_loop() as loop, trio_asyncio.open_loop() as loop2: 253 | assert sys.get_asyncgen_hooks() != before_hooks 254 | async with trio.open_nursery() as nursery: 255 | # Make sure the iterate_one aio tasks don't get 256 | # cancelled before they start: 257 | nursery.cancel_scope.shield = True 258 | try: 259 | nursery.start_soon(iterate_one, "trio") 260 | nursery.start_soon( 261 | loop.run_aio_coroutine, iterate_one("asyncio") 262 | ) 263 | nursery.start_soon( 264 | loop2.run_aio_coroutine, iterate_one("asyncio", "2") 265 | ) 266 | await loop.synchronize() 267 | await loop2.synchronize() 268 | finally: 269 | nursery.cancel_scope.shield = False 270 | if not alive_on_exit and sys.implementation.name == "pypy": 271 | for _ in range(5): 272 | gc.collect() 273 | 274 | # Make sure we cleaned up properly once all trio-aio loops were closed 275 | assert sys.get_asyncgen_hooks() == before_hooks 276 | 277 | # asyncio agens should be finalized as soon as asyncio loop ends, 278 | # regardless of liveness 279 | assert ("asyncio", "asyncio") in record 280 | assert ("asyncio2", "asyncio") in record 281 | 282 | # asyncio agen finalizers should be able to take a cancel 283 | if (slow_finalizer or loop_timeout == 0) and alive_on_exit: 284 | # Each loop finalizes in series, and takes 5 seconds 285 | # if slow_finalizer is true. 286 | assert trio.current_time() == start_time + min(loop_timeout, 10) 287 | assert scope.cancelled_caught == (loop_timeout < 10) 288 | else: 289 | # `not alive_on_exit` implies that the asyncio agen aclose() tasks 290 | # are started before loop shutdown, which means they'll be 291 | # cancelled during loop shutdown; this matches regular asyncio. 292 | # 293 | # `not slow_finalizer and loop_timeout > 0` implies that the agens 294 | # have time to complete before we cancel them. 295 | assert trio.current_time() == start_time 296 | assert not scope.cancelled_caught 297 | 298 | # trio asyncgen should eventually be finalized in trio mode 299 | del holder[:] 300 | for _ in range(5): 301 | gc.collect() 302 | await trio.testing.wait_all_tasks_blocked() 303 | assert record == { 304 | ("trio", "trio"), 305 | ("asyncio", "asyncio"), 306 | ("asyncio2", "asyncio"), 307 | } 308 | finally: 309 | sys.unraisablehook = prev_hook 310 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities shared by tests.""" 2 | 3 | import sys 4 | import pytest 5 | import contextlib 6 | import logging 7 | from asyncio.log import logger 8 | 9 | from trio_asyncio import TrioAsyncioDeprecationWarning 10 | 11 | 12 | def deprecate(tc): 13 | return pytest.warns(TrioAsyncioDeprecationWarning) 14 | 15 | 16 | def deprecate_stdlib(tc, vers=None): 17 | if vers is None or sys.version_info >= vers: 18 | return pytest.deprecated_call() 19 | 20 | class _deprecate: 21 | def __init__(self, tc): 22 | pass 23 | 24 | def __enter__(self): 25 | return self 26 | 27 | def __exit__(self, *tb): 28 | pass 29 | 30 | return _deprecate(tc) 31 | 32 | 33 | @contextlib.contextmanager 34 | def disable_logger(): 35 | """Context manager to disable asyncio logger. 36 | 37 | For example, it can be used to ignore warnings in debug mode. 38 | """ 39 | old_level = logger.level 40 | try: 41 | logger.setLevel(logging.CRITICAL + 1) 42 | yield 43 | finally: 44 | logger.setLevel(old_level) 45 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37 3 | 4 | [sphinx-vars] 5 | build_dir = build 6 | input_dir = docs/source 7 | sphinxopts = -a -W -b html 8 | autosphinxopts = -i *~ -i *.sw* -i Makefile* 9 | sphinxbuilddir = {[sphinx-vars]build_dir}/sphinx/html 10 | allsphinxopts = -d {[sphinx-vars]build_dir}/sphinx/doctrees {[sphinx-vars]sphinxopts} docs 11 | 12 | [testenv] 13 | deps = 14 | pytest 15 | outcome 16 | commands = 17 | pytest -xvvv --full-trace 18 | 19 | [testenv:pylint] 20 | deps = 21 | pylint 22 | commands = 23 | pylint trio_asyncio 24 | 25 | [testenv:flake8] 26 | deps = 27 | flake8 28 | commands = 29 | flake8 trio_asyncio tests 30 | 31 | [testenv:doc] 32 | deps = 33 | sphinx 34 | sphinx-rtd-theme 35 | sphinxcontrib-trio 36 | commands = 37 | sphinx-build -a {[sphinx-vars]input_dir} {[sphinx-vars]build_dir} 38 | 39 | [testenv:livehtml] 40 | deps = 41 | sphinx 42 | sphinx-autobuild 43 | commands = 44 | sphinx-autobuild {[sphinx-vars]autosphinxopts} {[sphinx-vars]allsphinxopts} {[sphinx-vars]sphinxbuilddir} 45 | -------------------------------------------------------------------------------- /trio_asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | # This code implements basic asyncio compatibility 2 | 3 | # Submodules are organized into the following layers, from highest to lowest; 4 | # to avoid circular dependencies, submodules can only depend on other submodules 5 | # in a strictly lower layer. 6 | # 7 | # nice facade: _adapter 8 | # event loop dispatch and policy: _loop 9 | # event loop implementations: _async, _sync 10 | # event loop base: _base 11 | # utilities: _handles, _util, _child, _deprecate, _version 12 | 13 | from ._version import __version__ # noqa 14 | 15 | from ._deprecate import TrioAsyncioDeprecationWarning 16 | from ._util import run_aio_future, run_aio_generator 17 | from ._base import BaseTrioEventLoop, TrioExecutor 18 | from ._async import TrioEventLoop 19 | from ._loop import ( 20 | # main entry point: 21 | open_loop, 22 | # trio.run() + trio_asyncio.open_loop(): 23 | run, 24 | # loop selection: 25 | current_loop, 26 | # forwarders to event loop methods: 27 | run_trio_task, 28 | run_trio, 29 | run_aio_coroutine, 30 | ) 31 | from ._adapter import ( 32 | aio_as_trio, 33 | trio_as_aio, 34 | # aliases for the above: 35 | asyncio_as_trio, 36 | trio_as_asyncio, 37 | # additional experimental goodie: 38 | allow_asyncio, 39 | ) 40 | 41 | import importlib as _importlib 42 | from . import _deprecate, _util 43 | 44 | _deprecate.enable_attribute_deprecations(__name__) 45 | __deprecated_attributes__ = { 46 | name: _deprecate.DeprecatedAttribute( 47 | _importlib.import_module("trio_asyncio._" + name.rstrip("_")), 48 | "0.11.0", 49 | issue=64, 50 | instead="an import from the top-level trio_asyncio package", 51 | ) 52 | for name in ( 53 | "adapter", 54 | "async_", 55 | "base", 56 | "child", 57 | "handles", 58 | "loop", 59 | "sync", 60 | "util", 61 | ) 62 | } 63 | __deprecated_attributes__.update( 64 | { 65 | name: _deprecate.DeprecatedAttribute( 66 | getattr(_loop, name), "0.11.0", issue=64, instead=None 67 | ) 68 | for name in ("TrioPolicy", "TrioChildWatcher", "current_policy") 69 | } 70 | ) 71 | 72 | # Provide aliases in the old place for names that moved between modules. 73 | # Remove these when the non-underscore-prefixed module names are removed. 74 | from . import _loop, _async 75 | 76 | _async.open_loop = _loop.open_loop 77 | 78 | _util.fixup_module_metadata(__name__, globals()) 79 | -------------------------------------------------------------------------------- /trio_asyncio/_adapter.py: -------------------------------------------------------------------------------- 1 | # This code implements a clone of the asyncio mainloop which hooks into 2 | # Trio. 3 | 4 | import types 5 | 6 | import asyncio 7 | import trio_asyncio 8 | from contextvars import ContextVar 9 | 10 | from ._util import run_aio_generator, run_aio_future, run_trio_generator 11 | from ._loop import current_loop 12 | 13 | from functools import partial 14 | 15 | 16 | async def _call_defer(proc, *args, **kwargs): 17 | return await proc(*args, **kwargs) 18 | 19 | 20 | class Asyncio_Trio_Wrapper: 21 | """ 22 | This wrapper object encapsulates an asyncio-style coroutine, 23 | procedure, generator, or iterator, to be called seamlessly from Trio. 24 | """ 25 | 26 | def __init__(self, proc, args=[], loop=None): 27 | self.proc = proc 28 | self.args = args 29 | self._loop = loop 30 | 31 | @property 32 | def loop(self): 33 | """The loop argument needs to be lazily evaluated.""" 34 | loop = self._loop 35 | if loop is None: 36 | loop = current_loop.get() 37 | return loop 38 | 39 | def __get__(self, obj, cls): 40 | """If this is used to decorate an instance/class method, 41 | we need to forward the original ``self`` to the wrapped method. 42 | """ 43 | if obj is None: 44 | return partial(self.__call__, cls) 45 | return partial(self.__call__, obj) 46 | 47 | async def __call__(self, *args, **kwargs): 48 | if self.args: 49 | raise RuntimeError( 50 | "Call 'aio_as_trio(proc)(*args)', not 'aio_as_trio(proc, *args)'" 51 | ) 52 | 53 | # We route this through _calL_defer because some wrappers require 54 | # running in asyncio context 55 | f = _call_defer(self.proc, *args, **kwargs) 56 | return await self.loop.run_aio_coroutine(f) 57 | 58 | def __await__(self): 59 | """Support for commonly used (but not recommended) "await aio_as_trio(proc(*args))" """ 60 | f = self.proc 61 | if not hasattr(f, "__await__"): 62 | f = _call_defer(f, *self.args) 63 | elif self.args: 64 | raise RuntimeError("You can't supply arguments to a coroutine") 65 | return self.loop.run_aio_coroutine(f).__await__() 66 | 67 | def __aenter__(self): 68 | proc_enter = getattr(self.proc, "__aenter__", None) 69 | if proc_enter is None or self.args: 70 | raise RuntimeError( 71 | "Call 'aio_as_trio(ctxfactory(*args))', not 'aio_as_trio(ctxfactory, *args)'" 72 | ) 73 | f = proc_enter() 74 | return self.loop.run_aio_coroutine(f) 75 | 76 | def __aexit__(self, *tb): 77 | f = self.proc.__aexit__(*tb) 78 | return self.loop.run_aio_coroutine(f) 79 | 80 | def __aiter__(self): 81 | proc_iter = getattr(self.proc, "__aiter__", None) 82 | if proc_iter is None or self.args: 83 | raise RuntimeError( 84 | "Call 'aio_as_trio(gen(*args))', not 'aio_as_trio(gen, *args)'" 85 | ) 86 | return run_aio_generator(self.loop, proc_iter()) 87 | 88 | 89 | def aio_as_trio(proc, *, loop=None): 90 | """Return a Trio-flavored wrapper for an asyncio-flavored awaitable, 91 | async function, async context manager, or async iterator. 92 | 93 | Alias: ``asyncio_as_trio()`` 94 | 95 | This is the primary interface for calling asyncio code from Trio code. 96 | You can also use it as a decorator on an asyncio-flavored async function; 97 | the decorated function will be callable from Trio-flavored code without 98 | additional boilerplate. 99 | 100 | Note that while adapting coroutines, i.e.:: 101 | 102 | await aio_as_trio(proc(*args)) 103 | 104 | is supported (because asyncio uses them a lot) they're not a good 105 | idea because setting up the coroutine won't run within an asyncio 106 | context. If possible, use:: 107 | 108 | await aio_as_trio(proc)(*args) 109 | 110 | instead. 111 | """ 112 | return Asyncio_Trio_Wrapper(proc, loop=loop) 113 | 114 | 115 | asyncio_as_trio = aio_as_trio 116 | 117 | 118 | class Trio_Asyncio_Wrapper: 119 | """ 120 | This wrapper object encapsulates a Trio-style procedure, 121 | generator, or iterator, to be called seamlessly from asyncio. 122 | """ 123 | 124 | # This class doesn't wrap coroutines because Trio's call convention 125 | # is 126 | # wrap(proc, *args) 127 | # and not 128 | # wrap(proc(*args)) 129 | 130 | def __init__(self, proc, loop=None): 131 | self.proc = proc 132 | self._loop = loop 133 | 134 | @property 135 | def loop(self): 136 | """The loop argument needs to be lazily evaluated.""" 137 | loop = self._loop 138 | if loop is None: 139 | loop = current_loop.get() 140 | return loop 141 | 142 | def __get__(self, obj, cls): 143 | """If this is used to decorate an instance/class method, 144 | we need to forward the original ``self`` to the wrapped method. 145 | """ 146 | if obj is None: 147 | return partial(self.__call__, cls) 148 | return partial(self.__call__, obj) 149 | 150 | def __call__(self, *args, **kwargs): 151 | proc = self.proc 152 | if kwargs: 153 | proc = partial(proc, **kwargs) 154 | return self.loop.trio_as_future(proc, *args) 155 | 156 | def __aenter__(self): 157 | proc_enter = getattr(self.proc, "__aenter__", None) 158 | if proc_enter is None: 159 | raise RuntimeError( 160 | "Call 'trio_as_aio(ctxfactory(*args))', not 'trio_as_aio(ctxfactory, *args)'" 161 | ) 162 | return self.loop.trio_as_future(proc_enter) 163 | 164 | def __aexit__(self, *tb): 165 | proc_exit = self.proc.__aexit__ 166 | return self.loop.trio_as_future(proc_exit, *tb) 167 | 168 | def __aiter__(self): 169 | proc_iter = getattr(self.proc, "__aiter__", None) 170 | if proc_iter is None: 171 | raise RuntimeError( 172 | "Call 'trio_as_aio(gen(*args))', not 'trio_as_aio(gen, *args)'" 173 | ) 174 | return run_trio_generator(self.loop, proc_iter()) 175 | 176 | 177 | def trio_as_aio(proc, *, loop=None): 178 | """Return an asyncio-flavored wrapper for a Trio-flavored async 179 | function, async context manager, or async iterator. 180 | 181 | Alias: ``trio_as_asyncio()`` 182 | 183 | This is the primary interface for calling Trio code from asyncio code. 184 | You can also use it as a decorator on a Trio-flavored async function; 185 | the decorated function will be callable from asyncio-flavored code without 186 | additional boilerplate. 187 | 188 | Note that adapting coroutines, i.e.:: 189 | 190 | await trio_as_aio(proc(*args)) 191 | 192 | is not supported, because Trio does not expose the existence of coroutine 193 | objects in its API. Instead, use:: 194 | 195 | await trio_as_aio(proc)(*args) 196 | 197 | Or if you already have ``proc(*args)`` as a single object ``coro`` for 198 | some reason:: 199 | 200 | await trio_as_aio(lambda: coro)() 201 | 202 | .. warning:: Be careful when using this to wrap an async context manager. 203 | There is currently no mechanism for running the entry and exit in 204 | the same Trio task, so if the async context manager wraps a nursery, 205 | havoc is likely to result. That is, instead of:: 206 | 207 | async def some_aio_func(): 208 | async with trio_asyncio.trio_as_aio(trio.open_nursery()) as nursery: 209 | # code that uses nursery -- this will blow up 210 | 211 | do something like:: 212 | 213 | async def some_aio_func(): 214 | @trio_asyncio.aio_as_trio 215 | async def aio_body(nursery): 216 | # code that uses nursery -- this will work 217 | 218 | @trio_asyncio.trio_as_aio 219 | async def trio_body(): 220 | async with trio.open_nursery() as nursery: 221 | await aio_body(nursery) 222 | 223 | await trio_body() 224 | 225 | """ 226 | return Trio_Asyncio_Wrapper(proc, loop=loop) 227 | 228 | 229 | trio_as_asyncio = trio_as_aio 230 | 231 | _shim_running = ContextVar("shim_running", default=False) 232 | 233 | 234 | @types.coroutine 235 | def _allow_asyncio(fn, *args): 236 | shim = _shim_running 237 | shim.set(True) 238 | 239 | coro = fn(*args) 240 | # start the coroutine 241 | if isinstance(coro, asyncio.Future): 242 | return (yield from trio_asyncio.run_aio_future(coro)) 243 | 244 | p, a = coro.send, None 245 | while True: 246 | try: 247 | yielded = p(a) 248 | except StopIteration as e: 249 | return e.value 250 | try: 251 | if isinstance(yielded, asyncio.Future): 252 | next_send = yield from run_aio_future(yielded) 253 | else: 254 | next_send = yield yielded 255 | except BaseException as exc: 256 | p, a = coro.throw, exc 257 | else: 258 | p, a = coro.send, next_send 259 | 260 | 261 | async def allow_asyncio(fn, *args): 262 | """Execute ``await fn(*args)`` in a context that allows ``fn`` to call 263 | both Trio-flavored and asyncio-flavored functions without marking 264 | which ones are which. 265 | 266 | This is a Trio-flavored async function. There is no asyncio-flavored 267 | equivalent. 268 | 269 | This wrapper allows you to indiscrimnately mix :mod:`trio` and 270 | :mod:`asyncio` functions, generators, or iterators:: 271 | 272 | import trio 273 | import asyncio 274 | import trio_asyncio 275 | 276 | async def hello(loop): 277 | await asyncio.sleep(1) 278 | print("Hello") 279 | await trio.sleep(1) 280 | print("World") 281 | 282 | async def main(): 283 | with trio_asyncio.open_loop() as loop: 284 | await trio_asyncio.allow_asyncio(hello, loop) 285 | trio.run(main) 286 | 287 | Unfortunately, there are issues with cancellation (specifically, 288 | :mod:`asyncio` function will see :class:`trio.Cancelled` instead of 289 | :exc:`concurrent.futures.CancelledError`). Thus, this mode is not the default. 290 | """ 291 | shim = _shim_running 292 | if shim.get(): # nested call: skip 293 | return await fn(*args) 294 | token = shim.set(True) 295 | try: 296 | return await _allow_asyncio(fn, *args) 297 | finally: 298 | shim.reset(token) 299 | -------------------------------------------------------------------------------- /trio_asyncio/_async.py: -------------------------------------------------------------------------------- 1 | import trio 2 | import asyncio 3 | 4 | from ._base import BaseTrioEventLoop, TrioAsyncioExit 5 | 6 | 7 | class TrioEventLoop(BaseTrioEventLoop): 8 | """An asyncio event loop that runs on top of Trio, opened from 9 | within Trio code using :func:`open_loop`. 10 | """ 11 | 12 | def _queue_handle(self, handle): 13 | self._check_closed() 14 | self._q_send.send_nowait(handle) 15 | return handle 16 | 17 | def default_exception_handler(self, context): 18 | """Default exception handler. 19 | 20 | This default handler simply re-raises an exception if there is one. 21 | 22 | Rationale: 23 | 24 | In traditional asyncio, there frequently is no context which the 25 | exception is supposed to affect. 26 | 27 | trio-asyncio, however, collects the loop and all its tasks in 28 | a Trio nursery. This means the context in which the error should 29 | be raised is known and can easily be controlled by the programmer. 30 | 31 | For maximum compatibility, this default handler is only used in 32 | asynchronous loops. 33 | 34 | """ 35 | 36 | # Call the original default handler so we get the full info in the log 37 | super().default_exception_handler(context) 38 | 39 | if self._nursery is None: 40 | # Event loop is already closed; don't do anything further. 41 | # Some asyncio libraries call the asyncio exception handler 42 | # from their __del__ methods, e.g., aiohttp for "Unclosed 43 | # client session". 44 | return 45 | 46 | # Also raise an exception so it can't go unnoticed 47 | exception = context.get("exception") 48 | if exception is None: 49 | message = context.get("message") 50 | if not message: 51 | message = "Unhandled error in event loop" 52 | exception = RuntimeError(message) 53 | 54 | async def propagate_asyncio_error(): 55 | raise exception 56 | 57 | self._nursery.start_soon(propagate_asyncio_error) 58 | 59 | def stop(self, waiter=None): 60 | """Halt the main loop. 61 | 62 | (Or rather, tell it to halt itself soon(ish).) 63 | 64 | :param waiter: an Event that is set when the loop is stopped. 65 | :type waiter: :class:`trio.Event` 66 | :return: Either the Event instance that was passed in, 67 | or a newly-allocated one. 68 | 69 | """ 70 | if waiter is None: 71 | if self._stop_wait is not None: 72 | return self._stop_wait 73 | waiter = self._stop_wait = trio.Event() 74 | else: 75 | if waiter.is_set(): 76 | waiter.clear() 77 | 78 | def stop_me(): 79 | waiter.set() 80 | raise TrioAsyncioExit("stopping trio-asyncio loop") 81 | 82 | if self._stopped.is_set(): 83 | waiter.set() 84 | else: 85 | self._queue_handle(asyncio.Handle(stop_me, (), self)) 86 | return waiter 87 | 88 | def _close(self): 89 | """Hook for actually closing down things.""" 90 | if not self._stopped.is_set(): 91 | raise RuntimeError("You need to stop the loop before closing it") 92 | super()._close() 93 | -------------------------------------------------------------------------------- /trio_asyncio/_child.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import threading 4 | import weakref 5 | import outcome 6 | 7 | import trio 8 | 9 | import logging 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | _mswindows = sys.platform == "win32" 14 | if _mswindows: 15 | import _winapi 16 | 17 | # TODO: use whatever works for Windows and MacOS/BSD 18 | 19 | _children = weakref.WeakValueDictionary() 20 | 21 | 22 | class UnknownStatus(ChildProcessError): 23 | pass 24 | 25 | 26 | def _compute_returncode(status): 27 | if os.WIFSIGNALED(status): 28 | # The child process died because of a signal. 29 | return -os.WTERMSIG(status) 30 | elif os.WIFEXITED(status): 31 | # The child process exited (e.g sys.exit()). 32 | return os.WEXITSTATUS(status) 33 | elif os.WIFSTOPPED(status): 34 | return -os.WSTOPSIG(status) 35 | else: 36 | # This shouldn't happen. 37 | raise UnknownStatus(status) 38 | 39 | 40 | def NOT_FOUND(): 41 | return outcome.Error(ChildProcessError()) 42 | 43 | 44 | class ProcessWaiter: 45 | """I implement waiting for a child process.""" 46 | 47 | __token = None 48 | __event = None 49 | __pid = None 50 | __result = None 51 | __thread = None 52 | _handle = None 53 | 54 | def __new__(cls, pid=None, _handle=None): 55 | """Grab an existing object if there is one""" 56 | self = None 57 | if pid is not None: 58 | self = _children.get(pid, None) 59 | if self is None: 60 | self = object.__new__(cls) 61 | return self 62 | 63 | def __init__(self, pid=None, _handle=None): 64 | if self.__pid is None: 65 | self._set_pid(pid, _handle) 66 | 67 | def _set_pid(self, pid, _handle=None): 68 | if self.__pid is not None: 69 | raise RuntimeError("You can't change the pid") 70 | if not isinstance(pid, int): 71 | raise RuntimeError("a PID needs to be an integer") 72 | 73 | self.__pid = pid 74 | _children[pid] = self 75 | 76 | if _mswindows: 77 | if _handle is None: 78 | _handle = _winapi.OpenProcess(_winapi.PROCESS_ALL_ACCESS, True, pid) 79 | self.__handle = _handle 80 | elif _handle is not None: 81 | raise RuntimeError("Process handles are a Windows thing.") 82 | 83 | async def wait(self): 84 | """Wait for this child process to end.""" 85 | if self.__result is None: 86 | if self.__pid is None: 87 | raise RuntimeError("I don't know what to wait for!") 88 | 89 | # Check once, before doing the heavy lifting 90 | self._wait_pid(blocking=False) 91 | if self.__result is None: 92 | if self.__thread is None: 93 | await self._start_waiting() 94 | await self.__event.wait() 95 | return self.__result.unwrap() 96 | 97 | async def _start_waiting(self): 98 | """Start the background thread that waits for a specific child""" 99 | self.__event = trio.Event() 100 | self.__token = trio.lowlevel.current_trio_token() 101 | 102 | self.__thread = threading.Thread( 103 | target=self._wait_thread, name="waitpid_%d" % self.__pid, daemon=True 104 | ) 105 | self.__thread.start() 106 | 107 | def _wait_thread(self): 108 | """The background thread that waits for a specific child""" 109 | self._wait_pid(blocking=True) 110 | self.__token.run_sync_soon(self.__event.set) 111 | 112 | if _mswindows: 113 | 114 | def _wait_pid(self, blocking): 115 | assert self.__handle is not None 116 | if blocking: 117 | timeout = _winapi.INFINITE 118 | else: 119 | timeout = 0 120 | result = _winapi.WaitForSingleObject(self.__handle, timeout) 121 | if result != _winapi.WAIT_TIMEOUT: 122 | self.__result = _winapi.GetExitCodeProcess(self._handle) 123 | 124 | else: 125 | 126 | def _wait_pid(self, blocking): 127 | """check up on a child process""" 128 | assert self.__pid > 0 129 | 130 | try: 131 | pid, status = os.waitpid(self.__pid, 0 if blocking else os.WNOHANG) 132 | except ChildProcessError: 133 | # The child process may already be reaped 134 | # (may happen if waitpid() is called elsewhere). 135 | self.__result = NOT_FOUND() 136 | else: 137 | if pid == 0: 138 | # The child process is still alive. 139 | return 140 | del _children[pid] 141 | self._handle_exitstatus(status) 142 | 143 | def _handle_exitstatus(self, sts): 144 | """This overrides an internal API of subprocess.Popen""" 145 | self.__result = outcome.capture(_compute_returncode, sts) 146 | 147 | @property 148 | def returncode(self): 149 | if self.__result is None: 150 | return None 151 | return self.__result.unwrap() 152 | 153 | 154 | async def wait_for_child(pid): 155 | waiter = ProcessWaiter(pid) 156 | return await waiter.wait() 157 | -------------------------------------------------------------------------------- /trio_asyncio/_deprecate.py: -------------------------------------------------------------------------------- 1 | # Mostly copied from trio._deprecate. 2 | 3 | import sys 4 | from functools import wraps 5 | from types import ModuleType 6 | import warnings 7 | 8 | import attr 9 | 10 | 11 | # We want our warnings to be visible by default (at least for now), but we 12 | # also want it to be possible to override that using the -W switch. AFAICT 13 | # this means we cannot inherit from DeprecationWarning, because the only way 14 | # to make it visible by default then would be to add our own filter at import 15 | # time, but that would override -W switches... 16 | class TrioAsyncioDeprecationWarning(FutureWarning): 17 | """Warning emitted if you use deprecated trio-asyncio functionality. 18 | 19 | This inherits from `FutureWarning`, not `DeprecationWarning`, for the 20 | same reasons described for `trio.TrioDeprecationWarning`. 21 | """ 22 | 23 | 24 | def _url_for_issue(issue): 25 | return "https://github.com/python-trio/trio-asyncio/issues/{}".format(issue) 26 | 27 | 28 | def _stringify(thing): 29 | if hasattr(thing, "__module__") and hasattr(thing, "__qualname__"): 30 | return "{}.{}".format(thing.__module__, thing.__qualname__) 31 | return str(thing) 32 | 33 | 34 | def warn_deprecated(thing, version, *, issue, instead, stacklevel=2): 35 | stacklevel += 1 36 | msg = "{} is deprecated since trio-asyncio {}".format(_stringify(thing), version) 37 | if instead is None: 38 | msg += " with no replacement" 39 | else: 40 | msg += "; use {} instead".format(_stringify(instead)) 41 | if issue is not None: 42 | msg += " ({})".format(_url_for_issue(issue)) 43 | warnings.warn(TrioAsyncioDeprecationWarning(msg), stacklevel=stacklevel) 44 | 45 | 46 | # @deprecated("0.2.0", issue=..., instead=...) 47 | # def ... 48 | def deprecated(version, *, thing=None, issue, instead): 49 | def do_wrap(fn): 50 | nonlocal thing 51 | 52 | @wraps(fn) 53 | def wrapper(*args, **kwargs): 54 | warn_deprecated(thing, version, instead=instead, issue=issue) 55 | return fn(*args, **kwargs) 56 | 57 | # If our __module__ or __qualname__ get modified, we want to pick up 58 | # on that, so we read them off the wrapper object instead of the (now 59 | # hidden) fn object 60 | if thing is None: 61 | thing = wrapper 62 | 63 | if wrapper.__doc__ is not None: 64 | doc = wrapper.__doc__ 65 | doc = doc.rstrip() 66 | doc += "\n\n" 67 | doc += ".. deprecated:: {}\n".format(version) 68 | if instead is not None: 69 | doc += " Use {} instead.\n".format(_stringify(instead)) 70 | if issue is not None: 71 | doc += " For details, see `issue #{} <{}>`__.\n".format( 72 | issue, _url_for_issue(issue) 73 | ) 74 | doc += "\n" 75 | wrapper.__doc__ = doc 76 | 77 | return wrapper 78 | 79 | return do_wrap 80 | 81 | 82 | def deprecated_alias(old_qualname, new_fn, version, *, issue): 83 | @deprecated(version, issue=issue, instead=new_fn) 84 | @wraps(new_fn, assigned=("__module__", "__annotations__")) 85 | def wrapper(*args, **kwargs): 86 | "Deprecated alias." 87 | return new_fn(*args, **kwargs) 88 | 89 | wrapper.__qualname__ = old_qualname 90 | wrapper.__name__ = old_qualname.rpartition(".")[-1] 91 | return wrapper 92 | 93 | 94 | @attr.s(frozen=True) 95 | class DeprecatedAttribute: 96 | _not_set = object() 97 | 98 | value = attr.ib() 99 | version = attr.ib() 100 | issue = attr.ib() 101 | instead = attr.ib(default=_not_set) 102 | 103 | 104 | class _ModuleWithDeprecations(ModuleType): 105 | def __getattr__(self, name): 106 | if name in self.__deprecated_attributes__: 107 | info = self.__deprecated_attributes__[name] 108 | instead = info.instead 109 | if instead is DeprecatedAttribute._not_set: 110 | instead = info.value 111 | thing = "{}.{}".format(self.__name__, name) 112 | warn_deprecated(thing, info.version, issue=info.issue, instead=instead) 113 | return info.value 114 | 115 | raise AttributeError(name) 116 | 117 | 118 | def enable_attribute_deprecations(module_name): 119 | module = sys.modules[module_name] 120 | module.__class__ = _ModuleWithDeprecations 121 | # Make sure that this is always defined so that 122 | # _ModuleWithDeprecations.__getattr__ can access it without jumping 123 | # through hoops or risking infinite recursion. 124 | module.__deprecated_attributes__ = {} 125 | -------------------------------------------------------------------------------- /trio_asyncio/_handles.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import trio 3 | import types 4 | import asyncio 5 | from asyncio.format_helpers import _format_callback, _get_function_source 6 | 7 | if sys.version_info < (3, 11): 8 | from exceptiongroup import BaseExceptionGroup 9 | 10 | 11 | def _format_callback_source(func, args): 12 | func_repr = _format_callback(func, args, None) 13 | source = _get_function_source(func) 14 | if source: # pragma: no cover 15 | func_repr += " at %s:%s" % source 16 | return func_repr 17 | 18 | 19 | class ScopedHandle(asyncio.Handle): 20 | """An asyncio.Handle that cancels a trio.CancelScope when the Handle is cancelled. 21 | 22 | This is used to manage installed readers and writers, so that the Trio call to 23 | wait_readable() or wait_writable() can be cancelled when the handle is. 24 | """ 25 | 26 | __slots__ = ("_scope",) 27 | 28 | def __init__(self, *args, **kw): 29 | super().__init__(*args, **kw) 30 | self._scope = trio.CancelScope() 31 | 32 | def cancel(self): 33 | super().cancel() 34 | self._loop._trio_io_cancel(self._scope) 35 | 36 | def _repr_info(self): 37 | return super()._repr_info() + ["scope={!r}".format(self._scope)] 38 | 39 | def _raise(self, exc): 40 | """This is a copy of the exception handling in asyncio.events.Handle._run(). 41 | It's used to report exceptions that arise when waiting for readability 42 | or writability, and exceptions in async tasks managed by our subclass 43 | AsyncHandle. 44 | """ 45 | cb = _format_callback_source(self._callback, self._args) 46 | msg = "Exception in callback {}".format(cb) 47 | context = { 48 | "message": msg, 49 | "exception": exc, 50 | "handle": self, 51 | } 52 | if self._source_traceback: 53 | context["source_traceback"] = self._source_traceback 54 | self._loop.call_exception_handler(context) 55 | 56 | 57 | # copied from trio._core._multierror, but relying on traceback constructability 58 | # from Python (as introduced in 3.7) instead of ctypes hackery 59 | def concat_tb(head, tail): 60 | # We have to use an iterative algorithm here, because in the worst case 61 | # this might be a RecursionError stack that is by definition too deep to 62 | # process by recursion! 63 | head_tbs = [] 64 | pointer = head 65 | while pointer is not None: 66 | head_tbs.append(pointer) 67 | pointer = pointer.tb_next 68 | current_head = tail 69 | for head_tb in reversed(head_tbs): 70 | current_head = types.TracebackType( 71 | current_head, head_tb.tb_frame, head_tb.tb_lasti, head_tb.tb_lineno 72 | ) 73 | return current_head 74 | 75 | 76 | # copied from trio._core._run with minor modifications: 77 | def collapse_exception_group(excgroup): 78 | """Recursively collapse any single-exception groups into that single contained 79 | exception. 80 | """ 81 | exceptions = list(excgroup.exceptions) 82 | modified = False 83 | for i, exc in enumerate(exceptions): 84 | if isinstance(exc, BaseExceptionGroup): 85 | new_exc = collapse_exception_group(exc) 86 | if new_exc is not exc: 87 | modified = True 88 | exceptions[i] = new_exc 89 | 90 | collapse = getattr(excgroup, "collapse", False) or ( # Trio 0.23.0 # Trio 0.24.0 91 | getattr(trio._core._run, "NONSTRICT_EXCEPTIONGROUP_NOTE", object()) 92 | in getattr(excgroup, "__notes__", ()) 93 | ) 94 | if len(exceptions) == 1 and collapse: 95 | exceptions[0].__traceback__ = concat_tb( 96 | excgroup.__traceback__, exceptions[0].__traceback__ 97 | ) 98 | return exceptions[0] 99 | elif modified: 100 | return excgroup.derive(exceptions) 101 | else: 102 | return excgroup 103 | 104 | 105 | def collapse_aware_exception_split(exc, etype): 106 | if not isinstance(exc, BaseExceptionGroup): 107 | if isinstance(exc, etype): 108 | return exc, None 109 | else: 110 | return None, exc 111 | 112 | match, rest = exc.split(etype) 113 | if isinstance(match, BaseExceptionGroup): 114 | match = collapse_exception_group(match) 115 | if isinstance(rest, BaseExceptionGroup): 116 | rest = collapse_exception_group(rest) 117 | return match, rest 118 | 119 | 120 | class AsyncHandle(ScopedHandle): 121 | """A ScopedHandle associated with the execution of a Trio-flavored 122 | async function. 123 | 124 | If the handle is cancelled, the cancel scope surrounding the async function 125 | will be cancelled too. It is also possible to link a future to the result 126 | of the async function. If you do that, the future will evaluate to the 127 | result of the function, and cancelling the future will cancel the handle too. 128 | 129 | """ 130 | 131 | __slots__ = ("_fut", "_started", "_finished", "_cancel_message") 132 | 133 | def __init__(self, *args, result_future=None, **kw): 134 | super().__init__(*args, **kw) 135 | self._fut = result_future 136 | self._started = trio.Event() 137 | self._finished = False 138 | self._cancel_message = None 139 | 140 | if self._fut is not None: 141 | orig_cancel = self._fut.cancel 142 | 143 | def wrapped_cancel(msg=None): 144 | if self._finished: 145 | # We're being called back after the task completed 146 | if msg is not None: 147 | return orig_cancel(msg) 148 | elif self._cancel_message is not None: 149 | return orig_cancel(self._cancel_message) 150 | else: 151 | return orig_cancel() 152 | if self._fut.done(): 153 | return False 154 | # Forward cancellation to the Trio task, don't mark 155 | # future as cancelled until it completes 156 | self._cancel_message = msg 157 | self.cancel() 158 | return True 159 | 160 | self._fut.cancel = wrapped_cancel 161 | 162 | async def _run(self): 163 | self._started.set() 164 | if self._cancelled: 165 | self._finished = True 166 | return 167 | 168 | try: 169 | # Run the callback 170 | with self._scope: 171 | try: 172 | res = await self._callback(*self._args) 173 | finally: 174 | self._finished = True 175 | 176 | if self._fut: 177 | # Propagate result or just-this-handle cancellation to the Future 178 | if self._scope.cancelled_caught: 179 | self._fut.cancel() 180 | elif not self._fut.cancelled(): 181 | self._fut.set_result(res) 182 | 183 | except BaseException as exc: 184 | if not self._fut: 185 | # Pass Exceptions through the fallback exception 186 | # handler since they have nowhere better to go. (In an 187 | # async loop this will still raise the exception out 188 | # of the loop, terminating it.) Let BaseExceptions 189 | # escape so that Cancelled and SystemExit work 190 | # reasonably. 191 | rest, base = collapse_aware_exception_split(exc, Exception) 192 | if rest: 193 | self._raise(rest) 194 | if base: 195 | raise base 196 | else: 197 | # The result future gets all the non-Cancelled 198 | # exceptions. Any Cancelled need to keep propagating 199 | # out of this stack frame in order to reach the cancel 200 | # scope for which they're intended. Any non-Cancelled 201 | # BaseExceptions keep propagating. 202 | cancelled, rest = collapse_aware_exception_split(exc, trio.Cancelled) 203 | if not self._fut.cancelled(): 204 | if rest: 205 | self._fut.set_exception(rest) 206 | else: 207 | self._fut.cancel() 208 | if cancelled: 209 | raise cancelled 210 | 211 | finally: 212 | # asyncio says this is needed to break cycles when an exception occurs. 213 | # I'm not so sure, but it doesn't seem to do any harm. 214 | self = None 215 | -------------------------------------------------------------------------------- /trio_asyncio/_sync.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import trio 4 | import queue 5 | import signal 6 | import asyncio 7 | import outcome 8 | import greenlet 9 | import contextlib 10 | 11 | from ._base import BaseTrioEventLoop, TrioAsyncioExit 12 | from ._loop import current_loop 13 | 14 | 15 | async def _sync(proc, *args): 16 | return proc(*args) 17 | 18 | 19 | # Context manager to ensure all between-greenlet switches occur with 20 | # an empty trio run context and unset signal wakeup fd. That way, each 21 | # greenlet can have its own private Trio run. 22 | @contextlib.contextmanager 23 | def clean_trio_state(): 24 | trio_globals = trio._core._run.GLOBAL_RUN_CONTEXT.__dict__ 25 | old_state = trio_globals.copy() 26 | old_wakeup_fd = None 27 | try: 28 | old_wakeup_fd = signal.set_wakeup_fd(-1) 29 | except ValueError: 30 | pass # probably we're on the non-main thread 31 | trio_globals.clear() 32 | try: 33 | yield 34 | finally: 35 | if old_wakeup_fd is not None: 36 | signal.set_wakeup_fd(old_wakeup_fd, warn_on_full_buffer=(not old_state)) 37 | trio_globals.clear() 38 | trio_globals.update(old_state) 39 | 40 | 41 | class SyncTrioEventLoop(BaseTrioEventLoop): 42 | """ 43 | This is the "compatibility mode" implementation of the Trio/asyncio 44 | event loop. It runs synchronously, by delegating the Trio event loop to 45 | a separate thread. 46 | 47 | For best results, you should use the asynchronous 48 | :class:`trio_asyncio.TrioEventLoop` – if possible. 49 | """ 50 | 51 | _loop_running = False 52 | _stop_pending = False 53 | _glet = None 54 | 55 | def __init__(self, *args, **kwds): 56 | super().__init__(*args, **kwds) 57 | 58 | # We must start the Trio loop immediately so that self.time() works 59 | self._glet = greenlet.greenlet(trio.run) 60 | with clean_trio_state(): 61 | if not self._glet.switch(self.__trio_main): 62 | raise RuntimeError("Loop could not be started") 63 | 64 | def stop(self): 65 | """Halt the main loop. 66 | 67 | Any callbacks queued before this point are processed before 68 | stopping. 69 | 70 | """ 71 | 72 | def do_stop(): 73 | self._stop_pending = False 74 | raise TrioAsyncioExit("stopping trio-asyncio loop") 75 | 76 | if self._loop_running and not self._stop_pending: 77 | self._stop_pending = True 78 | self._queue_handle(asyncio.Handle(do_stop, (), self)) 79 | 80 | def _queue_handle(self, handle): 81 | self._check_closed() 82 | if self._glet is not greenlet.getcurrent() and self._token is not None: 83 | self.__run_in_greenlet(_sync, self._q_send.send_nowait, handle) 84 | else: 85 | self._q_send.send_nowait(handle) 86 | return handle 87 | 88 | def run_forever(self): 89 | self.__run_in_greenlet(self._main_loop) 90 | 91 | def is_running(self): 92 | if self._closed: 93 | return False 94 | return self._loop_running 95 | 96 | def _add_reader(self, fd, callback, *args): 97 | if self._glet is not greenlet.getcurrent() and self._token is not None: 98 | self.__run_in_greenlet(_sync, super()._add_reader, fd, callback, *args) 99 | else: 100 | super()._add_reader(fd, callback, *args) 101 | 102 | def _add_writer(self, fd, callback, *args): 103 | if self._glet is not greenlet.getcurrent() and self._token is not None: 104 | self.__run_in_greenlet(_sync, super()._add_writer, fd, callback, *args) 105 | else: 106 | super()._add_writer(fd, callback, *args) 107 | 108 | def _trio_io_cancel(self, cancel_scope): 109 | if self._glet is not greenlet.getcurrent() and self._token is not None: 110 | self.__run_in_greenlet(_sync, cancel_scope.cancel) 111 | else: 112 | cancel_scope.cancel() 113 | 114 | def run_until_complete(self, future): 115 | """Run until the Future is done. 116 | 117 | If the argument is a coroutine, it is wrapped in a Task. 118 | 119 | WARNING: It would be disastrous to call run_until_complete() 120 | with the same coroutine twice -- it would wrap it in two 121 | different Tasks and that can't be good. 122 | 123 | Return the Future's result, or raise its exception. 124 | """ 125 | return self.__run_in_greenlet(self._run_coroutine, future) 126 | 127 | async def _run_coroutine(self, future): 128 | """Helper for run_until_complete(). 129 | 130 | We need to make sure that a RuntimeError is raised 131 | if the loop is stopped before the future completes. 132 | 133 | This code runs in the Trio greenlet. 134 | """ 135 | result = None 136 | future = asyncio.ensure_future(future, loop=self) 137 | 138 | def is_done(_): 139 | nonlocal result 140 | 141 | result = outcome.capture(future.result) 142 | if isinstance(result, outcome.Error) and isinstance( 143 | result.error, (SystemExit, KeyboardInterrupt) 144 | ): 145 | # These exceptions propagate out of the event loop; 146 | # don't stop the event loop again, or else it will 147 | # interfere with cleanup actions like 148 | # run_until_complete(shutdown_asyncgens) 149 | return 150 | self.stop() 151 | 152 | future.add_done_callback(is_done) 153 | try: 154 | await self._main_loop() 155 | try: 156 | while result is None: 157 | try: 158 | await self._main_loop_one(no_wait=True) 159 | except TrioAsyncioExit: 160 | pass 161 | except trio.WouldBlock: 162 | pass 163 | finally: 164 | future.remove_done_callback(is_done) 165 | 166 | if result is None: 167 | raise RuntimeError("Event loop stopped before Future completed.") 168 | return result.unwrap() 169 | 170 | def __run_in_greenlet(self, async_fn, *args): 171 | self._check_closed() 172 | if self._loop_running: 173 | raise RuntimeError( 174 | "You can't nest calls to run_until_complete()/run_forever()." 175 | ) 176 | if asyncio._get_running_loop() is not None: 177 | raise RuntimeError( 178 | "Cannot run the event loop while another loop is running" 179 | ) 180 | if not self._glet: 181 | if async_fn is _sync: 182 | # Allow for cleanups during close() 183 | sync_fn, *args = args 184 | return sync_fn(*args) 185 | raise RuntimeError("The Trio greenlet is not running") 186 | with clean_trio_state(): 187 | res = self._glet.switch((greenlet.getcurrent(), async_fn, args)) 188 | if res is None: 189 | raise RuntimeError("Loop has died / terminated") 190 | return res.unwrap() 191 | 192 | async def __trio_main(self): 193 | from ._loop import _sync_loop_task_name 194 | 195 | trio.lowlevel.current_task().name = _sync_loop_task_name 196 | 197 | # The non-context-manager equivalent of open_loop() 198 | async with trio.open_nursery() as nursery: 199 | await self._main_loop_init(nursery) 200 | with clean_trio_state(): 201 | req = greenlet.getcurrent().parent.switch(True) 202 | 203 | while not self._closed: 204 | if req is None: 205 | break 206 | caller, async_fn, args = req 207 | 208 | self._loop_running = True 209 | asyncio._set_running_loop(self) 210 | current_loop.set(self) 211 | result = await outcome.acapture(async_fn, *args) 212 | asyncio._set_running_loop(None) 213 | current_loop.set(None) 214 | self._loop_running = False 215 | 216 | if isinstance(result, outcome.Error) and isinstance( 217 | result.error, trio.Cancelled 218 | ): 219 | res = RuntimeError("Main loop cancelled") 220 | res.__cause__ = result.error.__cause__ 221 | result = outcome.Error(res) 222 | 223 | with clean_trio_state(): 224 | req = caller.switch(result) 225 | 226 | with trio.CancelScope(shield=True): 227 | await self._main_loop_exit() 228 | nursery.cancel_scope.cancel() 229 | 230 | def __enter__(self): 231 | return self 232 | 233 | def __exit__(self, *tb): 234 | self.stop() 235 | self.close() 236 | assert self._glet is None 237 | 238 | def _close(self): 239 | """Hook to terminate the thread""" 240 | if self._glet is not None: 241 | if self._glet is greenlet.getcurrent(): 242 | raise RuntimeError("You can't close a sync loop from the inside") 243 | # The parent will generally already be this greenlet, but might 244 | # not be in nested-loop cases. 245 | self._glet.parent = greenlet.getcurrent() 246 | with clean_trio_state(): 247 | self._glet.switch(None) 248 | assert self._glet.dead 249 | self._glet = None 250 | super()._close() 251 | -------------------------------------------------------------------------------- /trio_asyncio/_util.py: -------------------------------------------------------------------------------- 1 | # This code implements helper functions that work without running 2 | # a TrioEventLoop. 3 | 4 | import trio 5 | import asyncio 6 | import sys 7 | import outcome 8 | 9 | 10 | async def run_aio_future(future): 11 | """Wait for an asyncio-flavored future to become done, then return 12 | or raise its result. 13 | 14 | Cancelling the current Trio scope will cancel the future. If this 15 | results in the future resolving to an `asyncio.CancelledError` 16 | exception, the call to :func:`run_aio_future` will raise 17 | `trio.Cancelled`. But if the future resolves to 18 | `~asyncio.CancelledError` when the current Trio scope was *not* 19 | cancelled, the `~asyncio.CancelledError` will be passed along 20 | unchanged. 21 | 22 | This is a Trio-flavored async function. 23 | 24 | """ 25 | task = trio.lowlevel.current_task() 26 | raise_cancel = None 27 | 28 | def done_cb(_): 29 | trio.lowlevel.reschedule(task, outcome.capture(future.result)) 30 | 31 | future.add_done_callback(done_cb) 32 | 33 | def abort_cb(raise_cancel_arg): 34 | # Save the cancel-raising function 35 | nonlocal raise_cancel 36 | raise_cancel = raise_cancel_arg 37 | # Attempt to cancel our future 38 | future.cancel() 39 | # Keep waiting 40 | return trio.lowlevel.Abort.FAILED 41 | 42 | try: 43 | res = await trio.lowlevel.wait_task_rescheduled(abort_cb) 44 | return res 45 | except asyncio.CancelledError as exc: 46 | if raise_cancel is not None: 47 | try: 48 | raise_cancel() 49 | finally: 50 | # Try to preserve the exception chain, 51 | # for more detailed tracebacks 52 | sys.exc_info()[1].__cause__ = exc 53 | else: 54 | raise 55 | 56 | 57 | STOP = object() 58 | 59 | 60 | async def run_aio_generator(loop, async_generator): 61 | """Return a Trio-flavored async iterator which wraps the given 62 | asyncio-flavored async iterator (usually an async generator, but 63 | doesn't have to be). The asyncio tasks that perform each iteration 64 | of *async_generator* will run in *loop*. 65 | """ 66 | task = trio.lowlevel.current_task() 67 | raise_cancel = None 68 | current_read = None 69 | 70 | async def consume_next(): 71 | try: 72 | item = await async_generator.__anext__() 73 | result = outcome.Value(value=item) 74 | except StopAsyncIteration: 75 | result = outcome.Value(value=STOP) 76 | except asyncio.CancelledError: 77 | # Once we are cancelled, we do not call reschedule() anymore 78 | return 79 | except Exception as e: 80 | result = outcome.Error(error=e) 81 | 82 | trio.lowlevel.reschedule(task, result) 83 | 84 | def abort_cb(raise_cancel_arg): 85 | # Save the cancel-raising function 86 | nonlocal raise_cancel 87 | raise_cancel = raise_cancel_arg 88 | 89 | if not current_read: 90 | # There is no current read 91 | return trio.lowlevel.Abort.SUCCEEDED 92 | else: 93 | # Attempt to cancel the current iterator read, do not 94 | # report success until the future was actually cancelled. 95 | already_cancelled = current_read.cancel() 96 | if already_cancelled: 97 | return trio.lowlevel.Abort.SUCCEEDED 98 | else: 99 | # Continue dealing with the cancellation once 100 | # future.cancel() goes to the result of 101 | # wait_task_rescheduled() 102 | return trio.lowlevel.Abort.FAILED 103 | 104 | try: 105 | while True: 106 | # Schedule in asyncio that we read the next item from the iterator 107 | current_read = asyncio.ensure_future(consume_next()) 108 | 109 | item = await trio.lowlevel.wait_task_rescheduled(abort_cb) 110 | 111 | if item is STOP: 112 | break 113 | yield item 114 | 115 | except asyncio.CancelledError as exc: 116 | if raise_cancel is not None: 117 | try: 118 | raise_cancel() 119 | finally: 120 | # Try to preserve the exception chain, 121 | # for more detailed tracebacks 122 | sys.exc_info()[1].__cause__ = exc 123 | else: 124 | raise 125 | 126 | 127 | async def run_trio_generator(loop, async_generator): 128 | """Run a Trio generator from within asyncio""" 129 | while True: 130 | # Schedule in asyncio that we read the next item from the iterator 131 | try: 132 | item = await loop.trio_as_future(async_generator.__anext__) 133 | except StopAsyncIteration: 134 | break 135 | else: 136 | yield item 137 | 138 | 139 | # Copied from Trio: 140 | def fixup_module_metadata(module_name, namespace): 141 | seen_ids = set() 142 | 143 | def fix_one(qualname, name, obj): 144 | # avoid infinite recursion (relevant when using 145 | # typing.Generic, for example) 146 | if id(obj) in seen_ids: 147 | return 148 | seen_ids.add(id(obj)) 149 | 150 | mod = getattr(obj, "__module__", None) 151 | if mod is not None and mod.startswith("trio_asyncio."): 152 | obj.__module__ = module_name 153 | # Modules, unlike everything else in Python, put fully-qualitied 154 | # names into their __name__ attribute. We check for "." to avoid 155 | # rewriting these. 156 | if hasattr(obj, "__name__") and "." not in obj.__name__: 157 | obj.__name__ = name 158 | obj.__qualname__ = qualname 159 | if isinstance(obj, type): 160 | for attr_name, attr_value in obj.__dict__.items(): 161 | fix_one(objname + "." + attr_name, attr_name, attr_value) 162 | 163 | for objname, obj in namespace.items(): 164 | if not objname.startswith("_"): # ignore private attributes 165 | fix_one(objname, objname, obj) 166 | -------------------------------------------------------------------------------- /trio_asyncio/_version.py: -------------------------------------------------------------------------------- 1 | # This file is imported from __init__.py and exec'd from setup.py 2 | 3 | __version__ = "0.15.0+dev" 4 | --------------------------------------------------------------------------------