├── .appveyor.yml ├── .coveragerc ├── .gitignore ├── .readthedocs.yml ├── .style.yapf ├── .travis.yml ├── CHEATSHEET.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.APACHE2 ├── LICENSE.MIT ├── MANIFEST.in ├── README.rst ├── ci ├── rtd-requirements.txt └── travis.sh ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── .gitkeep │ ├── conf.py │ ├── history.rst │ └── index.rst ├── exceptiongroup ├── __init__.py ├── _monkeypatch.py ├── _tests │ ├── __init__.py │ ├── test_exceptiongroup.py │ ├── test_scripts │ │ ├── __init__.py │ │ ├── _common.py │ │ ├── custom_excepthook.py │ │ ├── ipython_custom_exc.py │ │ ├── simple_excepthook.py │ │ └── simple_excepthook_IPython.py │ └── test_tools.py ├── _tools.py └── _version.py ├── newsfragments ├── .gitkeep └── README.rst ├── pyproject.toml ├── setup.py └── test-requirements.txt /.appveyor.yml: -------------------------------------------------------------------------------- 1 | skip_tags: true 2 | 3 | os: Visual Studio 2015 4 | 5 | environment: 6 | matrix: 7 | - PYTHON: "C:\\Python35" 8 | - PYTHON: "C:\\Python35-x64" 9 | - PYTHON: "C:\\Python36" 10 | - PYTHON: "C:\\Python36-x64" 11 | - PYTHON: "C:\\Python37" 12 | - PYTHON: "C:\\Python37-x64" 13 | 14 | build_script: 15 | - "git --no-pager log -n2" 16 | - "echo %APPVEYOR_REPO_COMMIT%" 17 | - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;;%PATH%" 18 | - "python --version" 19 | - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" 20 | - "pip install ." 21 | - "pip install -Ur test-requirements.txt" 22 | - "pip install codecov" 23 | 24 | test_script: 25 | - "mkdir empty" 26 | - "cd empty" 27 | # Make sure it's being imported from where we expect 28 | - "python -c \"import os, exceptiongroup; print(os.path.dirname(exceptiongroup.__file__))\"" 29 | - "python -u -m pytest -W error -ra -v -s --pyargs exceptiongroup --cov=exceptiongroup --cov-config=../.coveragerc" 30 | - "codecov" 31 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch=True 3 | source=exceptiongroup 4 | 5 | [report] 6 | precision = 1 7 | exclude_lines = 8 | pragma: no cover 9 | abc.abstractmethod 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/latest/yaml-config.html 2 | formats: 3 | - htmlzip 4 | - epub 5 | 6 | requirements_file: ci/rtd-requirements.txt 7 | 8 | # Currently RTD's default image only has 3.5 9 | # This gets us 3.6 (and hopefully 3.7 in the future) 10 | # https://docs.readthedocs.io/en/latest/yaml-config.html#build-image 11 | build: 12 | image: latest 13 | 14 | python: 15 | version: 3 16 | pip_install: True 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | dist: trusty 4 | 5 | matrix: 6 | include: 7 | # These are quick and often catch errors, so list them first 8 | - python: 3.6 9 | env: CHECK_DOCS=1 10 | - python: 3.6 11 | env: CHECK_FORMATTING=1 12 | # The pypy tests are slow, so list them early 13 | - python: pypy3.5 14 | # Uncomment if you want to test on pypy nightly: 15 | # - language: generic 16 | # env: USE_PYPY_NIGHTLY=1 17 | - python: 3.5.0 18 | - python: 3.5.2 19 | - python: 3.6 20 | # As of 2018-07-05, Travis's 3.7 and 3.8 builds only work if you 21 | # use dist: xenial AND sudo: required 22 | # See: https://github.com/python-trio/trio/pull/556#issuecomment-402879391 23 | - python: 3.7 24 | dist: xenial 25 | sudo: required 26 | - python: 3.8-dev 27 | dist: xenial 28 | sudo: required 29 | - os: osx 30 | language: generic 31 | env: MACPYTHON=3.5.4 32 | - os: osx 33 | language: generic 34 | env: MACPYTHON=3.6.6 35 | - os: osx 36 | language: generic 37 | env: MACPYTHON=3.7.0 38 | 39 | script: 40 | - ci/travis.sh 41 | -------------------------------------------------------------------------------- /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 exceptiongroup`` 11 | 12 | 13 | To run yapf 14 | ----------- 15 | 16 | * Show what changes yapf wants to make: ``yapf -rpd setup.py 17 | exceptiongroup`` 18 | 19 | * Apply all changes directly to the source tree: ``yapf -rpi setup.py 20 | exceptiongroup`` 21 | 22 | 23 | To make a release 24 | ----------------- 25 | 26 | * Update the version in ``exceptiongroup/_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 | exceptiongroup 2 | ============== 3 | 4 | Welcome to `exceptiongroup `__! 5 | 6 | A way to represent multiple things going wrong at the same time, in 7 | Python. 8 | 9 | This project is currently maintained by the `Trio project 10 | `__, but the goal is for some version of 11 | it to be merged into Python 3.8, so it can be used in both Trio and 12 | asyncio. For more information, see: 13 | https://github.com/python-trio/trio/issues/611 14 | 15 | For a more recent effort, see: 16 | https://github.com/python/exceptiongroups 17 | 18 | License: Your choice of MIT or Apache License 2.0 19 | -------------------------------------------------------------------------------- /ci/rtd-requirements.txt: -------------------------------------------------------------------------------- 1 | # RTD is currently installing 1.5.3, which has a bug in :lineno-match: 2 | sphinx >= 1.6.1 3 | sphinx_rtd_theme 4 | sphinxcontrib-trio 5 | -------------------------------------------------------------------------------- /ci/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | if [ "$TRAVIS_OS_NAME" = "osx" ]; then 6 | curl -Lo macpython.pkg https://www.python.org/ftp/python/${MACPYTHON}/python-${MACPYTHON}-macosx10.6.pkg 7 | sudo installer -pkg macpython.pkg -target / 8 | ls /Library/Frameworks/Python.framework/Versions/*/bin/ 9 | PYTHON_EXE=/Library/Frameworks/Python.framework/Versions/*/bin/python3 10 | # The pip in older MacPython releases doesn't support a new enough TLS 11 | curl https://bootstrap.pypa.io/get-pip.py | sudo $PYTHON_EXE 12 | sudo $PYTHON_EXE -m pip install virtualenv 13 | $PYTHON_EXE -m virtualenv testenv 14 | source testenv/bin/activate 15 | fi 16 | 17 | if [ "$USE_PYPY_NIGHTLY" = "1" ]; then 18 | curl -fLo pypy.tar.bz2 http://buildbot.pypy.org/nightly/py3.5/pypy-c-jit-latest-linux64.tar.bz2 19 | if [ ! -s pypy.tar.bz2 ]; then 20 | # We know: 21 | # - curl succeeded (200 response code; -f means "exit with error if 22 | # server returns 4xx or 5xx") 23 | # - nonetheless, pypy.tar.bz2 does not exist, or contains no data 24 | # This isn't going to work, and the failure is not informative of 25 | # anything involving this package. 26 | ls -l 27 | echo "PyPy3 nightly build failed to download – something is wrong on their end." 28 | echo "Skipping testing against the nightly build for right now." 29 | exit 0 30 | fi 31 | tar xaf pypy.tar.bz2 32 | # something like "pypy-c-jit-89963-748aa3022295-linux64" 33 | PYPY_DIR=$(echo pypy-c-jit-*) 34 | PYTHON_EXE=$PYPY_DIR/bin/pypy3 35 | ($PYTHON_EXE -m ensurepip \ 36 | && $PYTHON_EXE -m pip install virtualenv \ 37 | && $PYTHON_EXE -m virtualenv testenv) \ 38 | || (echo "pypy nightly is broken; skipping tests"; exit 0) 39 | source testenv/bin/activate 40 | fi 41 | 42 | if [ "$USE_PYPY_RELEASE_VERSION" != "" ]; then 43 | curl -fLo pypy.tar.bz2 https://bitbucket.org/squeaky/portable-pypy/downloads/pypy3.5-${USE_PYPY_RELEASE_VERSION}-linux_x86_64-portable.tar.bz2 44 | tar xaf pypy.tar.bz2 45 | # something like "pypy3.5-5.7.1-beta-linux_x86_64-portable" 46 | PYPY_DIR=$(echo pypy3.5-*) 47 | PYTHON_EXE=$PYPY_DIR/bin/pypy3 48 | $PYTHON_EXE -m ensurepip 49 | $PYTHON_EXE -m pip install virtualenv 50 | $PYTHON_EXE -m virtualenv testenv 51 | source testenv/bin/activate 52 | fi 53 | 54 | pip install -U pip setuptools wheel 55 | 56 | if [ "$CHECK_FORMATTING" = "1" ]; then 57 | pip install black 58 | if ! black --check --diff setup.py exceptiongroup; then 59 | cat <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/exceptiongroup/dd8fe794d54f058c3bf06f4b06f2f5723879209e/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 = 'exceptiongroup' 71 | copyright = 'The exceptiongroup authors' 72 | author = 'The exceptiongroup 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 exceptiongroup 80 | version = exceptiongroup.__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 = 'exceptiongroupdoc' 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, 'exceptiongroup.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, 'exceptiongroup', 'exceptiongroup 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, 'exceptiongroup', 'exceptiongroup Documentation', 192 | author, 'exceptiongroup', 'A way to represent multiple things going wrong at the same time, in Python', 193 | 'Miscellaneous'), 194 | ] 195 | -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | Release history 2 | =============== 3 | 4 | .. currentmodule:: exceptiongroup 5 | 6 | .. towncrier release notes start 7 | -------------------------------------------------------------------------------- /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 | exceptiongroup: A way to represent multiple things going wrong at the same time, in Python 9 | ========================================================================================== 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | history.rst 15 | 16 | ==================== 17 | Indices and tables 18 | ==================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | * :ref:`glossary` 24 | -------------------------------------------------------------------------------- /exceptiongroup/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for exceptiongroup.""" 2 | 3 | from ._version import __version__ 4 | 5 | __all__ = ["ExceptionGroup", "split", "catch"] 6 | 7 | 8 | class ExceptionGroup(BaseException): 9 | """An exception that contains other exceptions. 10 | 11 | Its main use is to represent the situation when multiple child tasks all 12 | raise errors "in parallel". 13 | 14 | Args: 15 | message (str): A description of the overall exception. 16 | exceptions (list): The exceptions. 17 | sources (list): For each exception, a string describing where it came 18 | from. 19 | 20 | Raises: 21 | TypeError: if any of the passed in objects are not instances of 22 | :exc:`BaseException`. 23 | ValueError: if the exceptions and sources lists don't have the same 24 | length. 25 | 26 | """ 27 | 28 | def __init__(self, message, exceptions, sources): 29 | super().__init__(message, exceptions, sources) 30 | self.exceptions = list(exceptions) 31 | for exc in self.exceptions: 32 | if not isinstance(exc, BaseException): 33 | raise TypeError( 34 | "Expected an exception object, not {!r}".format(exc) 35 | ) 36 | self.message = message 37 | self.sources = list(sources) 38 | if len(self.sources) != len(self.exceptions): 39 | raise ValueError( 40 | "different number of sources ({}) and exceptions ({})".format( 41 | len(self.sources), len(self.exceptions) 42 | ) 43 | ) 44 | 45 | # copy.copy doesn't work for ExceptionGroup, because BaseException have 46 | # rewrite __reduce_ex__ method. We need to add __copy__ method to 47 | # make it can be copied. 48 | def __copy__(self): 49 | new_group = self.__class__(self.message, self.exceptions, self.sources) 50 | new_group.__traceback__ = self.__traceback__ 51 | new_group.__context__ = self.__context__ 52 | new_group.__cause__ = self.__cause__ 53 | # Setting __cause__ also implicitly sets the __suppress_context__ 54 | # attribute to True. So we should copy __suppress_context__ attribute 55 | # last, after copying __cause__. 56 | new_group.__suppress_context__ = self.__suppress_context__ 57 | return new_group 58 | 59 | def __str__(self): 60 | return ", ".join(repr(exc) for exc in self.exceptions) 61 | 62 | def __repr__(self): 63 | return "".format(self) 64 | 65 | 66 | from . import _monkeypatch 67 | from ._tools import split, catch 68 | -------------------------------------------------------------------------------- /exceptiongroup/_monkeypatch.py: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # ExceptionGroup traceback formatting 3 | # 4 | # This file contains the terrible, terrible monkey patching of various things, 5 | # especially the traceback module, to add support for handling 6 | # ExceptionGroups. 7 | ################################################################ 8 | 9 | import sys 10 | import textwrap 11 | import traceback 12 | import warnings 13 | 14 | from . import ExceptionGroup 15 | 16 | traceback_exception_original_init = traceback.TracebackException.__init__ 17 | 18 | 19 | def traceback_exception_init( 20 | self, 21 | exc_type, 22 | exc_value, 23 | exc_traceback, 24 | *, 25 | limit=None, 26 | lookup_lines=True, 27 | capture_locals=False, 28 | _seen=None 29 | ): 30 | if _seen is None: 31 | _seen = set() 32 | 33 | # Capture the original exception and its cause and context as 34 | # TracebackExceptions 35 | traceback_exception_original_init( 36 | self, 37 | exc_type, 38 | exc_value, 39 | exc_traceback, 40 | limit=limit, 41 | lookup_lines=lookup_lines, 42 | capture_locals=capture_locals, 43 | _seen=_seen, 44 | ) 45 | 46 | # Capture each of the exceptions in the ExceptionGroup along with each of 47 | # their causes and contexts 48 | if isinstance(exc_value, ExceptionGroup): 49 | exceptions = [] 50 | sources = [] 51 | for exc, source in zip(exc_value.exceptions, exc_value.sources): 52 | if exc not in _seen: 53 | exceptions.append( 54 | traceback.TracebackException.from_exception( 55 | exc, 56 | limit=limit, 57 | lookup_lines=lookup_lines, 58 | capture_locals=capture_locals, 59 | # copy the set of _seen exceptions so that duplicates 60 | # shared between sub-exceptions are not omitted 61 | _seen=set(_seen), 62 | ) 63 | ) 64 | sources.append(source) 65 | self.exceptions = exceptions 66 | self.sources = sources 67 | else: 68 | self.exceptions = [] 69 | self.sources = [] 70 | 71 | 72 | def traceback_exception_format(self, *, chain=True): 73 | yield from traceback_exception_original_format(self, chain=chain) 74 | 75 | for exc, source in zip(self.exceptions, self.sources): 76 | yield "\n {}:\n\n".format(source) 77 | yield from ( 78 | textwrap.indent(line, " " * 4) for line in exc.format(chain=chain) 79 | ) 80 | 81 | 82 | def exceptiongroup_excepthook(etype, value, tb): 83 | sys.stderr.write("".join(traceback.format_exception(etype, value, tb))) 84 | 85 | 86 | traceback.TracebackException.__init__ = traceback_exception_init 87 | traceback_exception_original_format = traceback.TracebackException.format 88 | traceback.TracebackException.format = traceback_exception_format 89 | 90 | IPython_handler_installed = False 91 | warning_given = False 92 | if "IPython" in sys.modules: 93 | import IPython 94 | 95 | ip = IPython.get_ipython() 96 | if ip is not None: 97 | if ip.custom_exceptions != (): 98 | warnings.warn( 99 | "IPython detected, but you already have a custom exception " 100 | "handler installed. I'll skip installing exceptiongroup's " 101 | "custom handler, but this means you won't see full tracebacks " 102 | "for ExceptionGroups.", 103 | category=RuntimeWarning, 104 | ) 105 | warning_given = True 106 | else: 107 | 108 | def trio_show_traceback(self, etype, value, tb, tb_offset=None): 109 | # XX it would be better to integrate with IPython's fancy 110 | # exception formatting stuff (and not ignore tb_offset) 111 | exceptiongroup_excepthook(etype, value, tb) 112 | 113 | ip.set_custom_exc((ExceptionGroup,), trio_show_traceback) 114 | IPython_handler_installed = True 115 | 116 | if sys.excepthook is sys.__excepthook__: 117 | sys.excepthook = exceptiongroup_excepthook 118 | else: 119 | if not IPython_handler_installed and not warning_given: 120 | warnings.warn( 121 | "You seem to already have a custom sys.excepthook handler " 122 | "installed. I'll skip installing exceptiongroup's custom handler, " 123 | "but this means you won't see full tracebacks for " 124 | "ExceptionGroups.", 125 | category=RuntimeWarning, 126 | ) 127 | -------------------------------------------------------------------------------- /exceptiongroup/_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/exceptiongroup/dd8fe794d54f058c3bf06f4b06f2f5723879209e/exceptiongroup/_tests/__init__.py -------------------------------------------------------------------------------- /exceptiongroup/_tests/test_exceptiongroup.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import pytest 3 | 4 | from exceptiongroup import ExceptionGroup 5 | 6 | 7 | def raise_group(): 8 | try: 9 | 1 / 0 10 | except Exception as e: 11 | raise ExceptionGroup("ManyError", [e], [str(e)]) from e 12 | 13 | 14 | def test_exception_group_init(): 15 | memberA = ValueError("A") 16 | memberB = RuntimeError("B") 17 | group = ExceptionGroup( 18 | "many error.", [memberA, memberB], [str(memberA), str(memberB)] 19 | ) 20 | assert group.exceptions == [memberA, memberB] 21 | assert group.message == "many error." 22 | assert group.sources == [str(memberA), str(memberB)] 23 | assert group.args == ( 24 | "many error.", 25 | [memberA, memberB], 26 | [str(memberA), str(memberB)], 27 | ) 28 | 29 | 30 | def test_exception_group_when_members_are_not_exceptions(): 31 | with pytest.raises(TypeError): 32 | ExceptionGroup( 33 | "error", 34 | [RuntimeError("RuntimeError"), "error2"], 35 | ["RuntimeError", "error2"], 36 | ) 37 | 38 | 39 | def test_exception_group_init_when_exceptions_messages_not_equal(): 40 | with pytest.raises(ValueError): 41 | ExceptionGroup( 42 | "many error.", [ValueError("A"), RuntimeError("B")], ["A"] 43 | ) 44 | 45 | 46 | def test_exception_group_str(): 47 | memberA = ValueError("memberA") 48 | memberB = ValueError("memberB") 49 | group = ExceptionGroup( 50 | "many error.", [memberA, memberB], [str(memberA), str(memberB)] 51 | ) 52 | assert "memberA" in str(group) 53 | assert "memberB" in str(group) 54 | 55 | assert "ExceptionGroup: " in repr(group) 56 | assert "memberA" in repr(group) 57 | assert "memberB" in repr(group) 58 | 59 | 60 | def test_exception_group_copy(): 61 | try: 62 | raise_group() # the exception is raise by `raise...from..` 63 | except ExceptionGroup as e: 64 | group = e 65 | 66 | another_group = copy.copy(group) 67 | assert another_group.message == group.message 68 | assert another_group.exceptions == group.exceptions 69 | assert another_group.sources == group.sources 70 | assert another_group.__traceback__ is group.__traceback__ 71 | assert another_group.__cause__ is group.__cause__ 72 | assert another_group.__context__ is group.__context__ 73 | assert another_group.__suppress_context__ is group.__suppress_context__ 74 | assert another_group.__cause__ is not None 75 | assert another_group.__context__ is not None 76 | assert another_group.__suppress_context__ is True 77 | 78 | # doing copy when __suppress_context__ is False 79 | group.__suppress_context__ = False 80 | another_group = copy.copy(group) 81 | assert another_group.__cause__ is group.__cause__ 82 | assert another_group.__context__ is group.__context__ 83 | assert another_group.__suppress_context__ is group.__suppress_context__ 84 | assert another_group.__suppress_context__ is False 85 | -------------------------------------------------------------------------------- /exceptiongroup/_tests/test_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # This isn't really a package, everything in here is a standalone script. This 2 | # __init__.py is just to fool setup.py into actually installing the things. 3 | -------------------------------------------------------------------------------- /exceptiongroup/_tests/test_scripts/_common.py: -------------------------------------------------------------------------------- 1 | # https://coverage.readthedocs.io/en/latest/subprocess.html 2 | try: 3 | import coverage 4 | except ImportError: # pragma: no cover 5 | pass 6 | else: 7 | coverage.process_startup() 8 | -------------------------------------------------------------------------------- /exceptiongroup/_tests/test_scripts/custom_excepthook.py: -------------------------------------------------------------------------------- 1 | import _common 2 | 3 | import sys 4 | 5 | 6 | def custom_excepthook(*args): 7 | print("custom running!") 8 | return sys.__excepthook__(*args) 9 | 10 | 11 | sys.excepthook = custom_excepthook 12 | 13 | # Should warn that we'll get kinda-broken tracebacks 14 | import exceptiongroup 15 | 16 | # The custom excepthook should run, because we were polite and didn't 17 | # override it 18 | raise exceptiongroup.ExceptionGroup( 19 | "demo", [ValueError(), KeyError()], ["a", "b"] 20 | ) 21 | -------------------------------------------------------------------------------- /exceptiongroup/_tests/test_scripts/ipython_custom_exc.py: -------------------------------------------------------------------------------- 1 | import _common 2 | 3 | # Override the regular excepthook too -- it doesn't change anything either way 4 | # because ipython doesn't use it, but we want to make sure exceptiongroup 5 | # doesn't warn about it. 6 | import sys 7 | 8 | 9 | def custom_excepthook(*args): 10 | print("custom running!") 11 | return sys.__excepthook__(*args) 12 | 13 | 14 | sys.excepthook = custom_excepthook 15 | 16 | import IPython 17 | 18 | ip = IPython.get_ipython() 19 | 20 | 21 | # Set this to some random nonsense 22 | class SomeError(Exception): 23 | pass 24 | 25 | 26 | def custom_exc_hook(etype, value, tb, tb_offset=None): 27 | ip.showtraceback() 28 | 29 | 30 | ip.set_custom_exc((SomeError,), custom_exc_hook) 31 | 32 | import exceptiongroup 33 | 34 | # The custom excepthook should run, because we were polite and didn't 35 | # override it 36 | raise exceptiongroup.ExceptionGroup( 37 | "demo", [ValueError(), KeyError()], ["a", "b"] 38 | ) 39 | -------------------------------------------------------------------------------- /exceptiongroup/_tests/test_scripts/simple_excepthook.py: -------------------------------------------------------------------------------- 1 | import _common 2 | 3 | import exceptiongroup 4 | 5 | 6 | def exc1_fn(): 7 | try: 8 | raise ValueError 9 | except Exception as exc: 10 | return exc 11 | 12 | 13 | def exc2_fn(): 14 | try: 15 | raise KeyError 16 | except Exception as exc: 17 | return exc 18 | 19 | 20 | # This should be printed nicely, because we overrode sys.excepthook 21 | raise exceptiongroup.ExceptionGroup("demo", [exc1_fn(), exc2_fn()], ["a", "b"]) 22 | -------------------------------------------------------------------------------- /exceptiongroup/_tests/test_scripts/simple_excepthook_IPython.py: -------------------------------------------------------------------------------- 1 | import _common 2 | 3 | # To tickle the "is IPython loaded?" logic, make sure that our package 4 | # tolerates IPython loaded but not actually in use 5 | import IPython 6 | 7 | import simple_excepthook 8 | -------------------------------------------------------------------------------- /exceptiongroup/_tests/test_tools.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from exceptiongroup import ExceptionGroup, split 3 | 4 | 5 | def raise_error(err): 6 | raise err 7 | 8 | 9 | def raise_error_from_another(out_err, another_err): 10 | # use try..except approache so out_error have meaningful 11 | # __context__, __cause__ attribute. 12 | try: 13 | raise another_err 14 | except Exception as e: 15 | raise out_err from e 16 | 17 | 18 | def test_split_for_none_exception_should_raise_value_error(): 19 | with pytest.raises(TypeError): 20 | matched, unmatched = split(RuntimeError, None) 21 | 22 | 23 | def test_split_when_all_exception_matched(): 24 | group = ExceptionGroup( 25 | "Many Errors", 26 | [RuntimeError("Runtime Error1"), RuntimeError("Runtime Error2")], 27 | ["Runtime Error1", "Runtime Error2"], 28 | ) 29 | matched, unmatched = split(RuntimeError, group) 30 | assert matched is group 31 | assert unmatched is None 32 | 33 | 34 | def test_split_when_all_exception_unmatched(): 35 | group = ExceptionGroup( 36 | "Many Errors", 37 | [RuntimeError("Runtime Error1"), RuntimeError("Runtime Error2")], 38 | ["Runtime Error1", "Runtime Error2"], 39 | ) 40 | matched, unmatched = split(ValueError, group) 41 | assert matched is None 42 | assert unmatched is group 43 | 44 | 45 | def test_split_when_contains_matched_and_unmatched(): 46 | error1 = RuntimeError("Runtime Error1") 47 | error2 = ValueError("Value Error2") 48 | group = ExceptionGroup( 49 | "Many Errors", [error1, error2], ["Runtime Error1", "Value Error2"] 50 | ) 51 | matched, unmatched = split(RuntimeError, group) 52 | assert isinstance(matched, ExceptionGroup) 53 | assert isinstance(unmatched, ExceptionGroup) 54 | assert matched.exceptions == [error1] 55 | assert matched.message == "Many Errors" 56 | assert matched.sources == ["Runtime Error1"] 57 | assert unmatched.exceptions == [error2] 58 | assert unmatched.message == "Many Errors" 59 | assert unmatched.sources == ["Value Error2"] 60 | 61 | 62 | def test_split_with_predicate(): 63 | def _match(err): 64 | return str(err) != "skip" 65 | 66 | error1 = RuntimeError("skip") 67 | error2 = RuntimeError("Runtime Error") 68 | group = ExceptionGroup( 69 | "Many Errors", [error1, error2], ["skip", "Runtime Error"] 70 | ) 71 | matched, unmatched = split(RuntimeError, group, match=_match) 72 | assert matched.exceptions == [error2] 73 | assert unmatched.exceptions == [error1] 74 | 75 | 76 | def test_split_with_single_exception(): 77 | err = RuntimeError("Error") 78 | matched, unmatched = split(RuntimeError, err) 79 | assert matched is err 80 | assert unmatched is None 81 | 82 | matched, unmatched = split(ValueError, err) 83 | assert matched is None 84 | assert unmatched is err 85 | 86 | 87 | def test_split_and_check_attributes_same(): 88 | try: 89 | raise_error(RuntimeError("RuntimeError")) 90 | except Exception as e: 91 | run_error = e 92 | 93 | try: 94 | raise_error(ValueError("ValueError")) 95 | except Exception as e: 96 | val_error = e 97 | 98 | group = ExceptionGroup( 99 | "ErrorGroup", [run_error, val_error], ["RuntimeError", "ValueError"] 100 | ) 101 | # go and check __traceback__, __cause__ attributes 102 | try: 103 | raise_error_from_another(group, RuntimeError("Cause")) 104 | except BaseException as e: 105 | new_group = e 106 | 107 | matched, unmatched = split(RuntimeError, group) 108 | assert matched.__traceback__ is new_group.__traceback__ 109 | assert matched.__cause__ is new_group.__cause__ 110 | assert matched.__context__ is new_group.__context__ 111 | assert matched.__suppress_context__ is new_group.__suppress_context__ 112 | assert unmatched.__traceback__ is new_group.__traceback__ 113 | assert unmatched.__cause__ is new_group.__cause__ 114 | assert unmatched.__context__ is new_group.__context__ 115 | assert unmatched.__suppress_context__ is new_group.__suppress_context__ 116 | -------------------------------------------------------------------------------- /exceptiongroup/_tools.py: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Core primitives for working with ExceptionGroups 3 | ################################################################ 4 | 5 | import copy 6 | from . import ExceptionGroup 7 | 8 | 9 | def split(exc_type, exc, *, match=None): 10 | """ splits the exception into one half (matched) representing all the parts of 11 | the exception that match the predicate, and another half (not matched) 12 | representing all the parts that don't match. 13 | 14 | Args: 15 | exc_type (type of exception): The exception type we use to split. 16 | exc (BaseException): Exception object we want to split. 17 | match (None or func): predicate function to restict the split process, 18 | if the argument is not None, only exceptions with match(exception) 19 | will go into matched part. 20 | 21 | Note that if the `exc` is type of ExceptionGroup, then the return 22 | value will be tuple of (ExceptionGroup or None, ExceptionGroup or None) 23 | """ 24 | if not isinstance(exc, BaseException): 25 | raise TypeError( 26 | "Argument `exc` should be an instance of BaseException." 27 | ) 28 | if isinstance(exc, ExceptionGroup): 29 | matches = [] 30 | match_notes = [] 31 | rests = [] 32 | rest_notes = [] 33 | for subexc, note in zip(exc.exceptions, exc.sources): 34 | matched, rest = split(exc_type, subexc, match=match) 35 | if matched is not None: 36 | matches.append(matched) 37 | match_notes.append(note) 38 | if rest is not None: 39 | rests.append(rest) 40 | rest_notes.append(note) 41 | if matches and not rests: 42 | return exc, None 43 | elif rests and not matches: 44 | return None, exc 45 | else: 46 | matched_group = copy.copy(exc) 47 | matched_group.exceptions = matches 48 | matched_group.sources = match_notes 49 | rest_group = copy.copy(exc) 50 | rest_group.exceptions = rests 51 | rest_group.sources = rest_notes 52 | return matched_group, rest_group 53 | else: 54 | if isinstance(exc, exc_type) and (match is None or match(exc)): 55 | return exc, None 56 | else: 57 | return None, exc 58 | 59 | 60 | class Catcher: 61 | def __init__(self, exc_type, handler, match): 62 | self._exc_type = exc_type 63 | self._handler = handler 64 | self._match = match 65 | 66 | def __enter__(self): 67 | pass 68 | 69 | # Cases to think about: 70 | # 71 | # except RuntimeError: 72 | # pass 73 | # 74 | # -> raise 'rest' if any, it gets one extra tb entry 75 | # 76 | # except RuntimeError as exc: 77 | # raise OtherError 78 | # 79 | # -> set __context__ on OtherError, raise (OtherError, rest) 80 | # obviously gets one extra tb entry, unavoidable. doesn't really matter 81 | # whether rest exists. 82 | # 83 | # except RuntimeError as exc: 84 | # raise 85 | # except RuntimeError as exc: 86 | # raise exc 87 | # 88 | # -> in regular Python these are different. ('raise' resets the 89 | # __traceback__ to whatever it was when it was caught, and leaves 90 | # __context__ alone; 'raise exc' treats 'exc' like a new exception and 91 | # triggers that processing.) We can't realistically tell the difference 92 | # between these two cases, so we treat them both like 'raise'. 93 | # Specifically, if handler re-raises then we clear all context and tb 94 | # changes and then let the original exception propagate. 95 | # 96 | # Weird language semantics to watch out for: 97 | # If __exit__ returns False, then exc is reraised, *after restoring its 98 | # traceback to whatever it was on entry*. I think. 99 | # 100 | # And bare 'raise' restores the traceback to whatever was in 101 | # sys.exc_info()[2] when the exception was caught. I think. (This is why 102 | # we restore caught.__traceback__ *after* the handler runs, because 103 | # otherwise it might reset the tb back to a mangled state.) 104 | def __exit__(self, etype, exc, tb): 105 | __traceback_hide__ = True # for pytest 106 | caught, rest = split(self._exc_type, exc, match=self._match) 107 | if caught is None: 108 | return False 109 | # 'raise caught' might mangle some of caught's attributes, and then 110 | # handler() might mangle them more. So we save and restore them. 111 | saved_caught_context = caught.__context__ 112 | saved_caught_traceback = caught.__traceback__ 113 | # Arrange that inside the handler, any new exceptions will get 114 | # 'caught' as their __context__, and bare 'raise' will work. 115 | try: 116 | raise caught 117 | except type(caught): 118 | try: 119 | self._handler(caught) 120 | except BaseException as handler_exc: 121 | if handler_exc is caught: 122 | return False 123 | if rest is None: 124 | exceptiongroup_catch_exc = handler_exc 125 | else: 126 | exceptiongroup_catch_exc = ExceptionGroup( 127 | "caught {}".format(self._exc_type.__class__.__name__), 128 | [handler_exc, rest], 129 | ["exception raised by handler", "uncaught exceptions"], 130 | ) 131 | else: 132 | exceptiongroup_catch_exc = rest 133 | finally: 134 | caught.__context__ = saved_caught_context 135 | caught.__traceback__ = saved_caught_traceback 136 | 137 | # The 'raise' line here is arcane plumbling that regular end users 138 | # will see in the middle of tracebacks, so we try to make it readable 139 | # out-of-context. 140 | saved_context = exceptiongroup_catch_exc.__context__ 141 | try: 142 | raise exceptiongroup_catch_exc 143 | finally: 144 | exceptiongroup_catch_exc.__context__ = saved_context 145 | 146 | 147 | def catch(exc_type, handler, match=None): 148 | """Return a context manager that catches and re-throws exception. 149 | after running :meth:`handle` on them. 150 | 151 | Args: 152 | exc_type: An exception type or A tuple of exception type that need 153 | to be handled by ``handler``. Exceptions which doesn't belong to 154 | exc_type or doesn't match the predicate will not be handled by 155 | ``handler``. 156 | handler: the handler to handle exception which match exc_type and 157 | predicate. 158 | match: when the match is not None, ``handler`` will only handle when 159 | match(exc) is True 160 | """ 161 | return Catcher(exc_type, handler, match) 162 | -------------------------------------------------------------------------------- /exceptiongroup/_version.py: -------------------------------------------------------------------------------- 1 | # This file is imported from __init__.py and exec'd from setup.py 2 | 3 | __version__ = "0.0.0" 4 | -------------------------------------------------------------------------------- /newsfragments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/exceptiongroup/dd8fe794d54f058c3bf06f4b06f2f5723879209e/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.towncrier] 2 | package = "exceptiongroup" 3 | filename = "docs/source/history.rst" 4 | directory = "newsfragments" 5 | underlines = ["-", "~", "^"] 6 | issue_format = "`#{issue} `__" 7 | 8 | [tool.black] 9 | line-length = 79 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | exec(open("exceptiongroup/_version.py", encoding="utf-8").read()) 4 | 5 | LONG_DESC = open("README.rst", encoding="utf-8").read() 6 | 7 | setup( 8 | name="exceptiongroup", 9 | version=__version__, 10 | description="A way to represent multiple things going wrong at the same time, in Python", 11 | url="https://github.com/python-trio/exceptiongroup", 12 | long_description=LONG_DESC, 13 | author="Nathaniel J. Smith", 14 | author_email="njs@pobox.com", 15 | license="MIT -or- Apache License 2.0", 16 | packages=find_packages(), 17 | install_requires=["trio"], 18 | keywords=["async", "exceptions", "error handling"], 19 | python_requires=">=3.5", 20 | classifiers=[ 21 | "License :: OSI Approved :: MIT License", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Framework :: Trio", 24 | "Framework :: AsyncIO", 25 | "Operating System :: POSIX :: Linux", 26 | "Operating System :: MacOS :: MacOS X", 27 | "Operating System :: Microsoft :: Windows", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: Implementation :: CPython", 30 | "Programming Language :: Python :: Implementation :: PyPy", 31 | "Intended Audience :: Developers", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | --------------------------------------------------------------------------------