├── .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 |
--------------------------------------------------------------------------------