├── .coveragerc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .mypy.ini ├── .readthedocs.yml ├── .style.yapf ├── CHEATSHEET.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.APACHE2 ├── LICENSE.MIT ├── MANIFEST.in ├── README.rst ├── ci.sh ├── ci └── rtd-requirements.txt ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── .gitkeep │ ├── conf.py │ ├── history.rst │ └── index.rst ├── newsfragments ├── .gitkeep └── README.rst ├── pyproject.toml ├── sniffio ├── __init__.py ├── _impl.py ├── _tests │ ├── __init__.py │ └── test_sniffio.py ├── _version.py └── py.typed └── test-requirements.txt /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch=True 3 | source=sniffio 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: [push, pull_request] 4 | 5 | jobs: 6 | Windows: 7 | name: 'Windows (${{ matrix.python }})' 8 | runs-on: 'windows-latest' 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Setup python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python }} 21 | allow-prereleases: true 22 | cache: pip 23 | cache-dependency-path: test-requirements.txt 24 | - name: Run tests 25 | run: ./ci.sh 26 | shell: bash 27 | env: 28 | # Should match 'name:' up above 29 | JOB_NAME: 'Windows (${{ matrix.python }})' 30 | 31 | Ubuntu: 32 | name: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' 33 | timeout-minutes: 10 34 | runs-on: 'ubuntu-latest' 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12-dev'] 39 | check_formatting: ['0'] 40 | extra_name: [''] 41 | include: 42 | - python: '3.10' 43 | check_formatting: '1' 44 | extra_name: ', check formatting' 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | - name: Setup python 49 | uses: actions/setup-python@v4 50 | if: "!endsWith(matrix.python, '-dev')" 51 | with: 52 | python-version: ${{ matrix.python }} 53 | cache: pip 54 | cache-dependency-path: test-requirements.txt 55 | - name: Setup python (dev) 56 | uses: deadsnakes/action@v3.0.1 57 | if: endsWith(matrix.python, '-dev') 58 | with: 59 | python-version: '${{ matrix.python }}' 60 | - name: Run tests 61 | run: ./ci.sh 62 | env: 63 | CHECK_FORMATTING: '${{ matrix.check_formatting }}' 64 | # Should match 'name:' up above 65 | JOB_NAME: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' 66 | 67 | macOS: 68 | name: 'macOS (${{ matrix.python }})' 69 | timeout-minutes: 10 70 | runs-on: 'macos-latest' 71 | strategy: 72 | fail-fast: false 73 | matrix: 74 | python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | - name: Setup python 79 | uses: actions/setup-python@v4 80 | with: 81 | python-version: ${{ matrix.python }} 82 | allow-prereleases: true 83 | cache: pip 84 | cache-dependency-path: test-requirements.txt 85 | - name: Run tests 86 | run: ./ci.sh 87 | env: 88 | # Should match 'name:' up above 89 | JOB_NAME: 'macOS (${{ matrix.python }})' 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Add any project-specific files here: 2 | 3 | 4 | # Sphinx docs 5 | docs/build/ 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *~ 11 | \#* 12 | .#* 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | /build/ 20 | /develop-eggs/ 21 | /dist/ 22 | /eggs/ 23 | /lib/ 24 | /lib64/ 25 | /parts/ 26 | /sdist/ 27 | /var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | .pytest_cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # Error codes can be used to write more specific `type: ignore` comments. 3 | show_error_codes = True 4 | 5 | [mypy-curio.*] 6 | # Curio doesn't provide type hints. 7 | ignore_missing_imports = True 8 | 9 | [mypy-pytest.*] 10 | # The version of pytest used doesn't provide type hints. 11 | ignore_missing_imports = True 12 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/latest/yaml-config.html 2 | version: 2 3 | 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.8" 8 | 9 | formats: 10 | - htmlzip 11 | - epub 12 | 13 | python: 14 | install: 15 | - requirements: ci/rtd-requirements.txt 16 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | # Align closing bracket with visual indentation. 3 | align_closing_bracket_with_visual_indent=True 4 | 5 | # Allow dictionary keys to exist on multiple lines. For example: 6 | # 7 | # x = { 8 | # ('this is the first element of a tuple', 9 | # 'this is the second element of a tuple'): 10 | # value, 11 | # } 12 | allow_multiline_dictionary_keys=False 13 | 14 | # Allow lambdas to be formatted on more than one line. 15 | allow_multiline_lambdas=False 16 | 17 | # Insert a blank line before a class-level docstring. 18 | blank_line_before_class_docstring=False 19 | 20 | # Insert a blank line before a 'def' or 'class' immediately nested 21 | # within another 'def' or 'class'. For example: 22 | # 23 | # class Foo: 24 | # # <------ this blank line 25 | # def method(): 26 | # ... 27 | blank_line_before_nested_class_or_def=False 28 | 29 | # Do not split consecutive brackets. Only relevant when 30 | # dedent_closing_brackets is set. For example: 31 | # 32 | # call_func_that_takes_a_dict( 33 | # { 34 | # 'key1': 'value1', 35 | # 'key2': 'value2', 36 | # } 37 | # ) 38 | # 39 | # would reformat to: 40 | # 41 | # call_func_that_takes_a_dict({ 42 | # 'key1': 'value1', 43 | # 'key2': 'value2', 44 | # }) 45 | coalesce_brackets=False 46 | 47 | # The column limit. 48 | column_limit=79 49 | 50 | # Indent width used for line continuations. 51 | continuation_indent_width=4 52 | 53 | # Put closing brackets on a separate line, dedented, if the bracketed 54 | # expression can't fit in a single line. Applies to all kinds of brackets, 55 | # including function definitions and calls. For example: 56 | # 57 | # config = { 58 | # 'key1': 'value1', 59 | # 'key2': 'value2', 60 | # } # <--- this bracket is dedented and on a separate line 61 | # 62 | # time_series = self.remote_client.query_entity_counters( 63 | # entity='dev3246.region1', 64 | # key='dns.query_latency_tcp', 65 | # transform=Transformation.AVERAGE(window=timedelta(seconds=60)), 66 | # start_ts=now()-timedelta(days=3), 67 | # end_ts=now(), 68 | # ) # <--- this bracket is dedented and on a separate line 69 | dedent_closing_brackets=True 70 | 71 | # Place each dictionary entry onto its own line. 72 | each_dict_entry_on_separate_line=True 73 | 74 | # The regex for an i18n comment. The presence of this comment stops 75 | # reformatting of that line, because the comments are required to be 76 | # next to the string they translate. 77 | i18n_comment= 78 | 79 | # The i18n function call names. The presence of this function stops 80 | # reformattting on that line, because the string it has cannot be moved 81 | # away from the i18n comment. 82 | i18n_function_call= 83 | 84 | # Indent the dictionary value if it cannot fit on the same line as the 85 | # dictionary key. For example: 86 | # 87 | # config = { 88 | # 'key1': 89 | # 'value1', 90 | # 'key2': value1 + 91 | # value2, 92 | # } 93 | indent_dictionary_value=True 94 | 95 | # The number of columns to use for indentation. 96 | indent_width=4 97 | 98 | # Join short lines into one line. E.g., single line 'if' statements. 99 | join_multiple_lines=False 100 | 101 | # Use spaces around default or named assigns. 102 | spaces_around_default_or_named_assign=False 103 | 104 | # Use spaces around the power operator. 105 | spaces_around_power_operator=False 106 | 107 | # The number of spaces required before a trailing comment. 108 | spaces_before_comment=2 109 | 110 | # Insert a space between the ending comma and closing bracket of a list, 111 | # etc. 112 | space_between_ending_comma_and_closing_bracket=False 113 | 114 | # Split before arguments if the argument list is terminated by a 115 | # comma. 116 | split_arguments_when_comma_terminated=True 117 | 118 | # Set to True to prefer splitting before '&', '|' or '^' rather than 119 | # after. 120 | split_before_bitwise_operator=True 121 | 122 | # Split before a dictionary or set generator (comp_for). For example, note 123 | # the split before the 'for': 124 | # 125 | # foo = { 126 | # variable: 'Hello world, have a nice day!' 127 | # for variable in bar if variable != 42 128 | # } 129 | split_before_dict_set_generator=True 130 | 131 | # If an argument / parameter list is going to be split, then split before 132 | # the first argument. 133 | split_before_first_argument=True 134 | 135 | # Set to True to prefer splitting before 'and' or 'or' rather than 136 | # after. 137 | split_before_logical_operator=True 138 | 139 | # Split named assignments onto individual lines. 140 | split_before_named_assigns=True 141 | 142 | # The penalty for splitting right after the opening bracket. 143 | split_penalty_after_opening_bracket=30 144 | 145 | # The penalty for splitting the line after a unary operator. 146 | split_penalty_after_unary_operator=10000 147 | 148 | # The penalty for splitting right before an if expression. 149 | split_penalty_before_if_expr=0 150 | 151 | # The penalty of splitting the line around the '&', '|', and '^' 152 | # operators. 153 | split_penalty_bitwise_operator=300 154 | 155 | # The penalty for characters over the column limit. 156 | split_penalty_excess_character=4500 157 | 158 | # The penalty incurred by adding a line split to the unwrapped line. The 159 | # more line splits added the higher the penalty. 160 | split_penalty_for_added_line_split=30 161 | 162 | # The penalty of splitting a list of "import as" names. For example: 163 | # 164 | # from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, 165 | # long_argument_2, 166 | # long_argument_3) 167 | # 168 | # would reformat to something like: 169 | # 170 | # from a_very_long_or_indented_module_name_yada_yad import ( 171 | # long_argument_1, long_argument_2, long_argument_3) 172 | split_penalty_import_names=0 173 | 174 | # The penalty of splitting the line around the 'and' and 'or' 175 | # operators. 176 | split_penalty_logical_operator=0 177 | 178 | # Use the Tab character for indentation. 179 | use_tabs=False 180 | 181 | -------------------------------------------------------------------------------- /CHEATSHEET.rst: -------------------------------------------------------------------------------- 1 | Tips 2 | ==== 3 | 4 | To run tests 5 | ------------ 6 | 7 | * Install requirements: ``pip install -r test-requirements.txt`` 8 | (possibly in a virtualenv) 9 | 10 | * Actually run the tests: ``pytest sniffio`` 11 | 12 | 13 | To run yapf 14 | ----------- 15 | 16 | * Show what changes yapf wants to make: ``yapf -rpd setup.py 17 | sniffio`` 18 | 19 | * Apply all changes directly to the source tree: ``yapf -rpi setup.py 20 | sniffio`` 21 | 22 | 23 | To make a release 24 | ----------------- 25 | 26 | * Update the version in ``sniffio/_version.py`` 27 | 28 | * Run ``towncrier`` to collect your release notes. 29 | 30 | * Review your release notes. 31 | 32 | * Check everything in. 33 | 34 | * Double-check it all works, docs build, etc. 35 | 36 | * Build your sdist and wheel: ``python setup.py sdist bdist_wheel`` 37 | 38 | * Upload to PyPI: ``twine upload dist/*`` 39 | 40 | * Use ``git tag`` to tag your version. 41 | 42 | * Don't forget to ``git push --tags``. 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | For the Trio code of conduct, see: 2 | https://trio.readthedocs.io/en/latest/code-of-conduct.html 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | For the Trio contributing guide, see: 2 | https://trio.readthedocs.io/en/latest/contributing.html 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is made available under the terms of *either* of the 2 | licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to are 3 | made under the terms of *both* these licenses. 4 | -------------------------------------------------------------------------------- /LICENSE.APACHE2: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHEATSHEET.rst LICENSE* CODE_OF_CONDUCT* CONTRIBUTING* 2 | include .coveragerc .style.yapf 3 | include test-requirements.txt 4 | recursive-include docs * 5 | prune docs/build 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/badge/chat-join%20now-blue.svg 2 | :target: https://gitter.im/python-trio/general 3 | :alt: Join chatroom 4 | 5 | .. image:: https://img.shields.io/badge/docs-read%20now-blue.svg 6 | :target: https://sniffio.readthedocs.io/en/latest/?badge=latest 7 | :alt: Documentation Status 8 | 9 | .. image:: https://img.shields.io/pypi/v/sniffio.svg 10 | :target: https://pypi.org/project/sniffio 11 | :alt: Latest PyPi version 12 | 13 | .. image:: https://img.shields.io/conda/vn/conda-forge/sniffio.svg 14 | :target: https://anaconda.org/conda-forge/sniffio 15 | :alt: Latest conda-forge version 16 | 17 | .. image:: https://travis-ci.org/python-trio/sniffio.svg?branch=master 18 | :target: https://travis-ci.org/python-trio/sniffio 19 | :alt: Automated test status 20 | 21 | .. image:: https://codecov.io/gh/python-trio/sniffio/branch/master/graph/badge.svg 22 | :target: https://codecov.io/gh/python-trio/sniffio 23 | :alt: Test coverage 24 | 25 | ================================================================= 26 | sniffio: Sniff out which async library your code is running under 27 | ================================================================= 28 | 29 | You're writing a library. You've decided to be ambitious, and support 30 | multiple async I/O packages, like `Trio 31 | `__, and `asyncio 32 | `__, and ... You've 33 | written a bunch of clever code to handle all the differences. But... 34 | how do you know *which* piece of clever code to run? 35 | 36 | This is a tiny package whose only purpose is to let you detect which 37 | async library your code is running under. 38 | 39 | * Documentation: https://sniffio.readthedocs.io 40 | 41 | * Bug tracker and source code: https://github.com/python-trio/sniffio 42 | 43 | * License: MIT or Apache License 2.0, your choice 44 | 45 | * Contributor guide: https://trio.readthedocs.io/en/latest/contributing.html 46 | 47 | * Code of conduct: Contributors are requested to follow our `code of 48 | conduct 49 | `_ 50 | in all project spaces. 51 | 52 | This library is maintained by the Trio project, as a service to the 53 | async Python community as a whole. 54 | 55 | 56 | Quickstart 57 | ---------- 58 | 59 | .. code-block:: python3 60 | 61 | from sniffio import current_async_library 62 | import trio 63 | import asyncio 64 | 65 | async def print_library(): 66 | library = current_async_library() 67 | print("This is:", library) 68 | 69 | # Prints "This is trio" 70 | trio.run(print_library) 71 | 72 | # Prints "This is asyncio" 73 | asyncio.run(print_library()) 74 | 75 | For more details, including how to add support to new async libraries, 76 | `please peruse our fine manual `__. 77 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | MYPY_VERSION=0.782 6 | YAPF_VERSION=0.22.0 7 | 8 | 9 | pip install -U pip setuptools wheel 10 | 11 | if [ "$CHECK_FORMATTING" = "1" ]; then 12 | pip install yapf==${YAPF_VERSION} 13 | if ! yapf -rpd sniffio; then 14 | cat <= 1.6.1 3 | sphinx_rtd_theme 4 | sphinxcontrib-trio 5 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = sniffio 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=sniffio 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/sniffio/a1cc1692f5be32645f65ebeb33e6a138180359e0/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 | # Warn about all references to unknown targets 26 | nitpicky = True 27 | # Except for these ones, which we expect to point to unknown targets: 28 | nitpick_ignore = [ 29 | # Format is ("sphinx reference type", "string"), e.g.: 30 | ("py:obj", "bytes-like"), 31 | ] 32 | 33 | # -- General configuration ------------------------------------------------ 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.intersphinx', 45 | 'sphinx.ext.coverage', 46 | 'sphinx.ext.napoleon', 47 | 'sphinxcontrib_trio', 48 | ] 49 | 50 | intersphinx_mapping = { 51 | "python": ('https://docs.python.org/3', None), 52 | "trio": ('https://trio.readthedocs.io/en/stable', None), 53 | } 54 | 55 | autodoc_member_order = "bysource" 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = [] 59 | 60 | # The suffix(es) of source filenames. 61 | # You can specify multiple suffix as a list of string: 62 | # 63 | # source_suffix = ['.rst', '.md'] 64 | source_suffix = '.rst' 65 | 66 | # The master toctree document. 67 | master_doc = 'index' 68 | 69 | # General information about the project. 70 | project = 'sniffio' 71 | copyright = 'The sniffio authors' 72 | author = 'The sniffio authors' 73 | 74 | # The version info for the project you're documenting, acts as replacement for 75 | # |version| and |release|, also used in various other places throughout the 76 | # built documents. 77 | # 78 | # The short X.Y version. 79 | import sniffio 80 | version = sniffio.__version__ 81 | # The full version, including alpha/beta/rc tags. 82 | release = version 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | # 87 | # This is also used if you do content translation via gettext catalogs. 88 | # Usually you set "language" from the command line for these cases. 89 | language = None 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | # This patterns also effect to html_static_path and html_extra_path 94 | exclude_patterns = [] 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # The default language for :: blocks 100 | highlight_language = 'python3' 101 | 102 | # If true, `todo` and `todoList` produce output, else they produce nothing. 103 | todo_include_todos = False 104 | 105 | 106 | # -- Options for HTML output ---------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | # 111 | #html_theme = 'alabaster' 112 | 113 | # We have to set this ourselves, not only because it's useful for local 114 | # testing, but also because if we don't then RTD will throw away our 115 | # html_theme_options. 116 | import sphinx_rtd_theme 117 | html_theme = 'sphinx_rtd_theme' 118 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | # 124 | html_theme_options = { 125 | # default is 2 126 | # show deeper nesting in the RTD theme's sidebar TOC 127 | # https://stackoverflow.com/questions/27669376/ 128 | # I'm not 100% sure this actually does anything with our current 129 | # versions/settings... 130 | "navigation_depth": 4, 131 | "logo_only": True, 132 | } 133 | 134 | # Add any paths that contain custom static files (such as style sheets) here, 135 | # relative to this directory. They are copied after the builtin static files, 136 | # so a file named "default.css" will overwrite the builtin "default.css". 137 | html_static_path = ['_static'] 138 | 139 | 140 | # -- Options for HTMLHelp output ------------------------------------------ 141 | 142 | # Output file base name for HTML help builder. 143 | htmlhelp_basename = 'sniffiodoc' 144 | 145 | 146 | # -- Options for LaTeX output --------------------------------------------- 147 | 148 | latex_elements = { 149 | # The paper size ('letterpaper' or 'a4paper'). 150 | # 151 | # 'papersize': 'letterpaper', 152 | 153 | # The font size ('10pt', '11pt' or '12pt'). 154 | # 155 | # 'pointsize': '10pt', 156 | 157 | # Additional stuff for the LaTeX preamble. 158 | # 159 | # 'preamble': '', 160 | 161 | # Latex figure (float) alignment 162 | # 163 | # 'figure_align': 'htbp', 164 | } 165 | 166 | # Grouping the document tree into LaTeX files. List of tuples 167 | # (source start file, target name, title, 168 | # author, documentclass [howto, manual, or own class]). 169 | latex_documents = [ 170 | (master_doc, 'sniffio.tex', 'Trio Documentation', 171 | author, 'manual'), 172 | ] 173 | 174 | 175 | # -- Options for manual page output --------------------------------------- 176 | 177 | # One entry per manual page. List of tuples 178 | # (source start file, name, description, authors, manual section). 179 | man_pages = [ 180 | (master_doc, 'sniffio', 'sniffio Documentation', 181 | [author], 1) 182 | ] 183 | 184 | 185 | # -- Options for Texinfo output ------------------------------------------- 186 | 187 | # Grouping the document tree into Texinfo files. List of tuples 188 | # (source start file, target name, title, author, 189 | # dir menu entry, description, category) 190 | texinfo_documents = [ 191 | (master_doc, 'sniffio', 'sniffio Documentation', 192 | author, 'sniffio', 'Sniff out which async library your code is running under', 193 | 'Miscellaneous'), 194 | ] 195 | -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | Release history 2 | =============== 3 | 4 | .. currentmodule:: sniffio 5 | 6 | .. towncrier release notes start 7 | 8 | Sniffio 1.3.1 (2024-02-26) 9 | -------------------------- 10 | 11 | Bugfixes 12 | ~~~~~~~~ 13 | 14 | - Added missing ``thread_local`` to ``sniffio.__all__`` (`#44 `__) 15 | 16 | 17 | sniffio 1.3.0 (2022-09-01) 18 | -------------------------- 19 | 20 | Features 21 | ~~~~~~~~ 22 | 23 | - Add support for Python 3.9 and 3.10. (`#29 `__) 24 | - Provide ``sniffio.thread_local.name`` for coroutine libraries to set (`#23 `__) 25 | 26 | 27 | Deprecations and Removals 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | - Drop support for Python 3.5 and 3.6. (`#29 `__) 31 | 32 | 33 | sniffio 1.2.0 (2020-10-11) 34 | -------------------------- 35 | 36 | Features 37 | ~~~~~~~~ 38 | 39 | - Include type hints (`#17 `__) 40 | 41 | 42 | sniffio 1.1.0 (2019-04-19) 43 | -------------------------- 44 | 45 | Features 46 | ~~~~~~~~ 47 | 48 | - Sniff for curio. (`#5 `__) 49 | 50 | 51 | sniffio 1.0.0 (2018-07-31) 52 | -------------------------- 53 | 54 | Initial release. 55 | -------------------------------------------------------------------------------- /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 | sniffio: Sniff out which async library your code is running under 9 | ================================================================= 10 | 11 | You're writing a library. You've decided to be ambitious, and support 12 | multiple async I/O packages, like `Trio 13 | `__, and `asyncio 14 | `__, and ... You've 15 | written a bunch of clever code to handle all the differences. But... 16 | how do you know *which* piece of clever code to run? 17 | 18 | This is a tiny package whose only purpose is to let you detect which 19 | async library your code is running under. 20 | 21 | * Documentation: https://sniffio.readthedocs.io 22 | 23 | * Bug tracker and source code: https://github.com/python-trio/sniffio 24 | 25 | * License: MIT or Apache License 2.0, your choice 26 | 27 | * Contributor guide: https://trio.readthedocs.io/en/latest/contributing.html 28 | 29 | * Code of conduct: Contributors are requested to follow our `code of 30 | conduct 31 | `_ 32 | in all project spaces. 33 | 34 | This library is maintained by the Trio project, as a service to the 35 | async Python community as a whole. 36 | 37 | 38 | .. module:: sniffio 39 | 40 | Usage 41 | ===== 42 | 43 | .. autofunction:: current_async_library 44 | 45 | .. autoexception:: AsyncLibraryNotFoundError 46 | 47 | 48 | Adding support to a new async library 49 | ===================================== 50 | 51 | If you'd like your library to be detected by ``sniffio``, it's pretty 52 | easy. 53 | 54 | **Step 1:** Pick the magic string that will identify your library. To 55 | avoid collisions, this should match your library's PEP 503 normalized name on PyPI. 56 | 57 | **Step 2:** There's a special :class:`threading.local` object: 58 | 59 | .. data:: thread_local.name 60 | 61 | Make sure that whenever your library is calling a coroutine ``throw()``, ``send()``, or ``close()`` 62 | that this is set to your identifier string. In most cases, this will be as simple as: 63 | 64 | .. code-block:: python3 65 | 66 | from sniffio import thread_local 67 | 68 | # Your library's step function 69 | def step(...): 70 | old_name, thread_local.name = thread_local.name, "my-library's-PyPI-name" 71 | try: 72 | result = coro.send(None) 73 | finally: 74 | thread_local.name = old_name 75 | 76 | **Step 3:** Send us a PR to add your library to the list of supported 77 | libraries above. 78 | 79 | That's it! 80 | 81 | There are libraries that directly drive a sniffio-naive coroutine from another, 82 | outer sniffio-aware coroutine such as `trio_asyncio`. 83 | These libraries should make sure to set the correct value 84 | while calling a synchronous function that will go on to drive the 85 | sniffio-naive coroutine. 86 | 87 | 88 | .. code-block:: python3 89 | 90 | from sniffio import thread_local 91 | 92 | # Your library's compatibility loop 93 | async def main_loop(self, ...) -> None: 94 | ... 95 | handle: asyncio.Handle = await self.get_next_handle() 96 | old_name, thread_local.name = thread_local.name, "asyncio" 97 | try: 98 | result = handle._callback(obj._args) 99 | finally: 100 | thread_local.name = old_name 101 | 102 | 103 | .. toctree:: 104 | :maxdepth: 1 105 | 106 | history.rst 107 | 108 | ==================== 109 | Indices and tables 110 | ==================== 111 | 112 | * :ref:`genindex` 113 | * :ref:`modindex` 114 | * :ref:`search` 115 | * :ref:`glossary` 116 | -------------------------------------------------------------------------------- /newsfragments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/sniffio/a1cc1692f5be32645f65ebeb33e6a138180359e0/newsfragments/.gitkeep -------------------------------------------------------------------------------- /newsfragments/README.rst: -------------------------------------------------------------------------------- 1 | Adding newsfragments 2 | ==================== 3 | 4 | This directory collects "newsfragments": short files that each contain 5 | a snippet of ReST-formatted text that will be added to the next 6 | release notes. This should be a description of aspects of the change 7 | (if any) that are relevant to users. (This contrasts with your commit 8 | message and PR description, which are a description of the change as 9 | relevant to people working on the code itself.) 10 | 11 | Each file should be named like ``..rst``, where 12 | ```` is an issue numbers, and ```` is one of: 13 | 14 | * ``feature`` 15 | * ``bugfix`` 16 | * ``doc`` 17 | * ``removal`` 18 | * ``misc`` 19 | 20 | So for example: ``123.feature.rst``, ``456.bugfix.rst`` 21 | 22 | If your PR fixes an issue, use that number here. If there is no issue, 23 | then after you submit the PR and get the PR number you can add a 24 | newsfragment using that instead. 25 | 26 | Note that the ``towncrier`` tool will automatically 27 | reflow your text, so don't try to do any fancy formatting. You can 28 | install ``towncrier`` and then run ``towncrier --draft`` if you want 29 | to get a preview of how your change will look in the final release 30 | notes. 31 | 32 | 33 | Making releases 34 | =============== 35 | 36 | ``pip install towncrier``, then run ``towncrier``. (You can use 37 | ``towncrier --draft`` to get a preview of what this will do.) 38 | 39 | You can configure ``towncrier`` (for example: customizing the 40 | different types of changes) by modifying ``pyproject.toml``. 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 64", 4 | "setuptools_scm >= 6.4" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "sniffio" 10 | description = "Sniff out which async library your code is running under" 11 | readme = "README.rst" 12 | authors = [{name = "Nathaniel J. Smith", email = "njs@pobox.com"}] 13 | license = {text = "MIT OR Apache-2.0"} 14 | keywords = ["async", "trio", "asyncio"] 15 | classifiers = [ 16 | "License :: OSI Approved :: MIT License", 17 | "License :: OSI Approved :: Apache Software License", 18 | "Framework :: Trio", 19 | "Framework :: AsyncIO", 20 | "Operating System :: POSIX :: Linux", 21 | "Operating System :: MacOS :: MacOS X", 22 | "Operating System :: Microsoft :: Windows", 23 | "Programming Language :: Python :: 3 :: Only", 24 | "Programming Language :: Python :: Implementation :: CPython", 25 | "Programming Language :: Python :: Implementation :: PyPy", 26 | "Intended Audience :: Developers", 27 | "Development Status :: 5 - Production/Stable", 28 | ] 29 | requires-python = ">= 3.7" 30 | dynamic = ["version"] 31 | 32 | [project.urls] 33 | Homepage = "https://github.com/python-trio/sniffio" 34 | Documentation = "https://sniffio.readthedocs.io/" 35 | Changelog = "https://sniffio.readthedocs.io/en/latest/history.html" 36 | 37 | [tool.setuptools.dynamic] 38 | version = {attr = "sniffio._version.__version__"} 39 | 40 | [tool.setuptools.packages.find] 41 | include = ["sniffio*"] 42 | namespaces = false 43 | 44 | [tool.towncrier] 45 | package = "sniffio" 46 | filename = "docs/source/history.rst" 47 | directory = "newsfragments" 48 | underlines = ["-", "~", "^"] 49 | issue_format = "`#{issue} `__" 50 | -------------------------------------------------------------------------------- /sniffio/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for sniffio.""" 2 | 3 | __all__ = [ 4 | "current_async_library", 5 | "AsyncLibraryNotFoundError", 6 | "current_async_library_cvar", 7 | "thread_local", 8 | ] 9 | 10 | from ._version import __version__ 11 | 12 | from ._impl import ( 13 | current_async_library, 14 | AsyncLibraryNotFoundError, 15 | current_async_library_cvar, 16 | thread_local, 17 | ) 18 | -------------------------------------------------------------------------------- /sniffio/_impl.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | from typing import Optional 3 | import sys 4 | import threading 5 | 6 | current_async_library_cvar = ContextVar( 7 | "current_async_library_cvar", default=None 8 | ) # type: ContextVar[Optional[str]] 9 | 10 | 11 | class _ThreadLocal(threading.local): 12 | # Since threading.local provides no explicit mechanism is for setting 13 | # a default for a value, a custom class with a class attribute is used 14 | # instead. 15 | name = None # type: Optional[str] 16 | 17 | 18 | thread_local = _ThreadLocal() 19 | 20 | 21 | class AsyncLibraryNotFoundError(RuntimeError): 22 | pass 23 | 24 | 25 | def current_async_library() -> str: 26 | """Detect which async library is currently running. 27 | 28 | The following libraries are currently supported: 29 | 30 | ================ =========== ============================ 31 | Library Requires Magic string 32 | ================ =========== ============================ 33 | **Trio** Trio v0.6+ ``"trio"`` 34 | **Curio** - ``"curio"`` 35 | **asyncio** ``"asyncio"`` 36 | **Trio-asyncio** v0.8.2+ ``"trio"`` or ``"asyncio"``, 37 | depending on current mode 38 | ================ =========== ============================ 39 | 40 | Returns: 41 | A string like ``"trio"``. 42 | 43 | Raises: 44 | AsyncLibraryNotFoundError: if called from synchronous context, 45 | or if the current async library was not recognized. 46 | 47 | Examples: 48 | 49 | .. code-block:: python3 50 | 51 | from sniffio import current_async_library 52 | 53 | async def generic_sleep(seconds): 54 | library = current_async_library() 55 | if library == "trio": 56 | import trio 57 | await trio.sleep(seconds) 58 | elif library == "asyncio": 59 | import asyncio 60 | await asyncio.sleep(seconds) 61 | # ... and so on ... 62 | else: 63 | raise RuntimeError(f"Unsupported library {library!r}") 64 | 65 | """ 66 | value = thread_local.name 67 | if value is not None: 68 | return value 69 | 70 | value = current_async_library_cvar.get() 71 | if value is not None: 72 | return value 73 | 74 | # Need to sniff for asyncio 75 | if "asyncio" in sys.modules: 76 | import asyncio 77 | try: 78 | current_task = asyncio.current_task # type: ignore[attr-defined] 79 | except AttributeError: 80 | current_task = asyncio.Task.current_task # type: ignore[attr-defined] 81 | try: 82 | if current_task() is not None: 83 | return "asyncio" 84 | except RuntimeError: 85 | pass 86 | 87 | # Sniff for curio (for now) 88 | if 'curio' in sys.modules: 89 | from curio.meta import curio_running 90 | if curio_running(): 91 | return 'curio' 92 | 93 | raise AsyncLibraryNotFoundError( 94 | "unknown async library, or not in async context" 95 | ) 96 | -------------------------------------------------------------------------------- /sniffio/_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/sniffio/a1cc1692f5be32645f65ebeb33e6a138180359e0/sniffio/_tests/__init__.py -------------------------------------------------------------------------------- /sniffio/_tests/test_sniffio.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | 6 | from .. import ( 7 | current_async_library, AsyncLibraryNotFoundError, 8 | current_async_library_cvar, thread_local 9 | ) 10 | 11 | 12 | def test_basics_cvar(): 13 | with pytest.raises(AsyncLibraryNotFoundError): 14 | current_async_library() 15 | 16 | token = current_async_library_cvar.set("generic-lib") 17 | try: 18 | assert current_async_library() == "generic-lib" 19 | finally: 20 | current_async_library_cvar.reset(token) 21 | 22 | with pytest.raises(AsyncLibraryNotFoundError): 23 | current_async_library() 24 | 25 | 26 | def test_basics_tlocal(): 27 | with pytest.raises(AsyncLibraryNotFoundError): 28 | current_async_library() 29 | 30 | old_name, thread_local.name = thread_local.name, "generic-lib" 31 | try: 32 | assert current_async_library() == "generic-lib" 33 | finally: 34 | thread_local.name = old_name 35 | 36 | with pytest.raises(AsyncLibraryNotFoundError): 37 | current_async_library() 38 | 39 | 40 | def test_asyncio(): 41 | import asyncio 42 | 43 | with pytest.raises(AsyncLibraryNotFoundError): 44 | current_async_library() 45 | 46 | ran = [] 47 | 48 | async def this_is_asyncio(): 49 | assert current_async_library() == "asyncio" 50 | # Call it a second time to exercise the caching logic 51 | assert current_async_library() == "asyncio" 52 | ran.append(True) 53 | 54 | asyncio.run(this_is_asyncio()) 55 | assert ran == [True] 56 | 57 | with pytest.raises(AsyncLibraryNotFoundError): 58 | current_async_library() 59 | 60 | 61 | @pytest.mark.skipif( 62 | sys.version_info >= (3, 12), 63 | reason= 64 | "curio broken on 3.12 (https://github.com/python-trio/sniffio/pull/42)", 65 | ) 66 | def test_curio(): 67 | import curio 68 | 69 | with pytest.raises(AsyncLibraryNotFoundError): 70 | current_async_library() 71 | 72 | ran = [] 73 | 74 | async def this_is_curio(): 75 | assert current_async_library() == "curio" 76 | # Call it a second time to exercise the caching logic 77 | assert current_async_library() == "curio" 78 | ran.append(True) 79 | 80 | curio.run(this_is_curio) 81 | assert ran == [True] 82 | 83 | with pytest.raises(AsyncLibraryNotFoundError): 84 | current_async_library() 85 | -------------------------------------------------------------------------------- /sniffio/_version.py: -------------------------------------------------------------------------------- 1 | # This file is imported from __init__.py and exec'd from setup.py 2 | 3 | __version__ = "1.3.1+dev" 4 | -------------------------------------------------------------------------------- /sniffio/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/sniffio/a1cc1692f5be32645f65ebeb33e6a138180359e0/sniffio/py.typed -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | curio 4 | --------------------------------------------------------------------------------