├── .build └── install_pyenv.sh ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.rst ├── appveyor.yml ├── ci_requirements.txt ├── examples.ipynb ├── pyproject.toml ├── requirements.txt ├── setup.py ├── setup_boilerplate.py ├── test ├── __init__.py ├── examples.py ├── test_dump.py ├── test_setup.py └── test_unparse.py ├── test_requirements.txt └── typed_astunparse ├── __init__.py ├── _version.py ├── printer.py ├── py.typed └── unparser.py /.build/install_pyenv.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuxo pipefail 3 | 4 | # pyenv installer (for macOS) 5 | # updated: 2019-05-13 6 | 7 | # use the following to enable diagnostics 8 | # export PYENV_DIAGNOSE=1 9 | 10 | if [[ "$(uname)" == "Darwin" ]]; then 11 | if [ -n "${DIAGNOSE_PYENV-}" ] ; then 12 | pyenv install --list 13 | fi 14 | if ! [[ ${TRAVIS_PYTHON_VERSION} =~ .*-dev$ ]] ; then 15 | TRAVIS_PYTHON_VERSION="$(pyenv install --list | grep -E " ${TRAVIS_PYTHON_VERSION}(\.[0-9brc]+)+" | tail -n 1 | sed -e 's/^[[:space:]]*//')" 16 | fi 17 | pyenv install "${TRAVIS_PYTHON_VERSION}" 18 | # export PATH="${HOME}/.pyenv/versions/${TRAVIS_PYTHON_VERSION}/bin:${PATH}" 19 | mkdir -p "${HOME}/.local/bin" 20 | ln -s "${HOME}/.pyenv/versions/${TRAVIS_PYTHON_VERSION}/bin/python" "${HOME}/.local/bin/python" 21 | ln -s "${HOME}/.pyenv/versions/${TRAVIS_PYTHON_VERSION}/bin/pip" "${HOME}/.local/bin/pip" 22 | ln -s "${HOME}/.pyenv/versions/${TRAVIS_PYTHON_VERSION}/bin/coverage" "${HOME}/.local/bin/coverage" 23 | ln -s "${HOME}/.pyenv/versions/${TRAVIS_PYTHON_VERSION}/bin/codecov" "${HOME}/.local/bin/codecov" 24 | fi 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Jupyter Notebook 2 | /.ipynb_checkpoints 3 | *-checkpoint.ipynb 4 | 5 | # macOS 6 | .DS_Store 7 | 8 | # Python 9 | /build 10 | /dist 11 | /.cache 12 | __pycache__ 13 | *.egg 14 | *.egg-info 15 | *.pyc 16 | 17 | # Python coverage 18 | /htmlcov 19 | /coverage.xml 20 | /.coverage 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | addons: 3 | homebrew: 4 | packages: 5 | - pyenv 6 | matrix: 7 | include: 8 | - os: linux 9 | language: python 10 | python: "3.5" 11 | - os: linux 12 | language: python 13 | python: "3.6" 14 | - os: linux 15 | language: python 16 | python: "3.7" 17 | - os: linux 18 | language: python 19 | python: "3.8" 20 | - os: osx 21 | osx_image: xcode11 22 | language: generic 23 | env: TRAVIS_PYTHON_VERSION="3.5" 24 | - os: osx 25 | osx_image: xcode11 26 | language: generic 27 | env: TRAVIS_PYTHON_VERSION="3.6" 28 | - os: osx 29 | osx_image: xcode11 30 | language: generic 31 | env: TRAVIS_PYTHON_VERSION="3.7" 32 | allow_failures: 33 | - python: "3.8" 34 | 35 | before_install: 36 | - .build/install_pyenv.sh 37 | 38 | install: 39 | - pip install -U pip 40 | - pip install -U -r ci_requirements.txt 41 | 42 | script: 43 | - TEST_PACKAGING=1 python -m coverage run --branch --source . -m unittest -v 44 | 45 | after_success: 46 | - python -m coverage report --show-missing 47 | - codecov 48 | 49 | before_deploy: 50 | - pip3 install -U --user version_query || pip3 install -U version_query 51 | - wget https://gist.githubusercontent.com/mbdevpl/46d458350f0c9cc7d793b67573e01f7b/raw/prepare_bintray_deployment.py 52 | - python3 prepare_bintray_deployment.py "$TRAVIS_OS_NAME-python$TRAVIS_PYTHON_VERSION" "dist/*.tar.gz" "dist/*.whl" "dist/*.zip" 53 | 54 | deploy: 55 | - provider: bintray 56 | file: ".bintray.json" 57 | user: "mbdevpl" 58 | key: 59 | secure: "QzpD9CQ+2BGmma1GLxMfBKjHRRqs0N1/CZLzvHxsitdy4HXXbfV2MG2Cq6OrYUKtZIkSoCWj/T9sEfiI/A/mszkXFgrzBBCxZm/viZ/o3SSmLZ6JfRMWFAW76K61KS9YWkRNPy8NKKTqQfiFbZij90BjvC2kanYAP+17e9xZ0VgoSoQaKehNMWTpUm+ZHEp03lF26CKroJcA2aLK4mk7OVBFd0nfmo/hGvGam4p4qYTmKrZksKFigCCjOYF/lqaFfPkGT8wvfzvywETVD/1aaP8nICxdL4b8GoIYftYludbtTkSP2VMRoyvjf9m53Ofic1I3DsJLs1j4uVDRVU/vPR4bLFXjc/SW2LjU5us6f58+4FuSpOBNMlHBhHXp00b86V40AeqZ/vRIEkBtCWDzS/kUH7NOKqGAaBbHomvirqyVjqtSPkAwEzILgcgXJrtsIT1XY1vW5Kr9HbLm4zrAX4jdJ6785Uc6/SeasbfrsZfkD70y5uaPGWGJ/7c0vwYeJFNZGZCeNYKyLPDdkf0gkeA2TQdq0ov0nWWLXCN/V6G9wtuXVY5g765PqD24FzzVPE/3YH+a1NnWOhpVmOYZ6cs0GstjtNgWOqoDx3Az35a96kxE2SjZsvIOuUamfDgfyQo5itMX0LaExPb7e4g9+zRoXQqRbKHILZsyG0L6vzk=" 60 | on: 61 | all_branches: true 62 | skip_cleanup: true 63 | 64 | notifications: 65 | slack: 66 | secure: "vlXI51/0L3jPt316tLvXxSgGQhlQYSxrVd0bHRYCW4iMQgxcURAlzydUO6+KJsnGQNd1wNehkIf8RMGuh7bX7siTBtRkrFdceGu2Mp69CVcX2wfZmTOvV6x3nyOUhFjDlWeaEpVg+kR2qmVSC+tJ6LLfIVRjPbcY+x/O4ubH79N1YYb6/ruqB2YK7ArY7yu+g6DcmYvb9xKMlG6KKQlAxoFywhLWkmReIvLH+mMcO+JUqez7HotMaVH8H6uwiA8NEX1FOeo1h7uXiqvtnxQcrWMZ3E/y7/W/BQEpfiy1JFGwInUN7Hs+uwevBtQx7q1/6EpTtP1xEFHKzGv/KTR5LGdeZTAw4KyL94Gifh2/+dTXG7jA9Ib4494JGduCPwTD5rbYQCgIu5fooCgJho+GJcShDNFrTwZ4AyCwPl3bpTgdUahqsM4kpp+dDfDXLiPO81ZRYFUM6h1VMM+KFNC/gLgnpt8Kz1JNPhPlBvlKu4rhFA7ULKzk0uKwS85U88+XhH3zzKi5jSDM9ejHKyNQFrHuIdkTjc+CGrc1U4/r3GlPFTjcJ0iVEftYwkTRYjAAHC/CUTsTYv5HKEY/eLYRH3R+el0p6BX9l2BIMTfJy6WXD8oyARN37ydRvkPxa1vQifG1MebMx0HxZseVcBfDOzvH9RLt5o6wMwnWkiHlm24=" 67 | email: false 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup_boilerplate.py 2 | include *requirements.txt 3 | include LICENSE 4 | include NOTICE 5 | include ./*/py.typed 6 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2019 Mateusz Bysiek https://mbdevpl.github.io/ 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. role:: bash(code) 2 | :language: bash 3 | 4 | .. role:: python(code) 5 | :language: python 6 | 7 | 8 | ================ 9 | typed-astunparse 10 | ================ 11 | 12 | Unparser for Python 3 abstract syntax trees (ASTs) with type comments. 13 | 14 | .. image:: https://img.shields.io/pypi/v/typed-astunparse.svg 15 | :target: https://pypi.org/project/typed-astunparse 16 | :alt: package version from PyPI 17 | 18 | .. image:: https://travis-ci.org/mbdevpl/typed-astunparse.svg?branch=master 19 | :target: https://travis-ci.org/mbdevpl/typed-astunparse 20 | :alt: build status from Travis CI 21 | 22 | .. image:: https://ci.appveyor.com/api/projects/status/github/mbdevpl/typed-astunparse?svg=true 23 | :target: https://ci.appveyor.com/project/mbdevpl/typed-astunparse 24 | :alt: build status from AppVeyor 25 | 26 | .. image:: https://api.codacy.com/project/badge/Grade/4a6d141d87c346f0b3c0d50d76a10e32 27 | :target: https://www.codacy.com/app/mbdevpl/typed-astunparse 28 | :alt: grade from Codacy 29 | 30 | .. image:: https://codecov.io/gh/mbdevpl/typed-astunparse/branch/master/graph/badge.svg 31 | :target: https://codecov.io/gh/mbdevpl/typed-astunparse 32 | :alt: test coverage from Codecov 33 | 34 | .. image:: https://img.shields.io/github/license/mbdevpl/typed-astunparse.svg 35 | :target: https://github.com/mbdevpl/typed-astunparse/blob/master/NOTICE 36 | :alt: license 37 | 38 | The *typed-astunparse* is to *typed-ast* as *astunparse* is to *ast*. In short: unparsing of Python 39 | 3 abstract syntax trees (ASTs) with type comments. 40 | 41 | .. contents:: 42 | :backlinks: none 43 | 44 | 45 | Why this module was created 46 | =========================== 47 | 48 | The built-in *ast* module can parse Python source code into AST but it can't generate source 49 | code from the AST. The *astunparse* module (using a refactored version of an obscure 50 | script found in official Python repository) provides code generation capability for native 51 | Python AST. 52 | 53 | However, both *ast* and *astunparse* modules completely ignore type comments introduced in 54 | PEP 484. They treat them like all other comments, so when you parse the code using 55 | :python:`compile()`, your type comments will be lost. There is no place for them in the AST, so 56 | obviously they also cannot be unparsed. 57 | 58 | The *typed-ast* module provides an updated AST including type comments defined in PEP 484 and 59 | a parser for Python code that contains such comments. 60 | 61 | Unfortunately, *typed-ast* doesn't provide any means to go from AST back to source code with type 62 | comments. This is why module *typed-astunparse* (i.e. this one) was created: to provide unparser 63 | for AST defined in *typed-ast*. 64 | 65 | 66 | Usage 67 | ===== 68 | 69 | Example of roundtrip from code through AST to code: 70 | 71 | .. code:: python 72 | 73 | import typed_ast.ast3 74 | import typed_astunparse 75 | 76 | code = 'my_string = None # type: str' 77 | roundtrip = typed_astunparse.unparse(typed_ast.ast3.parse(code)) 78 | print(roundtrip) 79 | 80 | This will print: 81 | 82 | .. code:: python 83 | 84 | my_string = None # type: str 85 | 86 | 87 | for more examples see ``_ notebook. 88 | 89 | 90 | 91 | Installation 92 | ============ 93 | 94 | For simplest installation use :bash:`pip`: 95 | 96 | .. code:: bash 97 | 98 | pip3 install typed-astunparse 99 | 100 | You can also build your own version: 101 | 102 | .. code:: bash 103 | 104 | git clone https://github.com/mbdevpl/typed-astunparse 105 | cd typed-astunparse 106 | pip3 install -U test_requirements.txt 107 | python3 -m unittest # make sure the tests pass 108 | python3 setup.py bdist_wheel 109 | pip3 install dist/*.whl 110 | 111 | 112 | Requirements 113 | ------------ 114 | 115 | Python version 3.5 or later. 116 | 117 | Python libraries as specified in ``_. 118 | 119 | Building and running tests additionally requires packages listed in ``_. 120 | 121 | Tested on Linux, OS X and Windows. 122 | 123 | 124 | Links 125 | ===== 126 | 127 | 128 | Extensions of this module 129 | ------------------------- 130 | 131 | If you're extending typed-astunparse and you'd like to share why, 132 | feel free to submit a `pull request `_ 133 | introducing your project. 134 | 135 | - *horast*: human-oriented ast 136 | 137 | Built upon both *typed-ast* and *typed-astunparse* providing parsing and unparsing 138 | of arbitrary comments in addition to type comments. 139 | 140 | https://pypi.org/project/horast 141 | 142 | https://github.com/mbdevpl/horast 143 | 144 | 145 | Who's using this module and why 146 | ------------------------------- 147 | 148 | If you're using typed-astunparse in your work and you'd like to share why, 149 | feel free to submit a `pull request `_ 150 | introducing your project. 151 | 152 | - *static-typing*: using *typed-astunparse* directly to provide AST unparsing function 153 | 154 | https://pypi.org/project/static-typing 155 | 156 | https://github.com/mbdevpl/static-typing 157 | 158 | 159 | References 160 | ---------- 161 | 162 | - *ast*: 163 | 164 | https://docs.python.org/3/library/ast.html 165 | 166 | https://greentreesnakes.readthedocs.io/ 167 | 168 | - *astunparse*: 169 | 170 | https://pypi.org/project/astunparse 171 | 172 | https://github.com/simonpercivall/astunparse 173 | 174 | https://astunparse.readthedocs.io/en/latest/ 175 | 176 | - PEP 483 - The Theory of Type Hints: 177 | 178 | https://www.python.org/dev/peps/pep-0483/ 179 | 180 | - PEP 484 - Type Hints: 181 | 182 | https://www.python.org/dev/peps/pep-0484/ 183 | 184 | - PEP 3107 - Function Annotations: 185 | 186 | https://www.python.org/dev/peps/pep-3107/ 187 | 188 | - PEP 526 - Syntax for Variable Annotations: 189 | 190 | https://www.python.org/dev/peps/pep-0526/ 191 | 192 | - *typed-ast*: 193 | 194 | https://pypi.org/project/typed-ast 195 | 196 | https://github.com/python/typed_ast 197 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | environment: 4 | matrix: 5 | - ARCHITECTURE: "x86" 6 | PYTHON_VERSION: "3.5" 7 | PYTHON: "C:\\Python35" 8 | - ARCHITECTURE: "x64" 9 | PYTHON_VERSION: "3.5" 10 | PYTHON: "C:\\Python35-x64" 11 | - ARCHITECTURE: "x86" 12 | PYTHON_VERSION: "3.6" 13 | PYTHON: "C:\\Python36" 14 | - ARCHITECTURE: "x64" 15 | PYTHON_VERSION: "3.6" 16 | PYTHON: "C:\\Python36-x64" 17 | - ARCHITECTURE: "x86" 18 | PYTHON_VERSION: "3.7" 19 | PYTHON: "C:\\Python37" 20 | - ARCHITECTURE: "x64" 21 | PYTHON_VERSION: "3.7" 22 | PYTHON: "C:\\Python37-x64" 23 | 24 | init: 25 | - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% 26 | 27 | install: 28 | - python -m pip install -U pip 29 | - python -m pip install -U -r ci_requirements.txt 30 | 31 | build: off 32 | 33 | test_script: 34 | - set TEST_PACKAGING=1 35 | - python -m coverage run --branch --source . -m unittest -v 36 | 37 | after_test: 38 | - python -m coverage report --show-missing 39 | - codecov 40 | # Bintray archive preparation 41 | - python -m pip install version_query 42 | - ps: Invoke-WebRequest "https://gist.githubusercontent.com/mbdevpl/46d458350f0c9cc7d793b67573e01f7b/raw/prepare_bintray_deployment.py" -OutFile "prepare_bintray_deployment.py" 43 | - python prepare_bintray_deployment.py "windows%ARCHITECTURE%-python%PYTHON_VERSION%" "dist\*.tar.gz" "dist\*.whl" "dist\*.zip" 44 | - set /p BINTRAY_VERSION=<.bintray_version.txt 45 | 46 | artifacts: 47 | - path: dist\*.tar.gz 48 | - path: dist\*.whl 49 | - path: dist\*.zip 50 | - path: '*-bintray.zip' 51 | 52 | deploy: 53 | - provider: BinTray 54 | username: $(APPVEYOR_ACCOUNT_NAME) 55 | api_key: 56 | secure: cMLbWadS24XyCD5RU3XM+2GrgqtTfoBgKwkQXyDyVa/3QOF1rXheHki+BRXP5tLo 57 | subject: $(APPVEYOR_ACCOUNT_NAME) 58 | repo: pkgs 59 | package: $(APPVEYOR_PROJECT_NAME) 60 | version: $(BINTRAY_VERSION) 61 | publish: true 62 | override: true 63 | explode: true 64 | artifact: /.*-bintray\.zip/ 65 | -------------------------------------------------------------------------------- /ci_requirements.txt: -------------------------------------------------------------------------------- 1 | codecov 2 | coverage 3 | -rtest_requirements.txt 4 | -------------------------------------------------------------------------------- /examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "scrolled": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import pathlib\n", 12 | "\n", 13 | "import typed_ast.ast3\n", 14 | "import typed_astunparse" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 2, 20 | "metadata": { 21 | "scrolled": false 22 | }, 23 | "outputs": [ 24 | { 25 | "name": "stdout", 26 | "output_type": "stream", 27 | "text": [ 28 | "\n", 29 | "my_string = None # type: str\n", 30 | "\n" 31 | ] 32 | } 33 | ], 34 | "source": [ 35 | "code = 'my_string = None # type: str'\n", 36 | "roundtrip = typed_astunparse.unparse(typed_ast.ast3.parse(code))\n", 37 | "print(roundtrip)" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 3, 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "name": "stdout", 47 | "output_type": "stream", 48 | "text": [ 49 | "\n", 50 | "\n", 51 | "def negation(arg):\n", 52 | " # type: (bool) -> bool\n", 53 | " return (not arg)\n", 54 | "\n" 55 | ] 56 | } 57 | ], 58 | "source": [ 59 | "code = \"def negation(arg): # type: (bool) -> bool\\n return (not arg)\"\n", 60 | "roundtrip = typed_astunparse.unparse(typed_ast.ast3.parse(code))\n", 61 | "print(roundtrip)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 4, 67 | "metadata": {}, 68 | "outputs": [ 69 | { 70 | "name": "stdout", 71 | "output_type": "stream", 72 | "text": [ 73 | "\n", 74 | "with open('setup.py') as f: # type: typing.io.TextIO\n", 75 | " print(f.read())\n", 76 | "\n" 77 | ] 78 | } 79 | ], 80 | "source": [ 81 | "code = \"with open('setup.py') as f: # type: typing.io.TextIO\\n print(f.read())\"\n", 82 | "roundtrip = typed_astunparse.unparse(typed_ast.ast3.parse(code))\n", 83 | "print(roundtrip)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 5, 89 | "metadata": { 90 | "scrolled": false 91 | }, 92 | "outputs": [ 93 | { 94 | "name": "stdout", 95 | "output_type": "stream", 96 | "text": [ 97 | "\n", 98 | "'This is \"__init__.py\" file for \"typed_astunparse\" package.\\n\\nfunctions: unparse, dump\\n'\n", 99 | "import ast\n", 100 | "import typing as t\n", 101 | "import typed_ast.ast3\n", 102 | "from six.moves import cStringIO\n", 103 | "from .unparser import Unparser\n", 104 | "from .printer import Printer\n", 105 | "from ._version import VERSION\n", 106 | "__version__ = VERSION\n", 107 | "\n", 108 | "def unparse(tree: t.Union[(ast.AST, typed_ast.ast3.AST)]) -> str:\n", 109 | " 'Unparse the abstract syntax tree into a str.\\n\\n Behave just like astunparse.unparse(tree), but handle trees which are typed, untyped, or mixed.\\n In other words, a mixture of ast.AST-based and typed_ast.ast3-based nodes will be unparsed.\\n '\n", 110 | " stream = cStringIO()\n", 111 | " Unparser(tree, file=stream)\n", 112 | " return stream.getvalue()\n", 113 | "\n", 114 | "def dump(tree: t.Union[(ast.AST, typed_ast.ast3.AST)], annotate_fields: bool=True, include_attributes: bool=False) -> str:\n", 115 | " 'Behave just like astunparse.dump(tree), but handle typed_ast.ast3-based trees.'\n", 116 | " stream = cStringIO()\n", 117 | " Printer(file=stream, annotate_fields=annotate_fields, include_attributes=include_attributes).visit(tree)\n", 118 | " return stream.getvalue()\n", 119 | "__all__ = ['unparse', 'dump']\n", 120 | "\n" 121 | ] 122 | } 123 | ], 124 | "source": [ 125 | "with pathlib.Path('typed_astunparse', '__init__.py').open() as f:\n", 126 | " tree = typed_ast.ast3.parse(f.read())\n", 127 | "#print(typed_astunparse.dump(tree)) # long output\n", 128 | "print(typed_astunparse.unparse(tree))" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 6, 134 | "metadata": { 135 | "scrolled": false 136 | }, 137 | "outputs": [ 138 | { 139 | "name": "stdout", 140 | "output_type": "stream", 141 | "text": [ 142 | "\n", 143 | "'This is setup.py file for typed-astunparse.'\n", 144 | "import setup_boilerplate\n", 145 | "\n", 146 | "class Package(setup_boilerplate.Package):\n", 147 | " 'Package metadata.'\n", 148 | " name = 'typed-astunparse'\n", 149 | " description = 'typed-astunparse is to typed-ast as astunparse is to ast'\n", 150 | " download_url = 'https://github.com/mbdevpl/typed-astunparse'\n", 151 | " classifiers = ['Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Education', 'Topic :: Scientific/Engineering', 'Topic :: Software Development :: Code Generators', 'Topic :: Software Development :: Compilers', 'Topic :: Software Development :: Pre-processors', 'Topic :: Utilities']\n", 152 | " keywords = ['ast', 'unparsing', 'pretty printing']\n", 153 | "if (__name__ == '__main__'):\n", 154 | " Package.setup()\n", 155 | "\n" 156 | ] 157 | } 158 | ], 159 | "source": [ 160 | "with pathlib.Path('setup.py').open() as f:\n", 161 | " tree = typed_ast.ast3.parse(f.read())\n", 162 | "#print(typed_astunparse.dump(tree)) # long output\n", 163 | "print(typed_astunparse.unparse(tree))" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 7, 169 | "metadata": { 170 | "scrolled": true 171 | }, 172 | "outputs": [ 173 | { 174 | "name": "stdout", 175 | "output_type": "stream", 176 | "text": [ 177 | "Module(\n", 178 | " body=[Assign(\n", 179 | " targets=[Name(\n", 180 | " id='a',\n", 181 | " ctx=Store())],\n", 182 | " value=Num(n=0),\n", 183 | " type_comment=None)],\n", 184 | " type_ignores=[])\n" 185 | ] 186 | } 187 | ], 188 | "source": [ 189 | "code = \"a = 0\"\n", 190 | "tree = typed_ast.ast3.parse(code)\n", 191 | "print(typed_astunparse.dump(tree))" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 8, 197 | "metadata": { 198 | "scrolled": true 199 | }, 200 | "outputs": [ 201 | { 202 | "name": "stdout", 203 | "output_type": "stream", 204 | "text": [ 205 | "Module(\n", 206 | " body=[Assign(\n", 207 | " targets=[Name(\n", 208 | " id='a',\n", 209 | " ctx=Store())],\n", 210 | " value=Num(n=0),\n", 211 | " type_comment='')],\n", 212 | " type_ignores=[])\n" 213 | ] 214 | } 215 | ], 216 | "source": [ 217 | "code = \"a = 0 # type:\"\n", 218 | "tree = typed_ast.ast3.parse(code)\n", 219 | "print(typed_astunparse.dump(tree))" 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": 9, 225 | "metadata": {}, 226 | "outputs": [ 227 | { 228 | "name": "stdout", 229 | "output_type": "stream", 230 | "text": [ 231 | "Module(\n", 232 | " body=[Assign(\n", 233 | " targets=[Name(\n", 234 | " id='a',\n", 235 | " ctx=Store())],\n", 236 | " value=Num(n=0),\n", 237 | " type_comment='int')],\n", 238 | " type_ignores=[])\n" 239 | ] 240 | } 241 | ], 242 | "source": [ 243 | "code = \"a = 0 # type: int\"\n", 244 | "tree = typed_ast.ast3.parse(code)\n", 245 | "print(typed_astunparse.dump(tree))" 246 | ] 247 | } 248 | ], 249 | "metadata": { 250 | "kernelspec": { 251 | "display_name": "Python 3", 252 | "language": "python", 253 | "name": "python3" 254 | }, 255 | "language_info": { 256 | "codemirror_mode": { 257 | "name": "ipython", 258 | "version": 3 259 | }, 260 | "file_extension": ".py", 261 | "mimetype": "text/x-python", 262 | "name": "python", 263 | "nbconvert_exporter": "python", 264 | "pygments_lexer": "ipython3", 265 | "version": "3.6.6" 266 | } 267 | }, 268 | "nbformat": 4, 269 | "nbformat_minor": 1 270 | } 271 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires=['docutils', 'setuptools', 'version-query', 'wheel'] 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astunparse >= 1.6.2 2 | typed-ast >= 1.4.0 3 | version-query 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """This is setup.py file for typed-astunparse.""" 2 | 3 | import setup_boilerplate 4 | 5 | 6 | class Package(setup_boilerplate.Package): 7 | 8 | """Package metadata.""" 9 | 10 | name = 'typed-astunparse' 11 | description = 'typed-astunparse is to typed-ast as astunparse is to ast' 12 | url = 'https://github.com/mbdevpl/typed-astunparse' 13 | classifiers = [ 14 | 'Development Status :: 4 - Beta', 15 | 'Environment :: Console', 16 | 'Intended Audience :: Developers', 17 | 'Intended Audience :: Science/Research', 18 | 'License :: OSI Approved :: Apache Software License', 19 | 'Natural Language :: English', 20 | 'Operating System :: MacOS :: MacOS X', 21 | 'Operating System :: Microsoft :: Windows', 22 | 'Operating System :: POSIX :: Linux', 23 | 'Programming Language :: Python :: 3.5', 24 | 'Programming Language :: Python :: 3.6', 25 | 'Programming Language :: Python :: 3.7', 26 | 'Programming Language :: Python :: 3 :: Only', 27 | 'Programming Language :: Python :: Implementation :: CPython', 28 | 'Topic :: Education', 29 | 'Topic :: Scientific/Engineering', 30 | 'Topic :: Software Development :: Code Generators', 31 | 'Topic :: Software Development :: Compilers', 32 | 'Topic :: Software Development :: Pre-processors', 33 | 'Topic :: Utilities'] 34 | keywords = ['ast', 'unparsing', 'pretty printing'] 35 | 36 | 37 | if __name__ == '__main__': 38 | Package.setup() 39 | -------------------------------------------------------------------------------- /setup_boilerplate.py: -------------------------------------------------------------------------------- 1 | """Below code is generic boilerplate and normally should not be changed. 2 | 3 | To avoid setup script boilerplate, create "setup.py" file with the minimal contents as given 4 | in SETUP_TEMPLATE below, and modify it according to the specifics of your package. 5 | 6 | See the implementation of setup_boilerplate.Package for default metadata values and available 7 | options. 8 | """ 9 | 10 | import pathlib 11 | import runpy 12 | import sys 13 | import typing as t 14 | 15 | import setuptools 16 | 17 | __updated__ = '2019-06-04' 18 | 19 | SETUP_TEMPLATE = '''"""Setup script.""" 20 | 21 | import setup_boilerplate 22 | 23 | 24 | class Package(setup_boilerplate.Package): 25 | 26 | """Package metadata.""" 27 | 28 | name = '' 29 | description = '' 30 | url = 'https://github.com/mbdevpl/...' 31 | classifiers = [ 32 | 'Development Status :: 1 - Planning', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | 'Programming Language :: Python :: 3.8', 37 | 'Programming Language :: Python :: 3 :: Only'] 38 | keywords = [] 39 | 40 | 41 | if __name__ == '__main__': 42 | Package.setup() 43 | ''' 44 | 45 | HERE = pathlib.Path(__file__).resolve().parent 46 | 47 | 48 | def find_version( 49 | package_name: str, version_module_name: str = '_version', 50 | version_variable_name: str = 'VERSION') -> str: 51 | """Simulate behaviour of "from package_name._version import VERSION", and return VERSION. 52 | 53 | To avoid importing whole package only to read the version, just module containing the version 54 | is imported. Therefore relative imports in that module will break the setup. 55 | """ 56 | version_module_path = '{}/{}.py'.format(package_name.replace('-', '_'), version_module_name) 57 | version_module_vars = runpy.run_path(version_module_path) 58 | return version_module_vars[version_variable_name] 59 | 60 | 61 | def find_packages(root_directory: str = '.') -> t.List[str]: 62 | """Find packages to pack.""" 63 | exclude = ['test', 'test.*'] if ('bdist_wheel' in sys.argv or 'bdist' in sys.argv) else [] 64 | packages_list = setuptools.find_packages(root_directory, exclude=exclude) 65 | return packages_list 66 | 67 | 68 | def parse_requirements( 69 | requirements_path: str = 'requirements.txt') -> t.List[str]: 70 | """Read contents of requirements.txt file and return data from its relevant lines. 71 | 72 | Only non-empty and non-comment lines are relevant. 73 | """ 74 | requirements = [] 75 | with HERE.joinpath(requirements_path).open() as reqs_file: 76 | for requirement in [line.strip() for line in reqs_file.read().splitlines()]: 77 | if not requirement or requirement.startswith('#'): 78 | continue 79 | requirements.append(requirement) 80 | return requirements 81 | 82 | 83 | def partition_version_classifiers( 84 | classifiers: t.Sequence[str], version_prefix: str = 'Programming Language :: Python :: ', 85 | only_suffix: str = ' :: Only') -> t.Tuple[t.List[str], t.List[str]]: 86 | """Find version number classifiers in given list and partition them into 2 groups.""" 87 | versions_min, versions_only = [], [] 88 | for classifier in classifiers: 89 | version = classifier.replace(version_prefix, '') 90 | versions = versions_min 91 | if version.endswith(only_suffix): 92 | version = version.replace(only_suffix, '') 93 | versions = versions_only 94 | try: 95 | versions.append(tuple([int(_) for _ in version.split('.')])) 96 | except ValueError: 97 | pass 98 | return versions_min, versions_only 99 | 100 | 101 | def find_required_python_version( 102 | classifiers: t.Sequence[str], version_prefix: str = 'Programming Language :: Python :: ', 103 | only_suffix: str = ' :: Only') -> t.Optional[str]: 104 | """Determine the minimum required Python version.""" 105 | versions_min, versions_only = partition_version_classifiers( 106 | classifiers, version_prefix, only_suffix) 107 | if len(versions_only) > 1: 108 | raise ValueError( 109 | 'more than one "{}" version encountered in {}'.format(only_suffix, versions_only)) 110 | only_version = None 111 | if len(versions_only) == 1: 112 | only_version = versions_only[0] 113 | for version in versions_min: 114 | if version[:len(only_version)] != only_version: 115 | raise ValueError( 116 | 'the "{}" version {} is inconsistent with version {}' 117 | .format(only_suffix, only_version, version)) 118 | min_supported_version = None 119 | for version in versions_min: 120 | if min_supported_version is None or \ 121 | (len(version) >= len(min_supported_version) and version < min_supported_version): 122 | min_supported_version = version 123 | if min_supported_version is None: 124 | if only_version is not None: 125 | return '.'.join([str(_) for _ in only_version]) 126 | else: 127 | return '>=' + '.'.join([str(_) for _ in min_supported_version]) 128 | return None 129 | 130 | 131 | def resolve_relative_rst_links(text: str, base_link: str): 132 | """Resolve all relative links in a given RST document. 133 | 134 | All links of form `link`_ become `link `_. 135 | """ 136 | import docutils.nodes 137 | import docutils.parsers.rst 138 | import docutils.utils 139 | 140 | def parse_rst(text: str) -> docutils.nodes.document: 141 | """Parse text assuming it's an RST markup.""" 142 | parser = docutils.parsers.rst.Parser() 143 | components = (docutils.parsers.rst.Parser,) 144 | settings = docutils.frontend.OptionParser(components=components).get_default_values() 145 | document = docutils.utils.new_document('', settings=settings) 146 | parser.parse(text, document) 147 | return document 148 | 149 | class SimpleRefCounter(docutils.nodes.NodeVisitor): 150 | """Find all simple references in a given docutils document.""" 151 | 152 | def __init__(self, *args, **kwargs): 153 | """Initialize the SimpleRefCounter object.""" 154 | super().__init__(*args, **kwargs) 155 | self.references = [] 156 | 157 | def visit_reference(self, node: docutils.nodes.reference) -> None: 158 | """Call for "reference" nodes.""" 159 | if len(node.children) != 1 or not isinstance(node.children[0], docutils.nodes.Text) \ 160 | or not all(_ in node.attributes for _ in ('name', 'refuri')): 161 | return 162 | path = pathlib.Path(node.attributes['refuri']) 163 | try: 164 | if path.is_absolute(): 165 | return 166 | resolved_path = path.resolve() 167 | except FileNotFoundError: # in resolve(), prior to Python 3.6 168 | return 169 | except OSError: # in is_absolute() and resolve(), on URLs in Windows 170 | return 171 | try: 172 | resolved_path.relative_to(HERE) 173 | except ValueError: 174 | return 175 | if not path.is_file(): 176 | return 177 | assert node.attributes['name'] == node.children[0].astext() 178 | self.references.append(node) 179 | 180 | def unknown_visit(self, node: docutils.nodes.Node) -> None: 181 | """Call for unknown node types.""" 182 | return 183 | 184 | document = parse_rst(text) 185 | visitor = SimpleRefCounter(document) 186 | document.walk(visitor) 187 | for target in visitor.references: 188 | name = target.attributes['name'] 189 | uri = target.attributes['refuri'] 190 | new_link = '`{} <{}{}>`_'.format(name, base_link, uri) 191 | if name == uri: 192 | text = text.replace('`<{}>`_'.format(uri), new_link) 193 | else: 194 | text = text.replace('`{} <{}>`_'.format(name, uri), new_link) 195 | return text 196 | 197 | 198 | class Package: 199 | """Default metadata and behaviour for a Python package setup script.""" 200 | 201 | root_directory = '.' # type: str 202 | """Root directory of the source code of the package, relative to the setup.py file location.""" 203 | 204 | name = None # type: str 205 | 206 | version = None # type: str 207 | """"If None, it will be obtained from "package_name._version.VERSION" variable.""" 208 | 209 | description = None # type: str 210 | 211 | long_description = None # type: str 212 | """If None, it will be generated from readme.""" 213 | 214 | long_description_content_type = None # type: str 215 | """If None, it will be set accodring to readme file extension. 216 | 217 | For this field to be automatically set, also long_description field has to be None. 218 | """ 219 | 220 | url = 'https://github.com/mbdevpl' # type: str 221 | download_url = None # type: str 222 | author = 'Mateusz Bysiek' # type: str 223 | author_email = 'mateusz.bysiek@gmail.com' # type: str 224 | # maintainer = None # type: str 225 | # maintainer_email = None # type: str 226 | license_str = 'Apache License 2.0' # type: str 227 | 228 | classifiers = [] # type: t.List[str] 229 | """List of valid project classifiers: https://pypi.org/pypi?:action=list_classifiers""" 230 | 231 | keywords = [] # type: t.List[str] 232 | 233 | packages = None # type: t.List[str] 234 | """If None, determined with help of setuptools.""" 235 | 236 | package_data = {} 237 | exclude_package_data = {} 238 | 239 | install_requires = None # type: t.List[str] 240 | """If None, determined using requirements.txt.""" 241 | 242 | extras_require = {} # type: t.Mapping[str, t.List[str]] 243 | """A dictionary containing entries of type 'some_feature': ['requirement1', 'requirement2'].""" 244 | 245 | python_requires = None # type: str 246 | """If None, determined from provided classifiers.""" 247 | 248 | entry_points = {} # type: t.Mapping[str, t.List[str]] 249 | """A dictionary used to enable automatic creation of console scripts, gui scripts and plugins. 250 | 251 | Example entry: 252 | 'console_scripts': ['script_name = package.subpackage:function'] 253 | """ 254 | 255 | test_suite = 'test' # type: str 256 | 257 | @classmethod 258 | def try_fields(cls, *names) -> t.Optional[t.Any]: 259 | """Return first existing of given class field names.""" 260 | for name in names: 261 | if hasattr(cls, name): 262 | return getattr(cls, name) 263 | raise AttributeError((cls, names)) 264 | 265 | @classmethod 266 | def parse_readme(cls, readme_path: str = 'README.rst', 267 | encoding: str = 'utf-8') -> t.Tuple[str, str]: 268 | """Parse readme and resolve relative links in it if it is feasible. 269 | 270 | Links are resolved if readme is in rst format and the package is hosted on GitHub. 271 | """ 272 | readme_path = pathlib.Path(readme_path) 273 | with HERE.joinpath(readme_path).open(encoding=encoding) as readme_file: 274 | long_description = readme_file.read() # type: str 275 | 276 | if readme_path.suffix.lower() == '.rst' and cls.url.startswith('https://github.com/'): 277 | base_url = '{}/blob/v{}/'.format(cls.url, cls.version) 278 | long_description = resolve_relative_rst_links(long_description, base_url) 279 | 280 | long_description_content_type = {'.rst': 'text/x-rst', '.md': 'text/markdown'}.get( 281 | readme_path.suffix.lower(), 'text/plain') 282 | long_description_content_type += '; charset=UTF-8' 283 | 284 | return long_description, long_description_content_type 285 | 286 | @classmethod 287 | def prepare(cls) -> None: 288 | """Fill in possibly missing package metadata.""" 289 | if cls.version is None: 290 | cls.version = find_version(cls.name) 291 | if cls.long_description is None: 292 | cls.long_description, cls.long_description_content_type = cls.parse_readme() 293 | if cls.packages is None: 294 | cls.packages = find_packages(cls.root_directory) 295 | if cls.install_requires is None: 296 | cls.install_requires = parse_requirements() 297 | if cls.python_requires is None: 298 | cls.python_requires = find_required_python_version(cls.classifiers) 299 | 300 | @classmethod 301 | def setup(cls) -> None: 302 | """Call setuptools.setup with correct arguments.""" 303 | cls.prepare() 304 | setuptools.setup( 305 | name=cls.name, version=cls.version, description=cls.description, 306 | long_description=cls.long_description, 307 | long_description_content_type=cls.long_description_content_type, 308 | url=cls.url, download_url=cls.download_url, 309 | author=cls.author, author_email=cls.author_email, 310 | maintainer=cls.try_fields('maintainer', 'author'), 311 | maintainer_email=cls.try_fields('maintainer_email', 'author_email'), 312 | license=cls.license_str, classifiers=cls.classifiers, keywords=cls.keywords, 313 | packages=cls.packages, package_dir={'': cls.root_directory}, 314 | include_package_data=True, 315 | package_data=cls.package_data, exclude_package_data=cls.exclude_package_data, 316 | install_requires=cls.install_requires, extras_require=cls.extras_require, 317 | python_requires=cls.python_requires, 318 | entry_points=cls.entry_points, test_suite=cls.test_suite) 319 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Mateusz Bysiek http://mbdev.pl/ 2 | # This file is part of typed-astunparse. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """This is "__init__.py" file for tests of "typed_astunparse" package.""" 17 | -------------------------------------------------------------------------------- /test/examples.py: -------------------------------------------------------------------------------- 1 | """Examples used in unit tests.""" 2 | 3 | import os 4 | import platform 5 | import sys 6 | 7 | import typed_ast.ast3 8 | import typed_astunparse 9 | 10 | MODES = ['exec', 'eval', 'single'] 11 | 12 | EXAMPLES = { 13 | 'function definiton': { 14 | 'code': "def negation(arg):\n return (not arg)", 15 | 'is_expression': False, 16 | 'tree': typed_ast.ast3.FunctionDef( 17 | 'negation', 18 | typed_ast.ast3.arguments( 19 | [typed_ast.ast3.arg('arg', None, None)], 20 | None, [], [], None, []), 21 | [typed_ast.ast3.Return(typed_ast.ast3.UnaryOp( 22 | typed_ast.ast3.Not(), typed_ast.ast3.Name('arg', typed_ast.ast3.Load())))], 23 | [], None, None), 24 | 'dump': 25 | "FunctionDef(name='negation',args=arguments(" 26 | "args=[arg(arg='arg',annotation=None,type_comment=None)]," 27 | "vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 28 | "body=[Return(value=UnaryOp(op=Not(),operand=Name(id='arg',ctx=Load())))]," 29 | "decorator_list=[],returns=None,type_comment=None)"}, 30 | 'function definition with type annotations': { 31 | 'code': "def negation(arg: bool) -> bool:\n return (not arg)", 32 | 'is_expression': False, 33 | 'tree': typed_ast.ast3.FunctionDef( 34 | 'negation', 35 | typed_ast.ast3.arguments( 36 | [typed_ast.ast3.arg( 37 | 'arg', typed_ast.ast3.Name('bool', typed_ast.ast3.Load()), None)], 38 | None, [], [], None, []), 39 | [typed_ast.ast3.Return(typed_ast.ast3.UnaryOp( 40 | typed_ast.ast3.Not(), typed_ast.ast3.Name('arg', typed_ast.ast3.Load())))], 41 | [], typed_ast.ast3.Name('bool', typed_ast.ast3.Load()), None), 42 | 'dump': 43 | "FunctionDef(name='negation',args=arguments(" 44 | "args=[arg(arg='arg',annotation=Name(id='bool',ctx=Load()),type_comment=None)]," 45 | "vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 46 | "body=[Return(value=UnaryOp(op=Not(),operand=Name(id='arg',ctx=Load())))]," 47 | "decorator_list=[],returns=Name(id='bool',ctx=Load()),type_comment=None)"}, 48 | 'function definiton with type comment': { 49 | 'code': "def negation(arg):\n # type: (bool) -> bool\n return (not arg)", 50 | 'is_expression': False, 51 | 'tree': typed_ast.ast3.FunctionDef( 52 | 'negation', 53 | typed_ast.ast3.arguments( 54 | [typed_ast.ast3.arg('arg', None, None)], 55 | None, [], [], None, []), 56 | [typed_ast.ast3.Return(typed_ast.ast3.UnaryOp( 57 | typed_ast.ast3.Not(), typed_ast.ast3.Name('arg', typed_ast.ast3.Load())))], 58 | [], None, '(bool) -> bool'), 59 | 'dump': 60 | "FunctionDef(name='negation',args=arguments(" 61 | "args=[arg(arg='arg',annotation=None,type_comment=None)]," 62 | "vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 63 | "body=[Return(value=UnaryOp(op=Not(),operand=Name(id='arg',ctx=Load())))]," 64 | "decorator_list=[],returns=None,type_comment='(bool)->bool')"}, 65 | 'function definiton with per-argument type comment': { 66 | 'code': 67 | "def negation(arg # type: bool\n" 68 | " ):\n # type: (...) -> bool\n return (not arg)", 69 | 'is_expression': False, 70 | 'tree': typed_ast.ast3.FunctionDef( 71 | 'negation', 72 | typed_ast.ast3.arguments( 73 | [typed_ast.ast3.arg('arg', None, 'bool')], 74 | None, [], [], None, []), 75 | [typed_ast.ast3.Return(typed_ast.ast3.UnaryOp( 76 | typed_ast.ast3.Not(), typed_ast.ast3.Name('arg', typed_ast.ast3.Load())))], 77 | [], None, '(...) -> bool'), 78 | 'dump': 79 | "FunctionDef(name='negation',args=arguments(" 80 | "args=[arg(arg='arg',annotation=None,type_comment='bool')]," 81 | "vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 82 | "body=[Return(value=UnaryOp(op=Not(),operand=Name(id='arg',ctx=Load())))]," 83 | "decorator_list=[],returns=None,type_comment='(...)->bool')"}, 84 | 'function definiton with several per-argument type comments': { 85 | 'code': 86 | "def lovely(spam, # type: bool\n" 87 | " eggs=None, # type: int\n" 88 | " ham=None # type: str\n" 89 | " ):\n # type: (...) -> bool\n return spam", 90 | 'is_expression': False, 91 | 'tree': typed_ast.ast3.FunctionDef( 92 | 'lovely', typed_ast.ast3.arguments( 93 | [typed_ast.ast3.arg('spam', None, 'bool'), 94 | typed_ast.ast3.arg('eggs', None, 'int'), 95 | typed_ast.ast3.arg('ham', None, 'str')], 96 | None, [], [], None, [ 97 | typed_ast.ast3.NameConstant(value=None), 98 | typed_ast.ast3.NameConstant(value=None)]), 99 | [typed_ast.ast3.Return(typed_ast.ast3.Name('spam', typed_ast.ast3.Load()))], 100 | [], None, '(...) -> bool'), 101 | 'dump': 102 | "FunctionDef(name='lovely',args=arguments(" 103 | "args=[arg(arg='spam',annotation=None,type_comment='bool')," 104 | "arg(arg='eggs',annotation=None,type_comment='int')," 105 | "arg(arg='ham',annotation=None,type_comment='str')]," 106 | "vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[" 107 | "NameConstant(value=None),NameConstant(value=None)])," 108 | "body=[Return(value=Name(id='spam',ctx=Load()))]," 109 | "decorator_list=[],returns=None,type_comment='(...)->bool')"}, 110 | 'function definiton with per-argument type comments and annotations': { 111 | 'code': 112 | "def fun(a, # type: int\n" 113 | " b: float, c # type: str\n" 114 | " ):\n pass", 115 | 'is_expression': False, 116 | 'tree': typed_ast.ast3.FunctionDef( 117 | 'fun', typed_ast.ast3.arguments( 118 | [typed_ast.ast3.arg('a', None, 'int'), typed_ast.ast3.arg( 119 | 'b', typed_ast.ast3.Name(id='float', ctx=typed_ast.ast3.Load()), None), 120 | typed_ast.ast3.arg('c', None, 'str')], 121 | None, [], [], None, []), 122 | [typed_ast.ast3.Pass()], 123 | [], None, None), 124 | 'dump': 125 | "FunctionDef(name='fun',args=arguments(" 126 | "args=[arg(arg='a',annotation=None,type_comment='int')," 127 | "arg(arg='b',annotation=Name(id='float',ctx=Load()),type_comment=None)," 128 | "arg(arg='c',annotation=None,type_comment='str')]," 129 | "vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 130 | "body=[Pass()],decorator_list=[],returns=None,type_comment=None)"}, 131 | 'function definiton with some arguments typed': { 132 | 'code': 133 | "def fun(a, b, # type: float\n" 134 | " c='' # type: str\n" 135 | " ):\n pass", 136 | 'is_expression': False, 137 | 'tree': typed_ast.ast3.FunctionDef( 138 | 'fun', typed_ast.ast3.arguments( 139 | [typed_ast.ast3.arg('a', None, None), typed_ast.ast3.arg('b', None, 'float'), 140 | typed_ast.ast3.arg('c', None, 'str')], 141 | None, [], [], None, [typed_ast.ast3.Str('', '')]), 142 | [typed_ast.ast3.Pass()], 143 | [], None, None), 144 | 'dump': 145 | "FunctionDef(name='fun',args=arguments(" 146 | "args=[arg(arg='a',annotation=None,type_comment=None)," 147 | "arg(arg='b',annotation=None,type_comment='float')," 148 | "arg(arg='c',annotation=None,type_comment='str')]," 149 | "vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[Str(s='',kind='')])," 150 | "body=[Pass()],decorator_list=[],returns=None,type_comment=None)"}, 151 | 'function definiton with some keyword-only arguments typed': { 152 | 'code': 153 | "def fun(a, *, b, # type: float\n" 154 | " c='' # type: str\n" 155 | " ):\n pass", 156 | 'is_expression': False, 157 | 'tree': typed_ast.ast3.FunctionDef( 158 | 'fun', typed_ast.ast3.arguments( 159 | [typed_ast.ast3.arg('a', None, None)], None, 160 | [typed_ast.ast3.arg('b', None, 'float'), typed_ast.ast3.arg('c', None, 'str')], 161 | [None, typed_ast.ast3.Str('', '')], None, []), 162 | [typed_ast.ast3.Pass()], 163 | [], None, None), 164 | 'dump': 165 | "FunctionDef(name='fun',args=arguments(" 166 | "args=[arg(arg='a',annotation=None,type_comment=None)]," 167 | "vararg=None,kwonlyargs=[" 168 | "arg(arg='b',annotation=None,type_comment='float')," 169 | "arg(arg='c',annotation=None,type_comment='str')]," 170 | "kw_defaults=[None,Str(s='',kind='')],kwarg=None,defaults=[])," 171 | "body=[Pass()],decorator_list=[],returns=None,type_comment=None)"}, 172 | 'function definiton with normal and keyword-only arguments typed': { 173 | 'code': 174 | "def fun(a, # type: int\n" 175 | " *args, b, # type: float\n" 176 | " c='', # type: str\n" 177 | " **kwargs):\n pass", 178 | 'is_expression': False, 179 | 'tree': typed_ast.ast3.FunctionDef( 180 | 'fun', typed_ast.ast3.arguments( 181 | [typed_ast.ast3.arg('a', None, 'int')], 182 | typed_ast.ast3.arg(arg='args', annotation=None, type_comment=None), 183 | [typed_ast.ast3.arg('b', None, 'float'), typed_ast.ast3.arg('c', None, 'str')], 184 | [None, typed_ast.ast3.Str('', '')], 185 | typed_ast.ast3.arg(arg='kwargs', annotation=None, type_comment=None), []), 186 | [typed_ast.ast3.Pass()], 187 | [], None, None), 188 | 'dump': 189 | "FunctionDef(name='fun',args=arguments(" 190 | "args=[arg(arg='a',annotation=None,type_comment='int')]," 191 | "vararg=arg(arg='args',annotation=None,type_comment=None),kwonlyargs=[" 192 | "arg(arg='b',annotation=None,type_comment='float')," 193 | "arg(arg='c',annotation=None,type_comment='str')]," 194 | "kw_defaults=[None,Str(s='',kind='')]," 195 | "kwarg=arg(arg='kwargs',annotation=None,type_comment=None),defaults=[])," 196 | "body=[Pass()],decorator_list=[],returns=None,type_comment=None)"}, 197 | 'function definiton with only varargs and per-argument type comment': { 198 | 'code': 199 | "def fun(*args: 'blahblahblah' # type: tuple\n" 200 | " ):\n pass", 201 | 'is_expression': False, 202 | 'tree': typed_ast.ast3.FunctionDef( 203 | 'fun', typed_ast.ast3.arguments( 204 | [], typed_ast.ast3.arg( 205 | arg='args', annotation=typed_ast.ast3.Str('blahblahblah', ''), 206 | type_comment='tuple'), 207 | [], [], None, []), 208 | [typed_ast.ast3.Pass()], 209 | [], None, None), 210 | 'dump': 211 | "FunctionDef(name='fun',args=arguments(" 212 | "args=[],vararg=arg(arg='args',annotation=Str(s='blahblahblah',kind='')," 213 | "type_comment='tuple')," 214 | "kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 215 | "body=[Pass()],decorator_list=[],returns=None,type_comment=None)"}, 216 | 'function definiton with only kwargs and per-argument type comment': { 217 | 'code': 218 | "def fun(**kwargs: 'blahblahblah' # type: dict\n" 219 | " ):\n pass", 220 | 'is_expression': False, 221 | 'tree': typed_ast.ast3.FunctionDef( 222 | 'fun', typed_ast.ast3.arguments( 223 | [], None, [], [], typed_ast.ast3.arg( 224 | arg='kwargs', annotation=typed_ast.ast3.Str('blahblahblah', ''), 225 | type_comment='dict'), []), 226 | [typed_ast.ast3.Pass()], 227 | [], None, None), 228 | 'dump': 229 | "FunctionDef(name='fun',args=arguments(" 230 | "args=[],vararg=None,kwonlyargs=[],kw_defaults=[]," 231 | "kwarg=arg(arg='kwargs',annotation=Str(s='blahblahblah',kind=''),type_comment='dict')," 232 | "defaults=[])," 233 | "body=[Pass()],decorator_list=[],returns=None,type_comment=None)"}, 234 | 'function definiton with last argument not typed': { 235 | 'code': 236 | "def fun(a, b, # type: float\n" 237 | " c):\n pass", 238 | 'is_expression': False, 239 | 'tree': typed_ast.ast3.FunctionDef( 240 | 'fun', typed_ast.ast3.arguments( 241 | [typed_ast.ast3.arg('a', None, None), typed_ast.ast3.arg('b', None, 'float'), 242 | typed_ast.ast3.arg('c', None, None)], 243 | None, [], [], None, []), 244 | [typed_ast.ast3.Pass()], 245 | [], None, None), 246 | 'dump': 247 | "FunctionDef(name='fun',args=arguments(" 248 | "args=[arg(arg='a',annotation=None,type_comment=None)," 249 | "arg(arg='b',annotation=None,type_comment='float')," 250 | "arg(arg='c',annotation=None,type_comment=None)]," 251 | "vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 252 | "body=[Pass()],decorator_list=[],returns=None,type_comment=None)"}, 253 | 'decorated function with type comment': { 254 | 'code': '@deco\ndef do_nothing():\n # type: () -> None\n pass', 255 | 'is_expression': False, 256 | 'tree': typed_ast.ast3.FunctionDef( 257 | name='do_nothing', 258 | args=typed_ast.ast3.arguments([], None, [], [], None, []), 259 | body=[typed_ast.ast3.Pass()], 260 | decorator_list=[typed_ast.ast3.Name(id='deco', ctx=typed_ast.ast3.Load())], 261 | returns=None, type_comment='() -> None'), 262 | 'dump': 263 | "FunctionDef(" 264 | "name='do_nothing'," 265 | "args=arguments(" 266 | "args=[],vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 267 | "body=[Pass()],decorator_list=[Name(id='deco',ctx=Load())]," 268 | "returns=None,type_comment='()->None')"}, 269 | 'function with type comment and annotations': { 270 | 'code': 'def do_nothing() -> None:\n # type: () -> None\n pass', 271 | 'is_expression': False, 272 | 'tree': typed_ast.ast3.FunctionDef( 273 | name='do_nothing', 274 | args=typed_ast.ast3.arguments([], None, [], [], None, []), 275 | body=[typed_ast.ast3.Pass()], decorator_list=[], 276 | returns=typed_ast.ast3.NameConstant(value=None), type_comment='() -> None'), 277 | 'dump': 278 | "FunctionDef(" 279 | "name='do_nothing'," 280 | "args=arguments(" 281 | "args=[],vararg=None,kwonlyargs=[],kw_defaults=[],kwarg=None,defaults=[])," 282 | "body=[Pass()],decorator_list=[]," 283 | "returns=NameConstant(value=None),type_comment='()->None')"}, 284 | 'assignment': { 285 | 'code': "my_string = None", 286 | 'is_expression': False, 287 | 'tree': typed_ast.ast3.Assign( 288 | [typed_ast.ast3.Name('my_string', typed_ast.ast3.Store())], 289 | typed_ast.ast3.NameConstant(None), None), 290 | 'dump': 291 | "Assign(targets=[Name(id='my_string',ctx=Store())],value=NameConstant(value=None)," 292 | "type_comment=None)"}, 293 | 'assignment with type comment': { 294 | 'code': "my_string = None # type: str", 295 | 'is_expression': False, 296 | 'tree': typed_ast.ast3.Assign( 297 | [typed_ast.ast3.Name('my_string', typed_ast.ast3.Store())], 298 | typed_ast.ast3.NameConstant(None), 'str'), 299 | 'dump': 300 | "Assign(targets=[Name(id='my_string',ctx=Store())],value=NameConstant(value=None)," 301 | "type_comment='str')"}, 302 | 'tuple unpacking assignment with type comment': { 303 | 'code': "(my_string, my_int) = my_list # type: str, int", 304 | 'is_expression': False, 305 | 'tree': typed_ast.ast3.Assign( 306 | [typed_ast.ast3.Tuple( 307 | [typed_ast.ast3.Name('my_string', typed_ast.ast3.Store()), 308 | typed_ast.ast3.Name('my_int', typed_ast.ast3.Store())], 309 | typed_ast.ast3.Store())], 310 | typed_ast.ast3.Name('my_list', typed_ast.ast3.Load()), 311 | 'str, int'), 312 | 'dump': 313 | "Assign(" 314 | "targets=[Tuple(elts=[" 315 | "Name(id='my_string',ctx=Store()),Name(id='my_int',ctx=Store())" 316 | "],ctx=Store())]," 317 | "value=Name(id='my_list',ctx=Load())," 318 | "type_comment='str,int')"}, 319 | 'assignment with type annotation': { 320 | 'code': "my_string: str = None", 321 | 'is_expression': False, 322 | 'tree': typed_ast.ast3.AnnAssign( 323 | typed_ast.ast3.Name('my_string', typed_ast.ast3.Store()), 324 | typed_ast.ast3.Name('str', typed_ast.ast3.Load()), 325 | typed_ast.ast3.NameConstant(None), 1), 326 | 'dump': 327 | "AnnAssign(target=Name(id='my_string',ctx=Store())," 328 | "annotation=Name(id='str',ctx=Load()),value=NameConstant(value=None),simple=1)"}, 329 | 'variable declaration with type annotation': { 330 | 'code': "my_string: str", 331 | 'is_expression': False, 332 | 'tree': typed_ast.ast3.AnnAssign( 333 | typed_ast.ast3.Name('my_string', typed_ast.ast3.Store()), 334 | typed_ast.ast3.Name('str', typed_ast.ast3.Load()), None, 1), 335 | 'dump': 336 | "AnnAssign(target=Name(id='my_string',ctx=Store())," 337 | "annotation=Name(id='str',ctx=Load()),value=None,simple=1)"}, 338 | 'for-else loop': { 339 | 'code': "for i in [0, 4, 2, 42]:\n print(i)\nelse:\n print('hmm')", 340 | 'is_expression': False, 341 | 'tree': typed_ast.ast3.For( 342 | typed_ast.ast3.Name('i', typed_ast.ast3.Store()), 343 | typed_ast.ast3.List([ 344 | typed_ast.ast3.Num(0), typed_ast.ast3.Num(4), typed_ast.ast3.Num(2), 345 | typed_ast.ast3.Num(42)], typed_ast.ast3.Load()), 346 | [typed_ast.ast3.Expr(typed_ast.ast3.Call( 347 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 348 | [typed_ast.ast3.Name('i', typed_ast.ast3.Load())], []))], 349 | [typed_ast.ast3.Expr(typed_ast.ast3.Call( 350 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 351 | [typed_ast.ast3.Str('hmm', '')], []))], 352 | None), 353 | 'dump': 354 | "For(" 355 | "target=Name(id='i',ctx=Store())," 356 | "iter=List(elts=[Num(n=0),Num(n=4),Num(n=2),Num(n=42)],ctx=Load())," 357 | "body=[Expr(value=Call(func=Name(id='print',ctx=Load())," 358 | "args=[Name(id='i',ctx=Load())],keywords=[]))]," 359 | "orelse=[Expr(value=Call(func=Name(id='print',ctx=Load())," 360 | "args=[Str(s='hmm',kind='')],keywords=[]))],type_comment=None)"}, 361 | 'for loop with type comment': { 362 | 'code': "for i in [0, 4, 2, 42]: # type: int\n print(i)", 363 | 'is_expression': False, 364 | 'tree': typed_ast.ast3.For( 365 | typed_ast.ast3.Name('i', typed_ast.ast3.Store()), 366 | typed_ast.ast3.List([ 367 | typed_ast.ast3.Num(0), typed_ast.ast3.Num(4), typed_ast.ast3.Num(2), 368 | typed_ast.ast3.Num(42)], typed_ast.ast3.Load()), 369 | [typed_ast.ast3.Expr(typed_ast.ast3.Call( 370 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 371 | [typed_ast.ast3.Name('i', typed_ast.ast3.Load())], []))], 372 | [], 'int'), 373 | 'dump': 374 | "For(" 375 | "target=Name(id='i',ctx=Store())," 376 | "iter=List(elts=[Num(n=0),Num(n=4),Num(n=2),Num(n=42)],ctx=Load())," 377 | "body=[Expr(value=Call(func=Name(id='print',ctx=Load())," 378 | "args=[Name(id='i',ctx=Load())],keywords=[]))]," 379 | "orelse=[],type_comment='int')"}, 380 | 'for-else loop with type comment': { 381 | 'code': "for i in [0, 4, 2, 42]: # type: int\n print(i)\nelse:\n print('hmm')", 382 | 'is_expression': False, 383 | 'tree': typed_ast.ast3.For( 384 | typed_ast.ast3.Name('i', typed_ast.ast3.Store()), 385 | typed_ast.ast3.List([ 386 | typed_ast.ast3.Num(0), typed_ast.ast3.Num(4), typed_ast.ast3.Num(2), 387 | typed_ast.ast3.Num(42)], typed_ast.ast3.Load()), 388 | [typed_ast.ast3.Expr(typed_ast.ast3.Call( 389 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 390 | [typed_ast.ast3.Name('i', typed_ast.ast3.Load())], []))], 391 | [typed_ast.ast3.Expr(typed_ast.ast3.Call( 392 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 393 | [typed_ast.ast3.Str('hmm', '')], []))], 394 | 'int'), 395 | 'dump': 396 | "For(" 397 | "target=Name(id='i',ctx=Store())," 398 | "iter=List(elts=[Num(n=0),Num(n=4),Num(n=2),Num(n=42)],ctx=Load())," 399 | "body=[Expr(value=Call(func=Name(id='print',ctx=Load())," 400 | "args=[Name(id='i',ctx=Load())],keywords=[]))]," 401 | "orelse=[Expr(value=Call(func=Name(id='print',ctx=Load())," 402 | "args=[Str(s='hmm',kind='')],keywords=[]))],type_comment='int')"}, 403 | 'if-elif-else': { 404 | 'code': "if False:\n pass\nelif True:\n pass\nelse:\n pass", 405 | 'is_expression': False, 406 | 'tree': typed_ast.ast3.If( 407 | test=typed_ast.ast3.NameConstant(value=False), 408 | body=[typed_ast.ast3.Pass()], 409 | orelse=[typed_ast.ast3.If( 410 | test=typed_ast.ast3.NameConstant(value=True), body=[typed_ast.ast3.Pass()], 411 | orelse=[typed_ast.ast3.Pass()])]), 412 | 'dump': 413 | "If(" 414 | "test=NameConstant(value=False),body=[Pass()]," 415 | "orelse=[If(test=NameConstant(value=True),body=[Pass()],orelse=[Pass()])]" 416 | ")"}, 417 | 'with statement': { 418 | 'code': "with open('setup.py') as f:\n print(f.read())", 419 | 'is_expression': False, 420 | 'tree': typed_ast.ast3.With( 421 | [typed_ast.ast3.withitem( 422 | typed_ast.ast3.Call( 423 | typed_ast.ast3.Name('open', typed_ast.ast3.Load()), 424 | [typed_ast.ast3.Str('setup.py', '')], []), 425 | typed_ast.ast3.Name('f', typed_ast.ast3.Store()))], 426 | [typed_ast.ast3.Expr(typed_ast.ast3.Call( 427 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 428 | [typed_ast.ast3.Call(typed_ast.ast3.Attribute( 429 | typed_ast.ast3.Name('f', typed_ast.ast3.Load()), 'read', 430 | typed_ast.ast3.Load()), [], [])], 431 | []))], 432 | None), 433 | 'dump': 434 | "With(" 435 | "items=[withitem(context_expr=Call(func=Name(id='open',ctx=Load())," 436 | "args=[Str(s='setup.py',kind='')],keywords=[])," 437 | "optional_vars=Name(id='f',ctx=Store()))]," 438 | "body=[Expr(value=Call(" 439 | "func=Name(id='print',ctx=Load())," 440 | "args=[Call(" 441 | "func=Attribute(value=Name(id='f',ctx=Load()),attr='read',ctx=Load())," 442 | "args=[],keywords=[])]," 443 | "keywords=[]))]," 444 | "type_comment=None)"}, 445 | 'with statement with type comment': { 446 | 'code': "with open('setup.py') as f: # type: typing.io.TextIO\n print(f.read())", 447 | 'is_expression': False, 448 | 'tree': typed_ast.ast3.With( 449 | [typed_ast.ast3.withitem( 450 | typed_ast.ast3.Call( 451 | typed_ast.ast3.Name('open', typed_ast.ast3.Load()), 452 | [typed_ast.ast3.Str('setup.py', '')], []), 453 | typed_ast.ast3.Name('f', typed_ast.ast3.Store()))], 454 | [typed_ast.ast3.Expr(typed_ast.ast3.Call( 455 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 456 | [typed_ast.ast3.Call(typed_ast.ast3.Attribute( 457 | typed_ast.ast3.Name('f', typed_ast.ast3.Load()), 'read', 458 | typed_ast.ast3.Load()), [], [])], 459 | []))], 460 | 'typing.io.TextIO'), 461 | 'dump': 462 | "With(" 463 | "items=[withitem(context_expr=Call(func=Name(id='open',ctx=Load())," 464 | "args=[Str(s='setup.py',kind='')],keywords=[])," 465 | "optional_vars=Name(id='f',ctx=Store()))]," 466 | "body=[Expr(value=Call(" 467 | "func=Name(id='print',ctx=Load())," 468 | "args=[Call(" 469 | "func=Attribute(value=Name(id='f',ctx=Load()),attr='read',ctx=Load())," 470 | "args=[],keywords=[])]," 471 | "keywords=[]))]," 472 | "type_comment='typing.io.TextIO')"}, 473 | 'multi-context with statement with type comment': { 474 | 'code': 475 | "with open('setup.py') as f1, open('README.rst') as f2:" 476 | " # type: typing.io.TextIO, typing.io.TextIO\n" 477 | " print(f1.read())\n print(f2.read())", 478 | 'is_expression': False, 479 | 'tree': typed_ast.ast3.With( 480 | [typed_ast.ast3.withitem( 481 | typed_ast.ast3.Call( 482 | typed_ast.ast3.Name('open', typed_ast.ast3.Load()), 483 | [typed_ast.ast3.Str('setup.py', '')], []), 484 | typed_ast.ast3.Name('f1', typed_ast.ast3.Store())), 485 | typed_ast.ast3.withitem( 486 | typed_ast.ast3.Call( 487 | typed_ast.ast3.Name('open', typed_ast.ast3.Load()), 488 | [typed_ast.ast3.Str('README.rst', '')], []), 489 | typed_ast.ast3.Name('f2', typed_ast.ast3.Store()))], 490 | [typed_ast.ast3.Expr(typed_ast.ast3.Call( 491 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 492 | [typed_ast.ast3.Call(typed_ast.ast3.Attribute( 493 | typed_ast.ast3.Name('f1', typed_ast.ast3.Load()), 'read', 494 | typed_ast.ast3.Load()), [], [])], 495 | [])), 496 | typed_ast.ast3.Expr(typed_ast.ast3.Call( 497 | typed_ast.ast3.Name('print', typed_ast.ast3.Load()), 498 | [typed_ast.ast3.Call(typed_ast.ast3.Attribute( 499 | typed_ast.ast3.Name('f2', typed_ast.ast3.Load()), 'read', 500 | typed_ast.ast3.Load()), [], [])], 501 | []))], 502 | 'typing.io.TextIO, typing.io.TextIO'), 503 | 'dump': 504 | "With(" 505 | "items=[withitem(context_expr=Call(func=Name(id='open',ctx=Load())," 506 | "args=[Str(s='setup.py',kind='')],keywords=[])," 507 | "optional_vars=Name(id='f1',ctx=Store()))," 508 | "withitem(context_expr=Call(func=Name(id='open',ctx=Load())," 509 | "args=[Str(s='README.rst',kind='')],keywords=[])," 510 | "optional_vars=Name(id='f2',ctx=Store()))]," 511 | "body=[Expr(value=Call(" 512 | "func=Name(id='print',ctx=Load())," 513 | "args=[Call(" 514 | "func=Attribute(value=Name(id='f1',ctx=Load()),attr='read',ctx=Load())," 515 | "args=[],keywords=[])]," 516 | "keywords=[]))," 517 | "Expr(value=Call(" 518 | "func=Name(id='print',ctx=Load())," 519 | "args=[Call(" 520 | "func=Attribute(value=Name(id='f2',ctx=Load()),attr='read',ctx=Load())," 521 | "args=[],keywords=[])]," 522 | "keywords=[]))]," 523 | "type_comment='typing.io.TextIO,typing.io.TextIO')"}, 524 | 'raw bytes': { 525 | 'code': "br'''1\"\"\"2\'3\"4\\'\\'\\'\\n'''", 526 | 'is_expression': True, 527 | 'tree': typed_ast.ast3.Bytes(b'1\"\"\"2\'3\"4\\\'\\\'\\\'\\n', 'br'), 528 | 'dump': "Bytes(s=b'1\"\"\"2\\'3\"4\\\\\\'\\\\\\'\\\\\\'\\\\n',kind='br')"}, 529 | 'raw string': { 530 | 'code': "r'spam'", 531 | 'is_expression': True, 532 | 'tree': typed_ast.ast3.Str('spam', 'r'), 533 | 'dump': "Str(s='spam',kind='r')"}, 534 | 'raw string variant': { 535 | 'code': "R'spam'", 536 | 'is_expression': True, 537 | 'tree': typed_ast.ast3.Str('spam', 'R'), 538 | 'dump': "Str(s='spam',kind='R')"}, 539 | 'unicode string': { 540 | 'code': "u'spam'", 541 | 'is_expression': True, 542 | 'tree': typed_ast.ast3.Str('spam', 'u'), 543 | 'dump': "Str(s='spam',kind='u')"}, 544 | 'addition': { 545 | 'code': "(a + b)", 546 | 'is_expression': True, 547 | 'tree': typed_ast.ast3.BinOp( 548 | typed_ast.ast3.Name('a', typed_ast.ast3.Load()), typed_ast.ast3.Add(), 549 | typed_ast.ast3.Name('b', typed_ast.ast3.Load())), 550 | 'dump': 551 | "BinOp(left=Name(id='a',ctx=Load()),op=Add(),right=Name(id='b',ctx=Load()))"}, 552 | 'attribute of integer literal': { 553 | 'code': "3 .__abs__()", 554 | 'is_expression': True, 555 | 'tree': typed_ast.ast3.Call( 556 | func=typed_ast.ast3.Attribute( 557 | value=typed_ast.ast3.Num(n=3), 558 | attr='__abs__', ctx=typed_ast.ast3.Load()), 559 | args=[], keywords=[]), 560 | 'dump': 561 | "Call(func=Attribute(value=Num(n=3),attr='__abs__',ctx=Load()),args=[],keywords=[])"}} 562 | 563 | UNVERIFIED_EXAMPLES = { 564 | 'assignment with type comment stored as AST': { 565 | 'code': "my_string = None # type: str", 566 | 'is_expression': False, 567 | 'tree': typed_ast.ast3.Assign( 568 | [typed_ast.ast3.Name('my_string', typed_ast.ast3.Store())], 569 | typed_ast.ast3.NameConstant(None), typed_ast.ast3.Name('str', typed_ast.ast3.Load())), 570 | 'dump': 571 | "Assign(targets=[Name(id='my_string',ctx=Store())],value=NameConstant(value=None)," 572 | "type_comment=Name(id='str',ctx=Load()))"}, 573 | } 574 | 575 | # 'raw bytes': { 576 | # 'code': "rb'spam'", 577 | # 'is_expression': True, 578 | # 'tree': typed_ast.ast3.Bytes(b'spam'), 579 | # 'dump': "Bytes(s='spam')"}, 580 | # 'raw bytes inverted prefix': { 581 | # 'code': "br'spam'", 582 | # 'is_expression': True, 583 | # 'tree': typed_ast.ast3.Bytes(b'spam'), 584 | # 'dump': "Bytes(s='spam')"}, 585 | 586 | # 'f-string': { 587 | # 'code': """f'len("lalala")={6}{42:0}{3.1415:2f}{3.1415:"{2}f"}'""", 588 | # 'is_expression': True, 589 | # 'tree': typed_ast.ast3.JoinedStr([ 590 | # typed_ast.ast3.Str('len("lalala")='), 591 | # typed_ast.ast3.FormattedValue(typed_ast.ast3.Num(6), -1, None), 592 | # typed_ast.ast3.FormattedValue(typed_ast.ast3.Num(42), -1, typed_ast.ast3.Num(0)), 593 | # typed_ast.ast3.FormattedValue(typed_ast.ast3.Num(3.1415), -1, typed_ast.ast3.Str('2f')), 594 | # typed_ast.ast3.FormattedValue(typed_ast.ast3.Num(3.1415), -1, typed_ast.ast3.JoinedStr([ 595 | # typed_ast.ast3.Str('"'), 596 | # typed_ast.ast3.FormattedValue(typed_ast.ast3.Num(2), -1, None), 597 | # typed_ast.ast3.Str('f"')]))]), 598 | # 'dump': 599 | # "JoinedStr(values=[" 600 | # """Str(s='len("lalala")='),""" 601 | # "FormattedValue(value=Num(n=6),conversion=-1,format_spec=None)," 602 | # "FormattedValue(value=Num(n=42),conversion=-1,format_spec=Str(s='0'))," 603 | # "FormattedValue(value=Num(n=3.1415),conversion=-1,format_spec=Str(s='2f'))," 604 | # "FormattedValue(value=Num(n=3.1415),conversion=-1,format_spec=JoinedStr(values=[" 605 | # """Str(s='"'),FormattedValue(value=Num(n=2),conversion=-1,format_spec=None),""" 606 | # """Str(s='f"')]))])"""} 607 | 608 | INVALID_EXAMPLES = { 609 | 'chained assignment with type annotation': { 610 | 'code': "my_string: str = my_string2 = None", 611 | 'is_expression': False, 612 | 'tree': typed_ast.ast3.AnnAssign( 613 | [typed_ast.ast3.Name('my_string', typed_ast.ast3.Store()), 614 | typed_ast.ast3.Name('my_string2', typed_ast.ast3.Store())], 615 | typed_ast.ast3.NameConstant(None), 616 | typed_ast.ast3.Name('str', typed_ast.ast3.Load()), True), 617 | 'dump': "None"}, 618 | 'tuple unpacking assignment with one type annotation': { 619 | 'code': "my_string, my_string2: str = my_tuple", 620 | 'is_expression': False, 621 | 'tree': typed_ast.ast3.AnnAssign( 622 | [typed_ast.ast3.Tuple( 623 | [typed_ast.ast3.Name('my_string', typed_ast.ast3.Store()), 624 | typed_ast.ast3.Name('my_string2', typed_ast.ast3.Store())], 625 | typed_ast.ast3.Store())], 626 | typed_ast.ast3.Name('my_tuple', typed_ast.ast3.Load()), 627 | typed_ast.ast3.Name('str', typed_ast.ast3.Load()), True), 628 | 'dump': "None"}, 629 | 'tuple unpacking assignment with multiple type annotations': { 630 | 'code': "my_string: str, my_string2: str = my_tuple", 631 | 'is_expression': False, 632 | 'tree': typed_ast.ast3.AnnAssign( 633 | [typed_ast.ast3.Tuple( 634 | [typed_ast.ast3.Name('my_string', typed_ast.ast3.Store()), 635 | typed_ast.ast3.Name('my_string2', typed_ast.ast3.Store())], 636 | typed_ast.ast3.Store())], 637 | typed_ast.ast3.Name('my_tuple', typed_ast.ast3.Load()), 638 | typed_ast.ast3.Name('str', typed_ast.ast3.Load()), True), 639 | 'dump': "None"}, 640 | 'assignment with type comment and annotation': { 641 | 'code': "my_string: str = None # type: str", 642 | 'is_expression': False, 643 | 'tree': typed_ast.ast3.AnnAssign( 644 | [typed_ast.ast3.Name('my_string', typed_ast.ast3.Store())], 645 | typed_ast.ast3.NameConstant(None), 646 | typed_ast.ast3.Name('str', typed_ast.ast3.Load()), True), 647 | 'dump': "None"} 648 | } 649 | 650 | 651 | def _generate_variants(example: dict): 652 | if example['is_expression']: 653 | example['trees'] = { 654 | 'exec': typed_ast.ast3.Module([typed_ast.ast3.Expr(example['tree'])], []), 655 | 'eval': typed_ast.ast3.Expression(example['tree']), 656 | 'single': typed_ast.ast3.Interactive([typed_ast.ast3.Expr(example['tree'])])} 657 | example['dumps'] = { 658 | 'exec': 'Module(body=[Expr(value={})],type_ignores=[])'.format(example['dump']), 659 | 'eval': 'Expression(body={})'.format(example['dump']), 660 | 'single': 'Interactive(body=[Expr(value={})])'.format(example['dump'])} 661 | else: 662 | example['trees'] = { 663 | 'exec': typed_ast.ast3.Module([example['tree']], []), 664 | 'eval': None, 665 | 'single': typed_ast.ast3.Interactive([example['tree']])} 666 | example['dumps'] = { 667 | 'exec': 'Module(body=[{}],type_ignores=[])'.format(example['dump']), 668 | 'eval': None, 669 | 'single': 'Interactive(body=[{}])'.format(example['dump'])} 670 | 671 | 672 | for _, _example in EXAMPLES.items(): 673 | _generate_variants(_example) 674 | 675 | for _, _example in UNVERIFIED_EXAMPLES.items(): 676 | _generate_variants(_example) 677 | 678 | for _, _example in INVALID_EXAMPLES.items(): 679 | _generate_variants(_example) 680 | 681 | # verify examples 682 | if __debug__: 683 | _MSG = '''example for {} in '{}' mode is incorrect: 684 | """ 685 | {} 686 | """ 687 | tree from source (above) != example tree (below) 688 | """ 689 | {} 690 | """''' 691 | for _description, _example in EXAMPLES.items(): 692 | for mode in MODES: 693 | if _example['trees'][mode] is None: 694 | try: 695 | tree_from_source = typed_ast.ast3.parse( 696 | source=_example['code'], filename='', mode=mode) 697 | except SyntaxError: 698 | tree_from_source = None 699 | example_tree = None 700 | else: 701 | tree_from_source = typed_astunparse.dump( 702 | typed_ast.ast3.parse(source=_example['code'], filename='', mode=mode) 703 | ).replace('\n', '').replace(' ', '') 704 | example_tree = typed_astunparse.dump( 705 | _example['trees'][mode]).replace('\n', '').replace(' ', '') 706 | assert tree_from_source == example_tree, _MSG.format( 707 | _description, mode, tree_from_source, example_tree) 708 | 709 | _ROOT_DIRECTORY_PARTS = [getattr(sys, 'real_prefix', sys.prefix), 'lib'] 710 | if platform.system() != 'Windows': 711 | _ROOT_DIRECTORY_PARTS.append('python{}.{}'.format(*sys.version_info[:2])) 712 | 713 | _ROOT_DIRECTORY = os.path.join(*_ROOT_DIRECTORY_PARTS) 714 | 715 | # verify root directory 716 | if __debug__: 717 | assert isinstance(_ROOT_DIRECTORY, str), _ROOT_DIRECTORY 718 | assert _ROOT_DIRECTORY 719 | assert os.path.isdir(_ROOT_DIRECTORY), _ROOT_DIRECTORY 720 | 721 | PATHS = sorted([ 722 | os.path.join(_ROOT_DIRECTORY, n) 723 | for n in os.listdir(_ROOT_DIRECTORY) 724 | if n.endswith('.py') and not n.startswith('bad')]) 725 | 726 | # verify found paths 727 | if __debug__: 728 | # On Ubuntu, Python built from source: 169 in 3.5.2, 170 in 3.6.0 729 | assert len(PATHS) > 150, len(PATHS) 730 | -------------------------------------------------------------------------------- /test/test_dump.py: -------------------------------------------------------------------------------- 1 | """Tested function: dump.""" 2 | 3 | import ast 4 | import logging 5 | import unittest 6 | 7 | import astunparse 8 | import typed_ast.ast3 9 | import typed_astunparse 10 | 11 | from .examples import MODES, EXAMPLES, PATHS 12 | 13 | _LOG = logging.getLogger(__name__) 14 | 15 | 16 | def _postprocess_dump(tested_typed_dump): 17 | 18 | lines = [s.strip() for s in tested_typed_dump.splitlines()] 19 | lines = [s + ' ' if s.endswith(',') else s for s in lines] 20 | return ''.join(lines) 21 | 22 | 23 | class DumpTests(unittest.TestCase): 24 | 25 | """Unit tests for dump() function.""" 26 | 27 | maxDiff = None 28 | 29 | def test_dump_examples(self): 30 | """Print ASTs of examples correctly.""" 31 | for description, example in EXAMPLES.items(): 32 | for mode in MODES: 33 | if example['trees'][mode] is None: 34 | continue 35 | dump = typed_astunparse.dump(example['trees'][mode]) 36 | _LOG.debug('%s', dump) 37 | dump = dump.replace('\n', '').replace(' ', '') 38 | self.assertEqual(dump, example['dumps'][mode], msg=(description, mode)) 39 | 40 | def test_dump_files_comparison(self): 41 | """Print the same data as other existing modules.""" 42 | for path in PATHS: 43 | with open(path, 'r', encoding='utf-8') as py_file: 44 | code = py_file.read() 45 | 46 | untyped_tree = ast.parse(source=code, filename=path) 47 | untyped_dump = astunparse.dump(untyped_tree) 48 | tested_untyped_dump = typed_astunparse.dump(untyped_tree) 49 | 50 | self.assertEqual(untyped_dump.splitlines(), tested_untyped_dump.splitlines()) 51 | 52 | typed_tree = typed_ast.ast3.parse(source=code, filename=path) 53 | bad_typed_dump = astunparse.dump(typed_tree) 54 | 55 | for annotate_fields in [True, False]: 56 | for include_attributes in [False, True]: 57 | if include_attributes and not annotate_fields: 58 | continue # behaviour differs from typed_ast 59 | 60 | with self.assertRaises(TypeError): 61 | _ = typed_ast.ast3.dump( 62 | untyped_tree, annotate_fields=annotate_fields, 63 | include_attributes=include_attributes) 64 | 65 | typed_dump = typed_ast.ast3.dump( 66 | typed_tree, annotate_fields=annotate_fields, 67 | include_attributes=include_attributes) 68 | tested_typed_dump = _postprocess_dump(typed_astunparse.dump( 69 | typed_tree, annotate_fields=annotate_fields, 70 | include_attributes=include_attributes)) 71 | 72 | if include_attributes: 73 | # because of https://github.com/python/typed_ast/issues/23 74 | self.assertEqual( 75 | typed_dump.replace(' ', ''), tested_typed_dump.replace(' ', '')) 76 | continue 77 | self.assertNotEqual(untyped_dump, bad_typed_dump) 78 | self.assertNotEqual(typed_dump, bad_typed_dump) 79 | self.assertEqual(typed_dump, tested_typed_dump) 80 | 81 | def test_many_dump_roundtrips(self): 82 | """Preserve ASTs after unparse(parse(...unparse(parse(dump(tree)))...)).""" 83 | for description, example in EXAMPLES.items(): 84 | for mode in MODES: 85 | if example['trees'][mode] is None: 86 | continue 87 | 88 | dump = typed_astunparse.dump(example['trees'][mode]) 89 | for _ in range(4): 90 | tree = typed_ast.ast3.parse(source=dump, mode=mode) 91 | dump = typed_astunparse.unparse(tree) 92 | _LOG.debug('%s', dump) 93 | clean_dump = dump.replace('\n', '').replace(' ', '') 94 | self.assertEqual(clean_dump, example['dumps'][mode], msg=(description, mode)) 95 | # TODO: use tree equality comparison below 96 | # tree = typed_ast.ast3.parse(source=dump, mode=mode) 97 | # self.assertTrue(typed_astunparse.equal(tree, example['trees'][mode])) 98 | -------------------------------------------------------------------------------- /test/test_setup.py: -------------------------------------------------------------------------------- 1 | """Tests for setup scripts.""" 2 | 3 | import importlib 4 | import itertools 5 | import os 6 | import pathlib 7 | import runpy 8 | import subprocess 9 | import sys 10 | import tempfile 11 | import types 12 | import typing as t 13 | import unittest 14 | 15 | __updated__ = '2019-06-04' 16 | 17 | 18 | def run_program(*args, glob: bool = False): 19 | """Run subprocess with given args. Use path globbing for each arg that contains an asterisk.""" 20 | if glob: 21 | cwd = pathlib.Path.cwd() 22 | args = tuple(itertools.chain.from_iterable( 23 | list(str(_.relative_to(cwd)) for _ in cwd.glob(arg)) if '*' in arg else [arg] 24 | for arg in args)) 25 | process = subprocess.Popen(args) 26 | process.wait() 27 | if process.returncode != 0: 28 | raise AssertionError('execution of {} returned {}'.format(args, process.returncode)) 29 | return process 30 | 31 | 32 | def run_pip(*args, **kwargs): 33 | python_exec_name = pathlib.Path(sys.executable).name 34 | pip_exec_name = python_exec_name.replace('python', 'pip') 35 | run_program(pip_exec_name, *args, **kwargs) 36 | 37 | 38 | def run_module(name: str, *args, run_name: str = '__main__') -> None: 39 | backup_sys_argv = sys.argv 40 | sys.argv = [name + '.py'] + list(args) 41 | runpy.run_module(name, run_name=run_name) 42 | sys.argv = backup_sys_argv 43 | 44 | 45 | def import_module(name: str = 'setup') -> types.ModuleType: 46 | setup_module = importlib.import_module(name) 47 | return setup_module 48 | 49 | 50 | def import_module_member(module_name: str, member_name: str) -> t.Any: 51 | module = import_module(module_name) 52 | return getattr(module, member_name) 53 | 54 | 55 | CLASSIFIERS_LICENSES = ( 56 | 'License :: OSI Approved :: Python License (CNRI Python License)', 57 | 'License :: OSI Approved :: Python Software Foundation License', 58 | 'License :: Other/Proprietary License', 59 | 'License :: Public Domain') 60 | 61 | CLASSIFIERS_PYTHON_VERSIONS = tuple("""Programming Language :: Python 62 | Programming Language :: Python :: 2 63 | Programming Language :: Python :: 2.2 64 | Programming Language :: Python :: 2.7 65 | Programming Language :: Python :: 2 :: Only 66 | Programming Language :: Python :: 3 67 | Programming Language :: Python :: 3.0 68 | Programming Language :: Python :: 3.5 69 | Programming Language :: Python :: 3 :: Only""".splitlines()) 70 | 71 | CLASSIFIERS_PYTHON_IMPLEMENTATIONS = tuple("""Programming Language :: Python :: Implementation 72 | Programming Language :: Python :: Implementation :: CPython 73 | Programming Language :: Python :: Implementation :: Jython 74 | Programming Language :: Python :: Implementation :: PyPy 75 | Programming Language :: Python :: Implementation :: Stackless""".splitlines()) 76 | 77 | CLASSIFIERS_VARIOUS = ( 78 | 'Framework :: IPython', 79 | 'Topic :: Scientific/Engineering', 80 | 'Topic :: Sociology', 81 | 'Topic :: Security :: Cryptography', 82 | 'Topic :: Software Development :: Libraries :: Python Modules', 83 | 'Topic :: Software Development :: Version Control :: Git', 84 | 'Topic :: System', 85 | 'Topic :: Utilities') 86 | 87 | CLASSIFIERS_LICENSES_TUPLES = tuple((_,) for _ in CLASSIFIERS_LICENSES) + ((),) 88 | 89 | CLASSIFIERS_PYTHON_VERSIONS_COMBINATIONS = tuple((_,) for _ in CLASSIFIERS_PYTHON_VERSIONS) 90 | 91 | CLASSIFIERS_PYTHON_IMPLEMENTATIONS_TUPLES = tuple((_,) for _ in CLASSIFIERS_PYTHON_IMPLEMENTATIONS) 92 | 93 | CLASSIFIERS_VARIOUS_COMBINATIONS = tuple(itertools.combinations( 94 | CLASSIFIERS_VARIOUS, len(CLASSIFIERS_VARIOUS) - 1)) + (CLASSIFIERS_VARIOUS,) 95 | 96 | ALL_CLASSIFIERS_VARIANTS = [ 97 | licenses + versions + implementations + various 98 | for licenses in CLASSIFIERS_LICENSES_TUPLES 99 | for versions in CLASSIFIERS_PYTHON_VERSIONS_COMBINATIONS 100 | for implementations in CLASSIFIERS_PYTHON_IMPLEMENTATIONS_TUPLES 101 | for various in CLASSIFIERS_VARIOUS_COMBINATIONS] 102 | 103 | LINK_EXAMPLES = [ 104 | (None, 'setup.py', True), ('this file', 'setup.py', True), (None, 'test/test_setup.py', True), 105 | (None, 'http://site.com', False), (None, '../something/else', False), (None, 'no.thing', False), 106 | (None, '/my/abs/path', False)] 107 | 108 | 109 | def get_package_folder_name(): 110 | """Attempt to guess the built package name.""" 111 | cwd = pathlib.Path.cwd() 112 | directories = [ 113 | path for path in cwd.iterdir() if pathlib.Path(cwd, path).is_dir() 114 | and pathlib.Path(cwd, path, '__init__.py').is_file() and path.name != 'test'] 115 | assert len(directories) == 1, directories 116 | return directories[0].name 117 | 118 | 119 | class UnitTests(unittest.TestCase): 120 | """Test basic functionalities of the setup boilerplate.""" 121 | 122 | def test_find_version(self): 123 | find_version = import_module_member('setup_boilerplate', 'find_version') 124 | result = find_version(get_package_folder_name()) 125 | self.assertIsInstance(result, str) 126 | 127 | def test_find_packages(self): 128 | find_packages = import_module_member('setup_boilerplate', 'find_packages') 129 | results = find_packages() 130 | self.assertIsInstance(results, list) 131 | for result in results: 132 | self.assertIsInstance(result, str) 133 | 134 | def test_requirements(self): 135 | parse_requirements = import_module_member('setup_boilerplate', 'parse_requirements') 136 | results = parse_requirements() 137 | self.assertIsInstance(results, list) 138 | self.assertTrue(all(isinstance(result, str) for result in results), msg=results) 139 | 140 | def test_requirements_empty(self): 141 | parse_requirements = import_module_member('setup_boilerplate', 'parse_requirements') 142 | reqs_file = tempfile.NamedTemporaryFile('w', delete=False) 143 | reqs_file.close() 144 | results = parse_requirements(reqs_file.name) 145 | self.assertIsInstance(results, list) 146 | self.assertEqual(len(results), 0) 147 | os.remove(reqs_file.name) 148 | 149 | def test_requirements_comments(self): 150 | parse_requirements = import_module_member('setup_boilerplate', 'parse_requirements') 151 | reqs = ['# comment', 'numpy', '', '# another comment', 'scipy', '', '# one more comment'] 152 | reqs_file = tempfile.NamedTemporaryFile('w', delete=False) 153 | for req in reqs: 154 | print(req, file=reqs_file) 155 | reqs_file.close() 156 | results = parse_requirements(reqs_file.name) 157 | self.assertIsInstance(results, list) 158 | self.assertGreater(len(results), 0) 159 | self.assertLess(len(results), len(reqs)) 160 | os.remove(reqs_file.name) 161 | 162 | def test_python_versions(self): 163 | find_required_python_version = import_module_member( 164 | 'setup_boilerplate', 'find_required_python_version') 165 | for variant in ALL_CLASSIFIERS_VARIANTS: 166 | with self.subTest(variant=variant): 167 | result = find_required_python_version(variant) 168 | if result is not None: 169 | self.assertIsInstance(result, str) 170 | 171 | def test_python_versions_combined(self): 172 | find_required_python_version = import_module_member( 173 | 'setup_boilerplate', 'find_required_python_version') 174 | classifiers = [ 175 | 'Programming Language :: Python :: 3 :: Only', 176 | 'Programming Language :: Python :: 3.5'] 177 | req = find_required_python_version(classifiers) 178 | self.assertEqual(req, '>=3.5') 179 | 180 | def test_python_versions_reversed(self): 181 | find_required_python_version = import_module_member( 182 | 'setup_boilerplate', 'find_required_python_version') 183 | classifiers = [ 184 | 'Programming Language :: Python :: 3.4', 185 | 'Programming Language :: Python :: 3.5', 186 | 'Programming Language :: Python :: 3.6'] 187 | req = find_required_python_version(classifiers) 188 | self.assertEqual(req, '>=3.4') 189 | req = find_required_python_version(reversed(classifiers)) 190 | self.assertEqual(req, '>=3.4') 191 | 192 | def test_python_versions_none(self): 193 | find_required_python_version = import_module_member( 194 | 'setup_boilerplate', 'find_required_python_version') 195 | result = find_required_python_version([]) 196 | self.assertIsNone(result) 197 | 198 | def test_python_versions_many_only(self): 199 | find_required_python_version = import_module_member( 200 | 'setup_boilerplate', 'find_required_python_version') 201 | classifiers = [ 202 | 'Programming Language :: Python :: 2 :: Only', 203 | 'Programming Language :: Python :: 3 :: Only'] 204 | with self.assertRaises(ValueError): 205 | find_required_python_version(classifiers) 206 | 207 | def test_python_versions_conflict(self): 208 | find_required_python_version = import_module_member( 209 | 'setup_boilerplate', 'find_required_python_version') 210 | classifier_variants = [ 211 | ['Programming Language :: Python :: 2.7', 212 | 'Programming Language :: Python :: 3 :: Only'], 213 | ['Programming Language :: Python :: 2 :: Only', 214 | 'Programming Language :: Python :: 3.0']] 215 | for classifiers in classifier_variants: 216 | with self.assertRaises(ValueError): 217 | find_required_python_version(classifiers) 218 | 219 | 220 | class PackageTests(unittest.TestCase): 221 | 222 | """Test methods of Package class.""" 223 | 224 | def test_try_fields(self): 225 | package = import_module_member('setup_boilerplate', 'Package') 226 | 227 | class Package(package): # pylint: disable=too-few-public-methods 228 | name = 'package name' 229 | description = 'package description' 230 | self.assertEqual(Package.try_fields('name', 'description'), 'package name') 231 | self.assertEqual(Package.try_fields('bad_field', 'description'), 'package description') 232 | with self.assertRaises(AttributeError): 233 | self.assertIsNone(Package.try_fields()) 234 | with self.assertRaises(AttributeError): 235 | Package.try_fields('bad_field', 'another_bad_field') 236 | 237 | def test_parse_readme(self): 238 | package = import_module_member('setup_boilerplate', 'Package') 239 | 240 | class Package(package): # pylint: disable=too-few-public-methods 241 | name = 'package name' 242 | description = 'package description' 243 | version = '1.2.3.4' 244 | url = 'https://github.com/example' 245 | 246 | with tempfile.NamedTemporaryFile('w', suffix='.md', delete=False) as temp_file: 247 | temp_file.write('test test test') 248 | result, content_type = Package.parse_readme(temp_file.name) 249 | os.remove(temp_file.name) 250 | self.assertIsInstance(result, str) 251 | self.assertIsInstance(content_type, str) 252 | 253 | prefix = 'https://github.com/example/blob/v1.2.3.4/' 254 | for name, link, done in LINK_EXAMPLES: 255 | name = '' if name is None else name + ' ' 256 | text = 'Please see `{}<{}>`_ for details.'.format(name, link) 257 | with tempfile.NamedTemporaryFile('w', suffix='.rst', delete=False) as temp_file: 258 | temp_file.write(text) 259 | result, content_type = Package.parse_readme(temp_file.name) 260 | os.remove(temp_file.name) 261 | self.assertIsInstance(result, str) 262 | self.assertIsInstance(content_type, str) 263 | if not done: 264 | self.assertEqual(result, text) 265 | continue 266 | if name == '': 267 | name = link + ' ' 268 | self.assertIn('`{}<{}{}>`_'.format(name, prefix, link), result) 269 | 270 | def test_prepare(self): 271 | package = import_module_member('setup_boilerplate', 'Package') 272 | 273 | version_ = '1.2.3.4.5.6.7' 274 | long_description_ = 'long package description' 275 | 276 | class Package(package): # pylint: disable=too-few-public-methods, missing-docstring 277 | name = 'package name' 278 | version = version_ 279 | description = 'package description' 280 | long_description = long_description_ 281 | packages = [] 282 | install_requires = [] 283 | python_requires = '' 284 | 285 | self.assertEqual(Package.version, version_) 286 | self.assertEqual(Package.long_description, long_description_) 287 | Package.prepare() 288 | self.assertEqual(Package.version, version_) 289 | self.assertEqual(Package.long_description, long_description_) 290 | 291 | Package.long_description = None 292 | Package.packages = None 293 | Package.install_requires = None 294 | Package.python_requires = None 295 | Package.prepare() 296 | 297 | Package.version = None 298 | with self.assertRaises(FileNotFoundError): 299 | Package.prepare() 300 | 301 | 302 | @unittest.skipUnless(os.environ.get('TEST_PACKAGING') or os.environ.get('CI'), 303 | 'skipping packaging tests for actual package') 304 | class IntergrationTests(unittest.TestCase): 305 | 306 | """Test if the boilerplate can actually create a valid package.""" 307 | 308 | pkg_name = get_package_folder_name() 309 | 310 | def test_build_binary(self): 311 | run_module('setup', 'bdist') 312 | self.assertTrue(os.path.isdir('dist')) 313 | 314 | def test_build_wheel(self): 315 | run_module('setup', 'bdist_wheel') 316 | self.assertTrue(os.path.isdir('dist')) 317 | 318 | def test_build_source(self): 319 | run_module('setup', 'sdist', '--formats=gztar,zip') 320 | self.assertTrue(os.path.isdir('dist')) 321 | 322 | def test_install_code(self): 323 | run_pip('install', '.') 324 | run_pip('uninstall', '-y', self.pkg_name) 325 | 326 | def test_install_source_tar(self): 327 | find_version = import_module_member('setup_boilerplate', 'find_version') 328 | version = find_version(self.pkg_name) 329 | run_pip('install', 'dist/*-{}.tar.gz'.format(version), glob=True) 330 | run_pip('uninstall', '-y', self.pkg_name) 331 | 332 | def test_install_source_zip(self): 333 | find_version = import_module_member('setup_boilerplate', 'find_version') 334 | version = find_version(self.pkg_name) 335 | run_pip('install', 'dist/*-{}.zip'.format(version), glob=True) 336 | run_pip('uninstall', '-y', self.pkg_name) 337 | 338 | def test_install_wheel(self): 339 | find_version = import_module_member('setup_boilerplate', 'find_version') 340 | version = find_version(self.pkg_name) 341 | run_pip('install', 'dist/*-{}-*.whl'.format(version), glob=True) 342 | run_pip('uninstall', '-y', self.pkg_name) 343 | 344 | def test_pip_error(self): 345 | with self.assertRaises(AssertionError): 346 | run_pip('wrong_pip_command') 347 | 348 | def test_setup_do_nothing_or_error(self): 349 | run_module('setup', 'wrong_setup_command', run_name='__not_main__') 350 | with self.assertRaises(SystemExit): 351 | run_module('setup', 'wrong_setup_command') 352 | -------------------------------------------------------------------------------- /test/test_unparse.py: -------------------------------------------------------------------------------- 1 | """Tested function: unparse.""" 2 | 3 | import ast 4 | import itertools 5 | import logging 6 | import pathlib 7 | import unittest 8 | import sys 9 | 10 | import typed_ast.ast3 11 | import typed_astunparse 12 | 13 | from .examples import MODES, EXAMPLES, UNVERIFIED_EXAMPLES, INVALID_EXAMPLES, PATHS 14 | 15 | _LOG = logging.getLogger(__name__) 16 | 17 | 18 | class UnparseTests(unittest.TestCase): 19 | 20 | """Unit tests for unparse() function.""" 21 | 22 | maxDiff = None 23 | 24 | def test_unparse_examples(self): 25 | """Unparse ASTs of examples correctly.""" 26 | for description, example in itertools.chain(EXAMPLES.items(), UNVERIFIED_EXAMPLES.items()): 27 | for mode in MODES: 28 | if example['trees'][mode] is None: 29 | continue 30 | with self.subTest(description=description): 31 | code = typed_astunparse.unparse(example['trees'][mode]) 32 | _LOG.debug('%s', code) 33 | code = code.strip() 34 | self.assertEqual(code, example['code'], msg=(description, mode)) 35 | 36 | def test_unparse_invalid_examples(self): 37 | """Raise errors on ASTs of invalid examples as expected.""" 38 | for description, example in INVALID_EXAMPLES.items(): 39 | for mode in MODES: 40 | if example['trees'][mode] is None: 41 | continue 42 | with self.assertRaises(SyntaxError, msg=(description, mode)): 43 | typed_ast.ast3.parse(source=example['code'], mode=mode) 44 | 45 | code = typed_astunparse.unparse(example['trees'][mode]) 46 | tree = None 47 | try: 48 | tree = typed_ast.ast3.parse(source=code, mode=mode) 49 | except SyntaxError: 50 | continue 51 | code = typed_astunparse.unparse(tree) 52 | 53 | def test_bad_raw_literal(self): 54 | raw_literal = rb'''\t\t ' """ ''' + rb""" " ''' \n""" 55 | tree = typed_ast.ast3.Bytes(raw_literal, 'rb') 56 | code = typed_astunparse.unparse(tree) 57 | _LOG.debug('%s', code) 58 | self.assertNotEqual(raw_literal, code) 59 | for mode in MODES: 60 | tree = typed_ast.ast3.parse(source=code, mode=mode) 61 | 62 | def test_many_roundtrips(self): 63 | """Prserve ASTs when doing parse(unparse(parse(...unparse(parse(code))...))).""" 64 | for description, example in EXAMPLES.items(): 65 | for mode in MODES: 66 | if example['trees'][mode] is None: 67 | continue 68 | 69 | tree = example['trees'][mode] 70 | with self.subTest(description=description): 71 | for _ in range(4): 72 | code = typed_astunparse.unparse(tree) 73 | _LOG.debug('%s', code) 74 | clean_code = code.strip() 75 | self.assertEqual(clean_code, example['code'], msg=(description, mode)) 76 | tree = typed_ast.ast3.parse(source=code, mode=mode) 77 | 78 | def test_files(self): 79 | """Keep Python stdlib tree the same after roundtrip parse-unparse.""" 80 | for path in PATHS: 81 | if sys.version_info[:2] == (3, 7) and pathlib.Path(path).name == 'dataclasses.py': 82 | continue 83 | with open(path, 'r', encoding='utf-8') as py_file: 84 | original_code = py_file.read() 85 | tree = typed_ast.ast3.parse(source=original_code, filename=path) 86 | code = typed_astunparse.unparse(tree) 87 | with self.subTest(path=path): 88 | roundtrip_tree = typed_ast.ast3.parse(source=code) 89 | tree_dump = typed_ast.ast3.dump(tree, include_attributes=False) 90 | roundtrip_tree_dump = typed_ast.ast3.dump(roundtrip_tree, include_attributes=False) 91 | self.assertEqual(tree_dump, roundtrip_tree_dump, msg=path) 92 | 93 | def test_untyped_files(self): 94 | """Unparse Python stdlib correctly even if parsed using built-in ast package.""" 95 | for path in PATHS: 96 | with open(path, 'r', encoding='utf-8') as py_file: 97 | original_code = py_file.read() 98 | tree = ast.parse(source=original_code, filename=path) 99 | code = typed_astunparse.unparse(tree) 100 | with self.subTest(path=path): 101 | roundtrip_tree = ast.parse(source=code) 102 | tree_dump = ast.dump(tree, include_attributes=False) 103 | roundtrip_tree_dump = ast.dump(roundtrip_tree, include_attributes=False) 104 | self.assertEqual(tree_dump, roundtrip_tree_dump, msg=path) 105 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | docutils 2 | pip >= 10.0 3 | pygments 4 | setuptools >= 41.0 5 | wheel 6 | -rrequirements.txt 7 | -------------------------------------------------------------------------------- /typed_astunparse/__init__.py: -------------------------------------------------------------------------------- 1 | """This is "__init__.py" file for "typed_astunparse" package. 2 | 3 | functions: unparse, dump 4 | """ 5 | 6 | import ast 7 | import typing as t 8 | 9 | import typed_ast.ast3 10 | from six.moves import cStringIO 11 | 12 | from .unparser import Unparser 13 | from .printer import Printer 14 | from ._version import VERSION 15 | 16 | __version__ = VERSION 17 | 18 | 19 | def unparse(tree: t.Union[ast.AST, typed_ast.ast3.AST]) -> str: 20 | """Unparse the abstract syntax tree into a str. 21 | 22 | Behave just like astunparse.unparse(tree), but handle trees which are typed, untyped, or mixed. 23 | In other words, a mixture of ast.AST-based and typed_ast.ast3-based nodes will be unparsed. 24 | """ 25 | stream = cStringIO() 26 | Unparser(tree, file=stream) 27 | return stream.getvalue() 28 | 29 | 30 | def dump( 31 | tree: t.Union[ast.AST, typed_ast.ast3.AST], annotate_fields: bool = True, 32 | include_attributes: bool = False) -> str: 33 | """Behave just like astunparse.dump(tree), but handle typed_ast.ast3-based trees.""" 34 | stream = cStringIO() 35 | Printer( 36 | file=stream, annotate_fields=annotate_fields, 37 | include_attributes=include_attributes).visit(tree) 38 | return stream.getvalue() 39 | 40 | 41 | __all__ = ['unparse', 'dump'] 42 | -------------------------------------------------------------------------------- /typed_astunparse/_version.py: -------------------------------------------------------------------------------- 1 | """Version of typed-astunparse package.""" 2 | 3 | from version_query import predict_version_str 4 | 5 | VERSION = predict_version_str() 6 | -------------------------------------------------------------------------------- /typed_astunparse/printer.py: -------------------------------------------------------------------------------- 1 | """Class: Printer.""" 2 | 3 | import ast 4 | import sys 5 | 6 | import astunparse 7 | import typed_ast.ast3 8 | 9 | 10 | class Printer(astunparse.Printer): 11 | """Partial rewrite of Printer from astunparse to handle typed_ast.ast3-based trees.""" 12 | 13 | def __init__( 14 | self, file=sys.stdout, indent=" ", annotate_fields: bool = True, 15 | include_attributes: bool = False): 16 | """Initialize Printer instance.""" 17 | super().__init__(file=file, indent=indent) 18 | self._annotate_fields = annotate_fields 19 | self._include_attributes = include_attributes 20 | 21 | def _prepare_for_print(self, node): 22 | if isinstance(node, list): 23 | nodestart = "[" 24 | nodeend = "]" 25 | children = [("", child) for child in node] 26 | else: 27 | nodestart = type(node).__name__ + "(" 28 | nodeend = ")" 29 | children = [ 30 | (name + "=" if self._annotate_fields else '', value) 31 | for name, value in typed_ast.ast3.iter_fields(node)] 32 | if self._include_attributes and node._attributes: 33 | children += [ 34 | (attr + '=' if self._annotate_fields else '', getattr(node, attr)) 35 | for attr in node._attributes] 36 | 37 | return nodestart, children, nodeend 38 | 39 | def generic_visit(self, node): 40 | """Print the syntax tree without unparsing it. 41 | 42 | Merge of astunparse.Printer.generic_visit() and typed_ast.ast3.dump(). 43 | """ 44 | nodestart, children, nodeend = self._prepare_for_print(node) 45 | 46 | if len(children) > 1: 47 | self.indentation += 1 48 | 49 | self.write(nodestart) 50 | for i, pair in enumerate(children): 51 | attr, child = pair 52 | if len(children) > 1: 53 | self.write("\n" + self.indent_with * self.indentation) 54 | if isinstance(child, (ast.AST, typed_ast.ast3.AST, list)): 55 | self.write(attr) 56 | self.visit(child) 57 | else: 58 | self.write(attr + repr(child)) 59 | 60 | if i != len(children) - 1: 61 | self.write(",") 62 | self.write(nodeend) 63 | 64 | if len(children) > 1: 65 | self.indentation -= 1 66 | -------------------------------------------------------------------------------- /typed_astunparse/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbdevpl/typed-astunparse/2bfb5150d3c5ba108faa55810c97b1c18c5585fa/typed_astunparse/py.typed -------------------------------------------------------------------------------- /typed_astunparse/unparser.py: -------------------------------------------------------------------------------- 1 | """Class: Unparser.""" 2 | 3 | import ast 4 | 5 | import astunparse 6 | from astunparse.unparser import interleave 7 | import typed_ast.ast3 8 | 9 | 10 | class Unparser(astunparse.Unparser): 11 | """Partial rewrite of Unparser from astunparse to handle typed_ast.ast3-based trees. 12 | 13 | The unparser aims at compatibility with native AST, as well as typed AST. 14 | 15 | Quoting grammar file[1] used by CPython, below is copy of the file for Python 3.6: 16 | 17 | " 18 | -- ASDL's 7 builtin types are: 19 | -- identifier, int, string, bytes, object, singleton, constant 20 | -- 21 | -- singleton: None, True or False 22 | -- constant can be None, whereas None means "no value" for object. 23 | 24 | module Python 25 | { 26 | mod = Module(stmt* body) 27 | | Interactive(stmt* body) 28 | | Expression(expr body) 29 | 30 | -- not really an actual node but useful in Jython's typesystem. 31 | | Suite(stmt* body) 32 | 33 | stmt = FunctionDef(identifier name, arguments args, 34 | stmt* body, expr* decorator_list, expr? returns) 35 | | AsyncFunctionDef(identifier name, arguments args, 36 | stmt* body, expr* decorator_list, expr? returns) 37 | 38 | | ClassDef(identifier name, expr* bases, keyword* keywords, 39 | stmt* body, expr* decorator_list) 40 | | Return(expr? value) 41 | 42 | | Delete(expr* targets) 43 | | Assign(expr* targets, expr value) 44 | | AugAssign(expr target, operator op, expr value) 45 | -- 'simple' indicates that we annotate simple name without parens 46 | | AnnAssign(expr target, expr annotation, expr? value, int simple) 47 | 48 | -- use 'orelse' because else is a keyword in target languages 49 | | For(expr target, expr iter, stmt* body, stmt* orelse) 50 | | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse) 51 | | While(expr test, stmt* body, stmt* orelse) 52 | | If(expr test, stmt* body, stmt* orelse) 53 | | With(withitem* items, stmt* body) 54 | | AsyncWith(withitem* items, stmt* body) 55 | 56 | | Raise(expr? exc, expr? cause) 57 | | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 58 | | Assert(expr test, expr? msg) 59 | 60 | | Import(alias* names) 61 | | ImportFrom(identifier? module, alias* names, int? level) 62 | 63 | | Global(identifier* names) 64 | | Nonlocal(identifier* names) 65 | | Expr(expr value) 66 | | Pass | Break | Continue 67 | 68 | -- col_offset is the byte offset in the utf8 string the parser uses 69 | attributes (int lineno, int col_offset) 70 | 71 | -- BoolOp() can use left & right? 72 | expr = BoolOp(boolop op, expr* values) 73 | | BinOp(expr left, operator op, expr right) 74 | | UnaryOp(unaryop op, expr operand) 75 | | Lambda(arguments args, expr body) 76 | | IfExp(expr test, expr body, expr orelse) 77 | | Dict(expr* keys, expr* values) 78 | | Set(expr* elts) 79 | | ListComp(expr elt, comprehension* generators) 80 | | SetComp(expr elt, comprehension* generators) 81 | | DictComp(expr key, expr value, comprehension* generators) 82 | | GeneratorExp(expr elt, comprehension* generators) 83 | -- the grammar constrains where yield expressions can occur 84 | | Await(expr value) 85 | | Yield(expr? value) 86 | | YieldFrom(expr value) 87 | -- need sequences for compare to distinguish between 88 | -- x < 4 < 3 and (x < 4) < 3 89 | | Compare(expr left, cmpop* ops, expr* comparators) 90 | | Call(expr func, expr* args, keyword* keywords) 91 | | Num(object n) -- a number as a PyObject. 92 | | Str(string s) -- need to specify raw, unicode, etc? 93 | | FormattedValue(expr value, int? conversion, expr? format_spec) 94 | | JoinedStr(expr* values) 95 | | Bytes(bytes s) 96 | | NameConstant(singleton value) 97 | | Ellipsis 98 | | Constant(constant value) 99 | 100 | -- the following expression can appear in assignment context 101 | | Attribute(expr value, identifier attr, expr_context ctx) 102 | | Subscript(expr value, slice slice, expr_context ctx) 103 | | Starred(expr value, expr_context ctx) 104 | | Name(identifier id, expr_context ctx) 105 | | List(expr* elts, expr_context ctx) 106 | | Tuple(expr* elts, expr_context ctx) 107 | 108 | -- col_offset is the byte offset in the utf8 string the parser uses 109 | attributes (int lineno, int col_offset) 110 | 111 | expr_context = Load | Store | Del | AugLoad | AugStore | Param 112 | 113 | slice = Slice(expr? lower, expr? upper, expr? step) 114 | | ExtSlice(slice* dims) 115 | | Index(expr value) 116 | 117 | boolop = And | Or 118 | 119 | operator = Add | Sub | Mult | MatMult | Div | Mod | Pow | LShift 120 | | RShift | BitOr | BitXor | BitAnd | FloorDiv 121 | 122 | unaryop = Invert | Not | UAdd | USub 123 | 124 | cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn 125 | 126 | comprehension = (expr target, expr iter, expr* ifs, int is_async) 127 | 128 | excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) 129 | attributes (int lineno, int col_offset) 130 | 131 | arguments = (arg* args, arg? vararg, arg* kwonlyargs, expr* kw_defaults, 132 | arg? kwarg, expr* defaults) 133 | 134 | arg = (identifier arg, expr? annotation) 135 | attributes (int lineno, int col_offset) 136 | 137 | -- keyword arguments supplied to call (NULL identifier for **kwargs) 138 | keyword = (identifier? arg, expr value) 139 | 140 | -- import name with optional 'as' alias. 141 | alias = (identifier name, identifier? asname) 142 | 143 | withitem = (expr context_expr, expr? optional_vars) 144 | } 145 | " 146 | 147 | Quoting the current docstring of "typed_ast/ast3.py"[2] from typed-ast repository: 148 | 149 | " 150 | The `ast3` module helps Python applications to process trees of the Python 151 | abstract syntax grammar. The abstract syntax itself might change with 152 | each Python release; this module helps to find out programmatically what 153 | the current grammar looks like and allows modifications of it. The 154 | difference between the `ast3` module and the builtin `ast` module is 155 | that `ast3` is version-independent and provides PEP 484 type comments as 156 | part of the AST. 157 | 158 | Specifically, these changes are made to the latest Python 3 AST: 159 | - The `FunctionDef`, `AsyncFunctionDef`, `Assign`, `For`, `AsyncFor`, 160 | `With`, `AsyncWith`, and `arg` classes all have a `type_comment` field 161 | which contains a `str` with the text of the associated type comment, if 162 | any. 163 | - `parse` has been augmented so it can parse function signature types when 164 | called with `mode=func_type`. 165 | - `parse` has an additional argument `feature_version`, which disables 166 | newer Python syntax features. 167 | - `Module` has a `type_ignores` field which contains a list of 168 | lines which have been `# type: ignore`d. 169 | - `Str` has a `kind` string field which preserves the original string 170 | prefix, so that `ast3.parse('u"test"').body[0].value.kind == 'u'`. 171 | 172 | An abstract syntax tree can be generated by using the `parse()` 173 | function from this module. The result will be a tree of objects whose 174 | classes all inherit from `ast3.AST`. 175 | 176 | Additionally various helper functions are provided that make working with 177 | the trees simpler. The main intention of the helper functions and this 178 | module in general is to provide an easy to use interface for libraries 179 | that work tightly with the python syntax (template engines for example). 180 | " 181 | 182 | [1]: https://hg.python.org/cpython/file/tip/Parser/Python.asdl 183 | [2]: https://github.com/python/typed_ast/blob/master/typed_ast/ast3.py#L5 184 | """ 185 | 186 | def _write_string_or_dispatch(self, value): 187 | """If value is str, write it. Otherwise, dispatch it.""" 188 | if isinstance(value, str): 189 | self.write(value) 190 | else: 191 | self.dispatch(value) 192 | 193 | def _fill_type_comment(self, type_comment): 194 | """Unparse type comment, adding it on the next line.""" 195 | self.fill('# type: ') 196 | self._write_string_or_dispatch(type_comment) 197 | 198 | def _write_type_comment(self, type_comment): 199 | """Unparse type comment, appending it to the end of the current line.""" 200 | self.write(' # type: ') 201 | self._write_string_or_dispatch(type_comment) 202 | 203 | def _write_raw_literal(self, text: str): 204 | delimiter = None 205 | for _ in ("'", '"', "'''", '"""'): 206 | if _ not in text: 207 | delimiter = _ 208 | break 209 | if delimiter is None: 210 | delimiter = '"""' 211 | assert delimiter is not None 212 | if '\n' in text and delimiter in {'"', "'"}: 213 | delimiter = {'"': '"""', "'": "'''"}[delimiter] 214 | if delimiter in text: 215 | escaped_delimiter = ''.join(['\\{}'.format(_) for _ in delimiter]) 216 | text = text.replace(delimiter, escaped_delimiter) 217 | self.write(delimiter) 218 | self.write(text) 219 | self.write(delimiter) 220 | 221 | def _ClassDef(self, t): 222 | if isinstance(t, ast.ClassDef): 223 | super()._ClassDef(t) 224 | return 225 | 226 | self.write("\n") 227 | for deco in t.decorator_list: 228 | self.fill("@") 229 | self.dispatch(deco) 230 | self.fill("class " + t.name) 231 | self.write("(") 232 | comma = False 233 | for base in t.bases: 234 | if comma: 235 | self.write(", ") 236 | else: 237 | comma = True 238 | self.dispatch(base) 239 | for keyword in t.keywords: 240 | if comma: 241 | self.write(", ") 242 | else: 243 | comma = True 244 | self.dispatch(keyword) 245 | self.write(")") 246 | self.enter() 247 | self.dispatch(t.body) 248 | self.leave() 249 | 250 | def _generic_FunctionDef(self, t, async_=False): 251 | """Unparse FunctionDef or AsyncFunctionDef node. 252 | 253 | Rather than handling: 254 | 255 | FunctionDef/AsyncFunctionDef( 256 | identifier name, arguments args, stmt* body, expr* decorator_list, expr? returns) 257 | 258 | handle: 259 | 260 | FunctionDef/AsyncFunctionDef( 261 | identifier name, arguments args, stmt* body, expr* decorator_list, expr? returns, 262 | string? type_comment) 263 | """ 264 | if not hasattr(t, 'type_comment') or t.type_comment is None: 265 | super()._generic_FunctionDef(t, async_) 266 | return 267 | 268 | self.write("\n") 269 | for deco in t.decorator_list: 270 | self.fill("@") 271 | self.dispatch(deco) 272 | self.fill(("async " if async_ else "") + "def " + t.name + "(") 273 | self.dispatch(t.args) 274 | self.write(")") 275 | if getattr(t, "returns", False): 276 | self.write(" -> ") 277 | self.dispatch(t.returns) 278 | self.enter() 279 | self._fill_type_comment(t.type_comment) 280 | self.dispatch(t.body) 281 | self.leave() 282 | 283 | def _Assign(self, t): 284 | """Unparse Assign node. 285 | 286 | Rather than handling just: 287 | 288 | Assign(expr* targets, expr value) 289 | 290 | handle: 291 | 292 | Assign(expr* targets, expr value, string? type_comment) 293 | """ 294 | super()._Assign(t) 295 | if hasattr(t, 'type_comment') and t.type_comment is not None: 296 | self._write_type_comment(t.type_comment) 297 | 298 | def _generic_For(self, t, async_=False): 299 | """Unparse For or AsyncFor node. 300 | 301 | Rather than handling just: 302 | 303 | For/AsyncFor(expr target, expr iter, stmt* body, stmt* orelse) 304 | 305 | handle: 306 | 307 | For/AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 308 | """ 309 | if not hasattr(t, 'type_comment') or t.type_comment is None: 310 | super()._generic_For(t, async_) 311 | return 312 | 313 | self.fill("async for " if async_ else "for ") 314 | self.dispatch(t.target) 315 | self.write(" in ") 316 | self.dispatch(t.iter) 317 | self.enter() 318 | self._write_type_comment(t.type_comment) 319 | self.dispatch(t.body) 320 | self.leave() 321 | if t.orelse: 322 | self.fill("else") 323 | self.enter() 324 | self.dispatch(t.orelse) 325 | self.leave() 326 | 327 | def _If(self, t): 328 | self.fill("if ") 329 | self.dispatch(t.test) 330 | self.enter() 331 | self.dispatch(t.body) 332 | self.leave() 333 | # collapse nested ifs into equivalent elifs. 334 | while t.orelse and len(t.orelse) == 1 \ 335 | and isinstance(t.orelse[0], (ast.If, typed_ast.ast3.If)): 336 | t = t.orelse[0] 337 | self.fill("elif ") 338 | self.dispatch(t.test) 339 | self.enter() 340 | self.dispatch(t.body) 341 | self.leave() 342 | # final else 343 | if t.orelse: 344 | self.fill("else") 345 | self.enter() 346 | self.dispatch(t.orelse) 347 | self.leave() 348 | 349 | def _generic_With(self, t, async_=False): 350 | """Unparse With or AsyncWith node. 351 | 352 | Rather than handling just: 353 | 354 | With/AsyncWith(withitem* items, stmt* body) 355 | 356 | handle: 357 | 358 | With/AsyncWith(withitem* items, stmt* body, string? type_comment) 359 | """ 360 | if not hasattr(t, 'type_comment') or t.type_comment is None: 361 | super()._generic_With(t, async_) 362 | return 363 | 364 | self.fill("async with " if async_ else "with ") 365 | interleave(lambda: self.write(", "), self.dispatch, t.items) 366 | self.enter() 367 | self._write_type_comment(t.type_comment) 368 | self.dispatch(t.body) 369 | self.leave() 370 | 371 | def _Bytes(self, tree): 372 | if hasattr(tree, 'kind') and tree.kind: 373 | self.write(tree.kind) 374 | if 'r' in tree.kind.lower(): 375 | self._write_raw_literal(tree.s.decode()) 376 | else: 377 | self.write(repr(tree.s)[1:]) 378 | return 379 | super()._Bytes(tree) 380 | 381 | def _Str(self, tree): 382 | if hasattr(tree, 'kind') and tree.kind: 383 | self.write(tree.kind) 384 | if 'r' in tree.kind.lower(): 385 | self._write_raw_literal(tree.s) 386 | return 387 | super()._Str(tree) 388 | 389 | boolops = {'And': 'and', 'Or': 'or'} 390 | 391 | def _BoolOp(self, syntax): 392 | # TODO: push this to astunparse (upstream) 393 | self.write('(') 394 | op_ = ' {} '.format(self.boolops[syntax.op.__class__.__name__]) 395 | interleave(lambda: self.write(op_), self.dispatch, syntax.values) 396 | self.write(')') 397 | 398 | def _Attribute(self, t): 399 | self.dispatch(t.value) 400 | # Special case: 3.__abs__() is a syntax error, so if t.value 401 | # is an integer literal then we need to either parenthesize 402 | # it or add an extra space to get 3 .__abs__(). 403 | if isinstance(t.value, (ast.Num, typed_ast.ast3.Num)) and isinstance(t.value.n, int): 404 | self.write(" ") 405 | self.write(".") 406 | self.write(t.attr) 407 | 408 | def _Call(self, t): 409 | if isinstance(t, ast.Call): 410 | super()._Call(t) 411 | return 412 | 413 | self.dispatch(t.func) 414 | self.write("(") 415 | comma = False 416 | for arg in t.args: 417 | if comma: 418 | self.write(", ") 419 | else: 420 | comma = True 421 | self.dispatch(arg) 422 | for keyword in t.keywords: 423 | if comma: 424 | self.write(", ") 425 | else: 426 | comma = True 427 | self.dispatch(keyword) 428 | self.write(")") 429 | 430 | def _arguments(self, t): 431 | first = True 432 | latest_comment = None 433 | # normal arguments 434 | defaults = [None] * (len(t.args) - len(t.defaults)) + t.defaults 435 | for arg, default in zip(t.args, defaults): 436 | if first: 437 | first = False 438 | else: 439 | self.write(',') 440 | if latest_comment is not None: 441 | self._write_type_comment(latest_comment) 442 | self.fill(' ') 443 | latest_comment = None 444 | else: 445 | self.write(' ') 446 | self.dispatch(arg) 447 | if default: 448 | self.write("=") 449 | self.dispatch(default) 450 | latest_comment = getattr(arg, 'type_comment', None) 451 | 452 | # varargs, or bare '*' if no varargs but keyword-only arguments present 453 | if t.vararg or getattr(t, "kwonlyargs", False): 454 | if first: 455 | first = False 456 | else: 457 | self.write(',') 458 | if latest_comment is not None: 459 | self._write_type_comment(latest_comment) 460 | self.fill(' ') 461 | latest_comment = None 462 | else: 463 | self.write(' ') 464 | self.write("*") 465 | if t.vararg: 466 | self.write(t.vararg.arg) 467 | if t.vararg.annotation: 468 | self.write(": ") 469 | self.dispatch(t.vararg.annotation) 470 | latest_comment = getattr(t.vararg, 'type_comment', None) 471 | 472 | # keyword-only arguments 473 | if getattr(t, "kwonlyargs", False): 474 | for kwarg, default in zip(t.kwonlyargs, t.kw_defaults): 475 | self.write(',') 476 | if latest_comment is not None: 477 | self._write_type_comment(latest_comment) 478 | self.fill(' ') 479 | latest_comment = None 480 | else: 481 | self.write(' ') 482 | self.dispatch(kwarg) 483 | if default: 484 | self.write("=") 485 | self.dispatch(default) 486 | latest_comment = getattr(kwarg, 'type_comment', None) 487 | 488 | # kwargs 489 | if t.kwarg: 490 | if first: 491 | first = False 492 | else: 493 | self.write(',') 494 | if latest_comment is not None: 495 | self._write_type_comment(latest_comment) 496 | self.fill(' ') 497 | latest_comment = None 498 | else: 499 | self.write(' ') 500 | self.write("**"+t.kwarg.arg) 501 | if t.kwarg.annotation: 502 | self.write(": ") 503 | self.dispatch(t.kwarg.annotation) 504 | latest_comment = getattr(t.kwarg, 'type_comment', None) 505 | 506 | if latest_comment is not None: 507 | self._write_type_comment(latest_comment) 508 | self.fill(' ') 509 | --------------------------------------------------------------------------------