├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── RELEASING.rst ├── appveyor.yml ├── codegen.py ├── docs └── index.md ├── mkdocs.yml ├── pytest_ast_back_to_python.py ├── setup.cfg ├── setup.py ├── tests ├── conftest.py └── test_ast_back_to_python.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | show_missing = True 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # IPython Notebook 66 | .ipynb_checkpoints 67 | 68 | # pyenv 69 | .python-version 70 | 71 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | sudo: false 4 | language: python 5 | python: 6 | - "2.7" 7 | - "3.4" 8 | - "3.5" 9 | - "3.6" 10 | - "3.7" 11 | - "3.8-dev" 12 | - "nightly" 13 | 14 | install: 15 | - pip install tox-travis coveralls pytest-travis-fold 16 | script: tox 17 | 18 | before_cache: 19 | - rm -rf $HOME/.cache/pip/log 20 | cache: 21 | directories: 22 | - $HOME/.cache/pip 23 | after_success: 24 | - coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Tom Viner 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of [project] nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include *.yml 3 | include .coveragerc 4 | include LICENSE 5 | include tox.ini 6 | recursive-include docs *.md 7 | recursive-include tests *.py 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pytest-ast-back-to-python 2 | ========================= 3 | 4 | .. image:: https://travis-ci.org/tomviner/pytest-ast-back-to-python.svg?branch=master 5 | :target: https://travis-ci.org/tomviner/pytest-ast-back-to-python 6 | :alt: See Build Status on Travis CI 7 | 8 | .. image:: https://ci.appveyor.com/api/projects/status/github/tomviner/pytest-ast-back-to-python?branch=master 9 | :target: https://ci.appveyor.com/project/tomviner/pytest-ast-back-to-python/branch/master 10 | :alt: See Build Status on AppVeyor 11 | 12 | A plugin for pytest devs to view how assertion rewriting recodes the AST 13 | 14 | ---- 15 | 16 | Features 17 | -------- 18 | 19 | Pytest rewrites the AST (abstract syntax tree) of your tests, for the purpose of displaying the subexpressions involved in your assert statements. This plugin converts that rewritten AST back to Python source, and displays it. 20 | 21 | 22 | Installation 23 | ------------ 24 | 25 | You can install "pytest-ast-back-to-python" via `pip`_ from `PyPI`_:: 26 | 27 | $ pip install pytest-ast-back-to-python 28 | 29 | 30 | Usage 31 | ----- 32 | 33 | .. code-block:: bash 34 | 35 | py.test --show-ast-as-python 36 | 37 | Example 38 | ------- 39 | 40 | Take a trivial test like: 41 | 42 | .. code-block:: python 43 | 44 | def test_simple(): 45 | a = 1 46 | b = 2 47 | assert a + b == 3 48 | 49 | View the rewritten AST as Python like this: 50 | 51 | .. code-block:: bash 52 | 53 | $ py.test --show-ast-as-python test_simple.py 54 | ======================================== test session starts ======================================== 55 | plugins: ast-back-to-python-0.1.0, cov-2.2.1 56 | collected 1 items 57 | 58 | test_simple.py . 59 | ======================================== Rewritten AST as Python ======================================== 60 | import builtins as @py_builtins 61 | import _pytest.assertion.rewrite as @pytest_ar 62 | 63 | def test_simple(): 64 | a = 1 65 | b = 2 66 | @py_assert2 = a + b 67 | @py_assert4 = 3 68 | @py_assert3 = @py_assert2 == @py_assert4 69 | if @py_assert3 is None: 70 | from _pytest.warning_types import PytestAssertRewriteWarning 71 | from warnings import warn_explicit 72 | warn_explicit(PytestAssertRewriteWarning('asserting the value None, please use "assert is None"'), category=None, filename='/home/tom/.virtualenvs/tmp-483cf04ecc31dda8/test_thing.py', lineno=4) 73 | if not @py_assert3: 74 | @py_format6 = @pytest_ar._call_reprcompare(('==',), (@py_assert3,), ('(%(py0)s + %(py1)s) == %(py5)s',), (@py_assert2, @py_assert4)) % {'py0': @pytest_ar._saferepr(a) if 'a' in @py_builtins.locals() or @pytest_ar._should_repr_global_name(a) else 'a', 'py1': @pytest_ar._saferepr(b) if 'b' in @py_builtins.locals() or @pytest_ar._should_repr_global_name(b) else 'b', 'py5': @pytest_ar._saferepr(@py_assert4)} 75 | @py_format8 = ('' + 'assert %(py7)s') % {'py7': @py_format6} 76 | raise AssertionError(@pytest_ar._format_explanation(@py_format8)) 77 | @py_assert2 = @py_assert3 = @py_assert4 = None 78 | 79 | 80 | Contributing 81 | ------------ 82 | Contributions are very welcome. Tests can be run with `tox`_, please ensure 83 | the coverage at least stays the same before you submit a pull request. 84 | 85 | License 86 | ------- 87 | 88 | Distributed under the terms of the `BSD-3`_ license, "pytest-ast-back-to-python" is free and open source software 89 | 90 | 91 | This `Pytest`_ plugin was generated with `Cookiecutter`_ along with `@hackebrot`_'s `Cookiecutter-pytest-plugin`_ template. 92 | 93 | Issues 94 | ------ 95 | 96 | If you encounter any problems, please `file an issue`_ along with a detailed description. 97 | 98 | .. _`Cookiecutter`: https://github.com/audreyr/cookiecutter 99 | .. _`@hackebrot`: https://github.com/hackebrot 100 | .. _`MIT`: http://opensource.org/licenses/MIT 101 | .. _`BSD-3`: http://opensource.org/licenses/BSD-3-Clause 102 | .. _`GNU GPL v3.0`: http://www.gnu.org/licenses/gpl-3.0.txt 103 | .. _`Apache Software License 2.0`: http://www.apache.org/licenses/LICENSE-2.0 104 | .. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin 105 | .. _`file an issue`: https://github.com/tomviner/pytest-ast-back-to-python/issues 106 | .. _`pytest`: https://github.com/pytest-dev/pytest 107 | .. _`tox`: https://tox.readthedocs.org/en/latest/ 108 | .. _`pip`: https://pypi.python.org/pypi/pip/ 109 | .. _`PyPI`: https://pypi.python.org/pypi 110 | -------------------------------------------------------------------------------- /RELEASING.rst: -------------------------------------------------------------------------------- 1 | Releasing 2 | ========= 3 | 4 | Commit changes and ensure tests pass. 5 | 6 | Increment the version number in ``setup.py`` according to *SEMVER*. 7 | 8 | Upload to pypi: 9 | 10 | .. code-block:: bash 11 | 12 | $ python setup.py sdist bdist_wheel upload -r pypi 13 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # What Python version is installed where: 2 | # http://www.appveyor.com/docs/installed-software#python 3 | 4 | environment: 5 | matrix: 6 | - PYTHON: "C:\\Python27" 7 | TOX_ENV: "py27" 8 | 9 | - PYTHON: "C:\\Python34" 10 | TOX_ENV: "py34" 11 | 12 | - PYTHON: "C:\\Python35" 13 | TOX_ENV: "py35" 14 | 15 | 16 | init: 17 | - "%PYTHON%/python -V" 18 | - "%PYTHON%/python -c \"import struct;print( 8 * struct.calcsize(\'P\'))\"" 19 | 20 | install: 21 | - "%PYTHON%/Scripts/easy_install -U pip" 22 | - "%PYTHON%/Scripts/pip install tox" 23 | - "%PYTHON%/Scripts/pip install wheel" 24 | 25 | build: false # Not a C# project, build stuff at the test step instead. 26 | 27 | test_script: 28 | - "%PYTHON%/Scripts/tox -e %TOX_ENV%" 29 | 30 | after_test: 31 | - "%PYTHON%/python setup.py bdist_wheel" 32 | - ps: "ls dist" 33 | 34 | artifacts: 35 | - path: dist\* 36 | 37 | #on_success: 38 | # - TODO: upload the content of dist/*.whl to a public wheelhouse 39 | -------------------------------------------------------------------------------- /codegen.py: -------------------------------------------------------------------------------- 1 | # From https://github.com/CensoredUsername/codegen 2 | """ 3 | codegen 4 | ~~~~~~~ 5 | 6 | Extension to ast that allow ast -> python code generation. 7 | 8 | :copyright: Copyright 2008 by Armin Ronacher. 9 | :license: BSD. 10 | 11 | Copyright (c) 2008, Armin Ronacher 12 | All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without modification, 15 | are permitted provided that the following conditions are met: 16 | 17 | - Redistributions of source code must retain the above copyright notice, this list of 18 | conditions and the following disclaimer. 19 | - Redistributions in binary form must reproduce the above copyright notice, this list of 20 | conditions and the following disclaimer in the documentation and/or other materials 21 | provided with the distribution. 22 | - Neither the name of the nor the names of its contributors may be used to 23 | endorse or promote products derived from this software without specific prior written 24 | permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 27 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 28 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 29 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 30 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 31 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 32 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 33 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | """ 35 | 36 | import sys 37 | PY3 = sys.version_info >= (3, 0) 38 | # These might not exist, so we put them equal to NoneType 39 | Try = TryExcept = TryFinally = YieldFrom = MatMult = Await = type(None) 40 | 41 | from ast import * 42 | 43 | class Sep(object): 44 | # Performs the common pattern of returning a different symbol the first 45 | # time the object is called 46 | def __init__(self, last, first=''): 47 | self.last = last 48 | self.first = first 49 | self.begin = True 50 | def __call__(self): 51 | if self.begin: 52 | self.begin = False 53 | return self.first 54 | return self.last 55 | 56 | def to_source(node, indent_with=' ' * 4, add_line_information=False, correct_line_numbers=False): 57 | """This function can convert a node tree back into python sourcecode. 58 | This is useful for debugging purposes, especially if you're dealing with 59 | custom asts not generated by python itself. 60 | 61 | It could be that the sourcecode is evaluable when the AST itself is not 62 | compilable / evaluable. The reason for this is that the AST contains some 63 | more data than regular sourcecode does, which is dropped during 64 | conversion. 65 | 66 | Each level of indentation is replaced with `indent_with`. Per default this 67 | parameter is equal to four spaces as suggested by PEP 8, but it might be 68 | adjusted to match the application's styleguide. 69 | 70 | If `add_line_information` is set to `True` comments for the line numbers 71 | of the nodes are added to the output. This can be used to spot wrong line 72 | number information of statement nodes. 73 | """ 74 | if correct_line_numbers: 75 | if hasattr(node, 'lineno'): 76 | return SourceGenerator(indent_with, add_line_information, True, node.lineno).process(node) 77 | else: 78 | return SourceGenerator(indent_with, add_line_information, True).process(node) 79 | else: 80 | return SourceGenerator(indent_with, add_line_information).process(node) 81 | 82 | 83 | class SourceGenerator(NodeVisitor): 84 | """This visitor is able to transform a well formed syntax tree into python 85 | sourcecode. For more details have a look at the docstring of the 86 | `node_to_source` function. 87 | """ 88 | 89 | COMMA = ', ' 90 | COLON = ': ' 91 | ASSIGN = ' = ' 92 | SEMICOLON = '; ' 93 | ARROW = ' -> ' 94 | 95 | BOOLOP_SYMBOLS = { 96 | And: (' and ', 5), 97 | Or: (' or ', 4) 98 | } 99 | 100 | BINOP_SYMBOLS = { 101 | Add: (' + ', 12), 102 | Sub: (' - ', 12), 103 | Mult: (' * ', 13), 104 | MatMult: (' @ ', 13), 105 | Div: (' / ', 13), 106 | FloorDiv: (' // ', 13), 107 | Mod: (' % ', 13), 108 | Pow: (' ** ', 15), 109 | LShift: (' << ', 11), 110 | RShift: (' >> ', 11), 111 | BitOr: (' | ', 8), 112 | BitAnd: (' & ', 10), 113 | BitXor: (' ^ ', 9) 114 | } 115 | 116 | CMPOP_SYMBOLS = { 117 | Eq: (' == ', 7), 118 | Gt: (' > ', 7), 119 | GtE: (' >= ', 7), 120 | In: (' in ', 7), 121 | Is: (' is ', 7), 122 | IsNot: (' is not ', 7), 123 | Lt: (' < ', 7), 124 | LtE: (' <= ', 7), 125 | NotEq: (' != ', 7), 126 | NotIn: (' not in ', 7) 127 | } 128 | 129 | UNARYOP_SYMBOLS = { 130 | Invert: ('~', 14), 131 | Not: ('not ', 6), 132 | UAdd: ('+', 14), 133 | USub: ('-', 14) 134 | } 135 | 136 | BLOCK_NODES = (If, For, While, With, Try, TryExcept, TryFinally, 137 | FunctionDef, ClassDef) 138 | 139 | def __init__(self, indent_with, add_line_information=False, correct_line_numbers=False, line_number=1): 140 | self.result = [] 141 | self.indent_with = indent_with 142 | self.add_line_information = add_line_information 143 | self.indentation = 0 144 | self.new_lines = 0 145 | 146 | # precedence_stack: what precedence level are we on, could we safely newline before and is this operator left-to-right 147 | self.precedence_stack = [[0, False, None]] 148 | 149 | self.correct_line_numbers = correct_line_numbers 150 | # The current line number we *think* we are on. As in it's most likely 151 | # the line number of the last node we passed which can differ when 152 | # the ast is broken 153 | self.line_number = line_number 154 | # Can we insert a newline here without having to escape it? 155 | # (are we between delimiting characters) 156 | self.can_newline = False 157 | # after a colon, we don't have to print a semi colon. set to 1 when self.body() is called, 158 | # set to 2 or 0 when it's actually used. set to 0 at the end of the body 159 | self.after_colon = 0 160 | # reset by a call to self.newline, set by the first call to write() afterwards 161 | # determines if we have to print the newlines and indent 162 | self.indented = False 163 | # the amount of newlines to be printed 164 | self.newlines = 0 165 | # force the printing of a proper newline (and not a semicolon) 166 | self.force_newline = False 167 | 168 | def process(self, node): 169 | self.visit(node) 170 | result = ''.join(self.result) 171 | self.result = [] 172 | return result 173 | 174 | # Precedence management 175 | 176 | def prec_start(self, value, ltr=None): 177 | newline = self.can_newline 178 | if value < self.precedence_stack[-1][0]: 179 | self.write('(') 180 | self.can_newline = True 181 | if ltr == False: 182 | value += 1 183 | self.precedence_stack.append([value, newline, ltr]) 184 | 185 | def prec_middle(self, level=None): 186 | if level is not None: 187 | self.precedence_stack[-1][0] = level 188 | elif self.precedence_stack[-1][2]: 189 | self.precedence_stack[-1][0] += 1 190 | elif self.precedence_stack[-1][2] is False: 191 | self.precedence_stack[-1][0] -= 1 192 | 193 | def prec_end(self): 194 | precedence, newline, ltr = self.precedence_stack.pop() 195 | if ltr: 196 | precedence -= 1 197 | if precedence < self.precedence_stack[-1][0]: 198 | self.write(')') 199 | self.can_newline = newline 200 | 201 | def paren_start(self, symbol='('): 202 | self.precedence_stack.append([0, self.can_newline, None]) 203 | self.write(symbol) 204 | self.can_newline = True 205 | 206 | def paren_end(self, symbol=')'): 207 | _, self.can_newline, _ = self.precedence_stack.pop() 208 | self.write(symbol) 209 | 210 | # convenience functions 211 | 212 | def write(self, x): 213 | # ignore empty writes 214 | if not x: 215 | return 216 | 217 | # Before we write, we must check if newlines have been queued. 218 | # If this is the case, we have to handle them properly 219 | if self.correct_line_numbers: 220 | if not self.indented: 221 | self.new_lines = max(self.new_lines, 1 if self.force_newline else 0) 222 | self.force_newline = False 223 | 224 | if self.new_lines: 225 | # we have new lines to print 226 | if self.after_colon == 2: 227 | self.result.append(';'+'\\\n' * self.new_lines) 228 | else: 229 | self.after_colon = 0 230 | self.result.append('\n' * self.new_lines) 231 | self.result.append(self.indent_with * self.indentation) 232 | elif self.after_colon == 1: 233 | # we're directly after a block-having statement and can write on the same line 234 | self.after_colon = 2 235 | self.result.append(' ') 236 | elif self.result: 237 | # we're after any statement. or at the start of the file 238 | self.result.append(self.SEMICOLON) 239 | self.indented = True 240 | elif self.new_lines > 0: 241 | if self.can_newline: 242 | self.result.append('\n' * self.new_lines) 243 | self.result.append(self.indent_with * (self.indentation + 1)) 244 | else: 245 | self.result.append('\\\n' * self.new_lines) 246 | self.result.append(self.indent_with * (self.indentation + 1)) 247 | self.new_lines = 0 248 | 249 | 250 | elif self.new_lines: 251 | # normal behaviour 252 | self.result.append('\n' * self.new_lines) 253 | self.result.append(self.indent_with * self.indentation) 254 | self.new_lines = 0 255 | self.result.append(x) 256 | 257 | def newline(self, node=None, extra=0, force=False): 258 | if not self.correct_line_numbers: 259 | self.new_lines = max(self.new_lines, 1 + extra) 260 | if not self.result: 261 | self.new_lines = 0 262 | if node is not None and self.add_line_information: 263 | self.write('# line: %s' % node.lineno) 264 | self.new_lines = 1 265 | else: 266 | if extra: 267 | #Ignore extra 268 | return 269 | 270 | self.indented = False 271 | 272 | if node is None: 273 | # else/finally statement. insert one true newline. body is implicit 274 | self.force_newline = True 275 | self.new_lines += 1 276 | self.line_number += 1 277 | 278 | elif force: 279 | # statement with a block: needs a true newline before it 280 | self.force_newline = True 281 | self.new_lines += node.lineno - self.line_number 282 | self.line_number = node.lineno 283 | 284 | else: 285 | # block-less statement: needs a semicolon, colon, or newline in front of it 286 | self.new_lines += node.lineno - self.line_number 287 | self.line_number = node.lineno 288 | 289 | def maybe_break(self, node): 290 | if self.correct_line_numbers: 291 | self.new_lines += node.lineno - self.line_number 292 | self.line_number = node.lineno 293 | 294 | def body(self, statements): 295 | self.force_newline = any(isinstance(i, self.BLOCK_NODES) for i in statements) 296 | self.indentation += 1 297 | self.after_colon = 1 298 | for stmt in statements: 299 | self.visit(stmt) 300 | self.indentation -= 1 301 | self.force_newline = True 302 | self.after_colon = 0 # do empty blocks even exist? 303 | 304 | def body_or_else(self, node): 305 | self.body(node.body) 306 | if node.orelse: 307 | self.newline() 308 | self.write('else:') 309 | self.body(node.orelse) 310 | 311 | def visit_bare(self, node): 312 | # this node is allowed to be a bare tuple 313 | if isinstance(node, Tuple): 314 | self.visit_Tuple(node, False) 315 | else: 316 | self.visit(node) 317 | 318 | def visit_bareyield(self, node): 319 | if isinstance(node, Yield): 320 | self.visit_Yield(node, False) 321 | elif isinstance(node, YieldFrom): 322 | self.visit_YieldFrom(node, False) 323 | else: 324 | self.visit_bare(node) 325 | 326 | def decorators(self, node): 327 | for decorator in node.decorator_list: 328 | self.newline(decorator, force=True) 329 | self.write('@') 330 | self.visit(decorator) 331 | if node.decorator_list: 332 | self.newline() 333 | else: 334 | self.newline(node, force=True) 335 | 336 | # Module 337 | def visit_Module(self, node): 338 | self.generic_visit(node) 339 | self.write('\n') 340 | self.line_number += 1 341 | 342 | # Statements 343 | 344 | def visit_Assert(self, node): 345 | self.newline(node) 346 | self.write('assert ') 347 | self.visit(node.test) 348 | if node.msg: 349 | self.write(self.COMMA) 350 | self.visit(node.msg) 351 | 352 | def visit_Assign(self, node): 353 | self.newline(node) 354 | for target in node.targets: 355 | self.visit_bare(target) 356 | self.write(self.ASSIGN) 357 | self.visit_bareyield(node.value) 358 | 359 | def visit_AugAssign(self, node): 360 | self.newline(node) 361 | self.visit_bare(node.target) 362 | self.write(self.BINOP_SYMBOLS[type(node.op)][0].rstrip() + self.ASSIGN.lstrip()) 363 | self.visit_bareyield(node.value) 364 | 365 | def visit_Await(self, node): 366 | self.maybe_break(node) 367 | self.prec_start(16, True) 368 | self.prec_middle() 369 | self.write('await ') 370 | self.visit(node.value) 371 | self.prec_end() 372 | 373 | def visit_ImportFrom(self, node): 374 | self.newline(node) 375 | self.write('from ') 376 | self.write('%s%s' % ('.' * node.level, node.module or '')) 377 | self.write(' import ') 378 | sep = Sep(self.COMMA) 379 | for item in node.names: 380 | self.write(sep()) 381 | self.visit(item) 382 | 383 | def visit_Import(self, node): 384 | self.newline(node) 385 | self.write('import ') 386 | sep = Sep(self.COMMA) 387 | for item in node.names: 388 | self.write(sep()) 389 | self.visit(item) 390 | 391 | def visit_Exec(self, node): 392 | self.newline(node) 393 | self.write('exec ') 394 | self.visit(node.body) 395 | if node.globals: 396 | self.write(' in ') 397 | self.visit(node.globals) 398 | if node.locals: 399 | self.write(self.COMMA) 400 | self.visit(node.locals) 401 | 402 | def visit_Expr(self, node): 403 | self.newline(node) 404 | self.visit_bareyield(node.value) 405 | 406 | def visit_AsyncFunctionDef(self, node): 407 | self.visit_FunctionDef(node, True) 408 | 409 | def visit_FunctionDef(self, node, async_=False): 410 | self.newline(extra=1) 411 | # first decorator line number will be used 412 | self.decorators(node) 413 | if async_: 414 | self.write('async ') 415 | self.write('def ') 416 | self.write(node.name) 417 | self.paren_start() 418 | self.visit_arguments(node.args) 419 | self.paren_end() 420 | if hasattr(node, 'returns') and node.returns is not None: 421 | self.write(self.ARROW) 422 | self.visit(node.returns) 423 | self.write(':') 424 | self.body(node.body) 425 | 426 | def visit_arguments(self, node): 427 | sep = Sep(self.COMMA) 428 | padding = [None] * (len(node.args) - len(node.defaults)) 429 | if hasattr(node, 'kwonlyargs'): 430 | for arg, default in zip(node.args, padding + node.defaults): 431 | self.write(sep()) 432 | self.visit(arg) 433 | if default is not None: 434 | self.write('=') 435 | self.visit(default) 436 | if node.vararg is not None: 437 | self.write(sep()) 438 | if hasattr(node, 'varargannotation'): 439 | if node.varargannotation is None: 440 | self.write('*' + node.vararg) 441 | else: 442 | self.maybe_break(node.varargannotation) 443 | self.write('*' + node.vararg) 444 | self.write(self.COLON) 445 | self.visit(node.varargannotation) 446 | else: 447 | self.maybe_break(node.vararg) 448 | self.write('*') 449 | self.visit(node.vararg) 450 | elif node.kwonlyargs: 451 | self.write(sep() + '*') 452 | 453 | for arg, default in zip(node.kwonlyargs, node.kw_defaults): 454 | self.write(sep()) 455 | self.visit(arg) 456 | if default is not None: 457 | self.write('=') 458 | self.visit(default) 459 | if node.kwarg is not None: 460 | self.write(sep()) 461 | if hasattr(node, 'kwargannotation'): 462 | if node.kwargannotation is None: 463 | self.write('**' + node.kwarg) 464 | else: 465 | self.maybe_break(node.kwargannotation) 466 | self.write('**' + node.kwarg) 467 | self.write(self.COLON) 468 | self.visit(node.kwargannotation) 469 | else: 470 | self.maybe_break(node.kwarg) 471 | self.write('**') 472 | self.visit(node.kwarg) 473 | else: 474 | for arg, default in zip(node.args, padding + node.defaults): 475 | self.write(sep()) 476 | self.visit(arg) 477 | if default is not None: 478 | self.write('=') 479 | self.visit(default) 480 | if node.vararg is not None: 481 | self.write(sep()) 482 | self.write('*' + node.vararg) 483 | if node.kwarg is not None: 484 | self.write(sep()) 485 | self.write('**' + node.kwarg) 486 | 487 | def visit_arg(self, node): 488 | # Py3 only 489 | self.maybe_break(node) 490 | self.write(node.arg) 491 | if node.annotation is not None: 492 | self.write(self.COLON) 493 | self.visit(node.annotation) 494 | 495 | def visit_keyword(self, node): 496 | self.maybe_break(node.value) 497 | if node.arg is not None: 498 | self.write(node.arg + '=') 499 | else: 500 | self.write('**') 501 | self.visit(node.value) 502 | 503 | def visit_ClassDef(self, node): 504 | self.newline(extra=2) 505 | # first decorator line number will be used 506 | self.decorators(node) 507 | self.write('class %s' % node.name) 508 | 509 | if (node.bases or (hasattr(node, 'keywords') and node.keywords) or 510 | (hasattr(node, 'starargs') and (node.starargs or node.kwargs))): 511 | self.paren_start() 512 | sep = Sep(self.COMMA) 513 | 514 | for base in node.bases: 515 | self.write(sep()) 516 | self.visit(base) 517 | # XXX: the if here is used to keep this module compatible 518 | # with python 2.6. 519 | if hasattr(node, 'keywords'): 520 | for keyword in node.keywords: 521 | self.write(sep()) 522 | self.visit(keyword) 523 | if hasattr(node, 'starargs'): 524 | if node.starargs is not None: 525 | self.write(sep()) 526 | self.maybe_break(node.starargs) 527 | self.write('*') 528 | self.visit(node.starargs) 529 | if node.kwargs is not None: 530 | self.write(sep()) 531 | self.maybe_break(node.kwargs) 532 | self.write('**') 533 | self.visit(node.kwargs) 534 | self.paren_end() 535 | self.write(':') 536 | self.body(node.body) 537 | 538 | def visit_If(self, node): 539 | self.newline(node, force=True) 540 | self.write('if ') 541 | self.visit(node.test) 542 | self.write(':') 543 | self.body(node.body) 544 | while True: 545 | if len(node.orelse) == 1 and isinstance(node.orelse[0], If): 546 | node = node.orelse[0] 547 | self.newline(node.test, force=True) 548 | self.write('elif ') 549 | self.visit(node.test) 550 | self.write(':') 551 | self.body(node.body) 552 | else: 553 | if node.orelse: 554 | self.newline() 555 | self.write('else:') 556 | self.body(node.orelse) 557 | break 558 | 559 | def visit_AsyncFor(self, node): 560 | self.visit_For(node, True) 561 | 562 | def visit_For(self, node, async_=False): 563 | self.newline(node, force=True) 564 | if async_: 565 | self.write('async ') 566 | self.write('for ') 567 | self.visit_bare(node.target) 568 | self.write(' in ') 569 | self.visit(node.iter) 570 | self.write(':') 571 | self.body_or_else(node) 572 | 573 | def visit_While(self, node): 574 | self.newline(node, force=True) 575 | self.write('while ') 576 | self.visit(node.test) 577 | self.write(':') 578 | self.body_or_else(node) 579 | 580 | def visit_AsyncWith(self, node): 581 | self.visit_With(node, True) 582 | 583 | def visit_With(self, node, async_=False): 584 | self.newline(node, force=True) 585 | if async_: 586 | self.write('async ') 587 | self.write('with ') 588 | 589 | if hasattr(node, 'items'): 590 | sep = Sep(self.COMMA) 591 | for item in node.items: 592 | self.write(sep()) 593 | self.visit_withitem(item) 594 | else: 595 | # in python 2, similarly to the elif statement, multiple nested context managers 596 | # are generally the multi-form of a single with statement 597 | self.visit_withitem(node) 598 | while len(node.body) == 1 and isinstance(node.body[0], With): 599 | node = node.body[0] 600 | self.write(self.COMMA) 601 | self.visit_withitem(node) 602 | self.write(':') 603 | self.body(node.body) 604 | 605 | def visit_withitem(self, node): 606 | self.visit(node.context_expr) 607 | if node.optional_vars is not None: 608 | self.write(' as ') 609 | self.visit(node.optional_vars) 610 | 611 | def visit_Pass(self, node): 612 | self.newline(node) 613 | self.write('pass') 614 | 615 | def visit_Print(self, node): 616 | # XXX: python 2 only 617 | self.newline(node) 618 | self.write('print ') 619 | sep = Sep(self.COMMA) 620 | if node.dest is not None: 621 | self.write(' >> ') 622 | self.visit(node.dest) 623 | sep() 624 | for value in node.values: 625 | self.write(sep()) 626 | self.visit(value) 627 | if not node.nl: 628 | self.write(',') 629 | 630 | def visit_Delete(self, node): 631 | self.newline(node) 632 | self.write('del ') 633 | sep = Sep(self.COMMA) 634 | for target in node.targets: 635 | self.write(sep()) 636 | self.visit(target) 637 | 638 | def visit_Try(self, node): 639 | # Python 3 only. exploits the fact that TryExcept uses the same attribute names 640 | self.visit_TryExcept(node) 641 | if node.finalbody: 642 | self.newline() 643 | self.write('finally:') 644 | self.body(node.finalbody) 645 | 646 | def visit_TryExcept(self, node): 647 | self.newline(node, force=True) 648 | self.write('try:') 649 | self.body(node.body) 650 | for handler in node.handlers: 651 | self.visit(handler) 652 | if node.orelse: 653 | self.newline() 654 | self.write('else:') 655 | self.body(node.orelse) 656 | 657 | def visit_TryFinally(self, node): 658 | # Python 2 only 659 | if len(node.body) == 1 and isinstance(node.body[0], TryExcept): 660 | self.visit_TryExcept(node.body[0]) 661 | else: 662 | self.newline(node, force=True) 663 | self.write('try:') 664 | self.body(node.body) 665 | self.newline() 666 | self.write('finally:') 667 | self.body(node.finalbody) 668 | 669 | def visit_ExceptHandler(self, node): 670 | self.newline(node, force=True) 671 | self.write('except') 672 | if node.type: 673 | self.write(' ') 674 | self.visit(node.type) 675 | if node.name: 676 | self.write(' as ') 677 | # Compatability 678 | if isinstance(node.name, AST): 679 | self.visit(node.name) 680 | else: 681 | self.write(node.name) 682 | self.write(':') 683 | self.body(node.body) 684 | 685 | def visit_Global(self, node): 686 | self.newline(node) 687 | self.write('global ' + self.COMMA.join(node.names)) 688 | 689 | def visit_Nonlocal(self, node): 690 | self.newline(node) 691 | self.write('nonlocal ' + self.COMMA.join(node.names)) 692 | 693 | def visit_Return(self, node): 694 | self.newline(node) 695 | if node.value is not None: 696 | self.write('return ') 697 | self.visit(node.value) 698 | else: 699 | self.write('return') 700 | 701 | def visit_Break(self, node): 702 | self.newline(node) 703 | self.write('break') 704 | 705 | def visit_Continue(self, node): 706 | self.newline(node) 707 | self.write('continue') 708 | 709 | def visit_Raise(self, node): 710 | # XXX: Python 2.6 / 3.0 compatibility 711 | self.newline(node) 712 | if hasattr(node, 'exc') and node.exc is not None: 713 | self.write('raise ') 714 | self.visit(node.exc) 715 | if node.cause is not None: 716 | self.write(' from ') 717 | self.visit(node.cause) 718 | elif hasattr(node, 'type') and node.type is not None: 719 | self.write('raise ') 720 | self.visit(node.type) 721 | if node.inst is not None: 722 | self.write(self.COMMA) 723 | self.visit(node.inst) 724 | if node.tback is not None: 725 | self.write(self.COMMA) 726 | self.visit(node.tback) 727 | else: 728 | self.write('raise') 729 | 730 | # Expressions 731 | 732 | def visit_Attribute(self, node): 733 | self.maybe_break(node) 734 | # Edge case: due to the use of \d*[.]\d* for floats \d*[.]\w*, you have 735 | # to put parenthesis around an integer literal do get an attribute from it 736 | if isinstance(node.value, Num): 737 | self.paren_start() 738 | self.visit(node.value) 739 | self.paren_end() 740 | else: 741 | self.prec_start(17) 742 | self.visit(node.value) 743 | self.prec_end() 744 | self.write('.' + node.attr) 745 | 746 | def visit_Call(self, node): 747 | self.maybe_break(node) 748 | #need to put parenthesis around numbers being called (this makes no sense) 749 | if isinstance(node.func, Num): 750 | self.paren_start() 751 | self.visit_Num(node.func) 752 | self.paren_end() 753 | else: 754 | self.prec_start(17) 755 | self.visit(node.func) 756 | self.prec_end() 757 | # special case generator expressions as only argument 758 | if (len(node.args) == 1 and isinstance(node.args[0], GeneratorExp) and 759 | not node.keywords and hasattr(node, 'starargs') and 760 | not node.starargs and not node.kwargs): 761 | self.visit_GeneratorExp(node.args[0]) 762 | return 763 | 764 | self.paren_start() 765 | sep = Sep(self.COMMA) 766 | for arg in node.args: 767 | self.write(sep()) 768 | self.maybe_break(arg) 769 | self.visit(arg) 770 | for keyword in node.keywords: 771 | self.write(sep()) 772 | self.visit(keyword) 773 | if hasattr(node, 'starargs'): 774 | if node.starargs is not None: 775 | self.write(sep()) 776 | self.maybe_break(node.starargs) 777 | self.write('*') 778 | self.visit(node.starargs) 779 | if node.kwargs is not None: 780 | self.write(sep()) 781 | self.maybe_break(node.kwargs) 782 | self.write('**') 783 | self.visit(node.kwargs) 784 | self.paren_end() 785 | 786 | def visit_Name(self, node): 787 | self.maybe_break(node) 788 | self.write(node.id) 789 | 790 | def visit_NameConstant(self, node): 791 | self.maybe_break(node) 792 | self.write(repr(node.value)) 793 | 794 | def visit_Str(self, node, frombytes=False): 795 | self.maybe_break(node) 796 | if frombytes: 797 | newline_count = node.s.count('\n'.encode('utf-8')) 798 | else: 799 | newline_count = node.s.count('\n') 800 | 801 | # heuristic, expand when more than 1 newline and when at least 80% 802 | # of the characters aren't newlines 803 | expand = newline_count > 1 and len(node.s) > 5 * newline_count 804 | if self.correct_line_numbers: 805 | # Also check if we have enougn newlines to expand in if we're going for correct line numbers 806 | if self.after_colon: 807 | # Although this makes little sense just after a colon 808 | expand = expand and self.new_lines > newline_count 809 | else: 810 | expand = expand and self.new_lines >= newline_count 811 | 812 | if expand and (not self.correct_line_numbers or self.new_lines >= newline_count): 813 | if self.correct_line_numbers: 814 | self.new_lines -= newline_count 815 | 816 | a = repr(node.s) 817 | delimiter = a[-1] 818 | header, content = a[:-1].split(delimiter, 1) 819 | lines = [] 820 | chain = False 821 | for i in content.split('\\n'): 822 | if chain: 823 | i = lines.pop() + i 824 | chain = False 825 | if (len(i) - len(i.rstrip('\\'))) % 2: 826 | i += '\\n' 827 | chain = True 828 | lines.append(i) 829 | assert newline_count + 1 == len(lines) 830 | self.write(header) 831 | self.write(delimiter * 3) 832 | self.write('\n'.join(lines)) 833 | self.write(delimiter * 3) 834 | else: 835 | self.write(repr(node.s)) 836 | 837 | def visit_Bytes(self, node): 838 | self.visit_Str(node, True) 839 | 840 | def visit_Num(self, node): 841 | self.maybe_break(node) 842 | 843 | negative = (node.n.imag or node.n.real) < 0 and not PY3 844 | if negative: 845 | self.prec_start(self.UNARYOP_SYMBOLS[USub][1]) 846 | 847 | # 1e999 and related friends are parsed into inf 848 | if abs(node.n) == 1e999: 849 | if negative: 850 | self.write('-') 851 | self.write('1e999') 852 | if node.n.imag: 853 | self.write('j') 854 | else: 855 | self.write(repr(node.n)) 856 | 857 | if negative: 858 | self.prec_end() 859 | 860 | def visit_Tuple(self, node, guard=True): 861 | if guard or not node.elts: 862 | self.paren_start() 863 | sep = Sep(self.COMMA) 864 | for item in node.elts: 865 | self.write(sep()) 866 | self.visit(item) 867 | if len(node.elts) == 1: 868 | self.write(',') 869 | if guard or not node.elts: 870 | self.paren_end() 871 | 872 | def _sequence_visit(left, right): # pylint: disable=E0213 873 | def visit(self, node): 874 | self.maybe_break(node) 875 | self.paren_start(left) 876 | sep = Sep(self.COMMA) 877 | for item in node.elts: 878 | self.write(sep()) 879 | self.visit(item) 880 | self.paren_end(right) 881 | return visit 882 | 883 | visit_List = _sequence_visit('[', ']') 884 | visit_Set = _sequence_visit('{', '}') 885 | 886 | def visit_Dict(self, node): 887 | self.maybe_break(node) 888 | self.paren_start('{') 889 | sep = Sep(self.COMMA) 890 | for key, value in zip(node.keys, node.values): 891 | self.write(sep()) 892 | self.visit(key) 893 | self.write(self.COLON) 894 | self.visit(value) 895 | self.paren_end('}') 896 | 897 | def visit_BinOp(self, node): 898 | self.maybe_break(node) 899 | symbol, precedence = self.BINOP_SYMBOLS[type(node.op)] 900 | self.prec_start(precedence, type(node.op) != Pow) 901 | 902 | # work around python's negative integer literal optimization 903 | if isinstance(node.op, Pow): 904 | self.visit(node.left) 905 | self.prec_middle(14) 906 | else: 907 | self.visit(node.left) 908 | self.prec_middle() 909 | self.write(symbol) 910 | self.visit(node.right) 911 | self.prec_end() 912 | 913 | def visit_BoolOp(self, node): 914 | self.maybe_break(node) 915 | symbol, precedence = self.BOOLOP_SYMBOLS[type(node.op)] 916 | self.prec_start(precedence, True) 917 | self.prec_middle() 918 | sep = Sep(symbol) 919 | for value in node.values: 920 | self.write(sep()) 921 | self.visit(value) 922 | self.prec_end() 923 | 924 | def visit_Compare(self, node): 925 | self.maybe_break(node) 926 | self.prec_start(7, True) 927 | self.prec_middle() 928 | self.visit(node.left) 929 | for op, right in zip(node.ops, node.comparators): 930 | self.write(self.CMPOP_SYMBOLS[type(op)][0]) 931 | self.visit(right) 932 | self.prec_end() 933 | 934 | def visit_UnaryOp(self, node): 935 | self.maybe_break(node) 936 | symbol, precedence = self.UNARYOP_SYMBOLS[type(node.op)] 937 | self.prec_start(precedence) 938 | self.write(symbol) 939 | # workaround: in python 2, an explicit USub node around a number literal 940 | # indicates the literal was surrounded by parenthesis 941 | if (not PY3 and isinstance(node.op, USub) and isinstance(node.operand, Num) 942 | and (node.operand.n.real or node.operand.n.imag) >= 0): 943 | self.paren_start() 944 | self.visit(node.operand) 945 | self.paren_end() 946 | else: 947 | self.visit(node.operand) 948 | self.prec_end() 949 | 950 | def visit_Subscript(self, node): 951 | self.maybe_break(node) 952 | # have to surround literals by parenthesis (at least in Py2) 953 | if isinstance(node.value, Num): 954 | self.paren_start() 955 | self.visit_Num(node.value) 956 | self.paren_end() 957 | else: 958 | self.prec_start(17) 959 | self.visit(node.value) 960 | self.prec_end() 961 | self.paren_start('[') 962 | self.visit(node.slice) 963 | self.paren_end(']') 964 | 965 | def visit_Index(self, node, guard=False): 966 | # Index has no lineno information 967 | # When a subscript includes a tuple directly, the parenthesis can be dropped 968 | if not guard: 969 | self.visit_bare(node.value) 970 | else: 971 | self.visit(node.value) 972 | 973 | def visit_Slice(self, node): 974 | # Slice has no lineno information 975 | if node.lower is not None: 976 | self.visit(node.lower) 977 | self.write(':') 978 | if node.upper is not None: 979 | self.visit(node.upper) 980 | if node.step is not None: 981 | self.write(':') 982 | if not (isinstance(node.step, Name) and node.step.id == 'None'): 983 | self.visit(node.step) 984 | 985 | def visit_Ellipsis(self, node): 986 | # Ellipsis has no lineno information 987 | self.write('...') 988 | 989 | def visit_ExtSlice(self, node): 990 | # Extslice has no lineno information 991 | for idx, item in enumerate(node.dims): 992 | if idx: 993 | self.write(self.COMMA) 994 | if isinstance(item, Index): 995 | self.visit_Index(item, True) 996 | else: 997 | self.visit(item) 998 | 999 | def visit_Yield(self, node, paren=True): 1000 | # yield can only be used in a statement context, or we're between parenthesis 1001 | self.maybe_break(node) 1002 | if paren: 1003 | self.paren_start() 1004 | if node.value is not None: 1005 | self.write('yield ') 1006 | self.visit_bare(node.value) 1007 | else: 1008 | self.write('yield') 1009 | if paren: 1010 | self.paren_end() 1011 | 1012 | def visit_YieldFrom(self, node, paren=True): 1013 | # Even though yield and yield from technically occupy precedence level 1, certain code 1014 | # using them is illegal e.g. "return yield from a()" will not work unless you 1015 | # put the yield from statement within parenthesis. 1016 | self.maybe_break(node) 1017 | if paren: 1018 | self.paren_start() 1019 | self.write('yield from ') 1020 | self.visit(node.value) 1021 | if paren: 1022 | self.paren_end() 1023 | 1024 | def visit_Lambda(self, node): 1025 | self.maybe_break(node) 1026 | self.prec_start(2) 1027 | self.write('lambda ') 1028 | self.visit_arguments(node.args) 1029 | self.write(self.COLON) 1030 | self.visit(node.body) 1031 | self.prec_end() 1032 | 1033 | def _generator_visit(left, right): 1034 | def visit(self, node): 1035 | self.maybe_break(node) 1036 | self.paren_start(left) 1037 | self.visit(node.elt) 1038 | for comprehension in node.generators: 1039 | self.visit(comprehension) 1040 | self.paren_end(right) 1041 | return visit 1042 | 1043 | visit_ListComp = _generator_visit('[', ']') 1044 | visit_GeneratorExp = _generator_visit('(', ')') 1045 | visit_SetComp = _generator_visit('{', '}') 1046 | 1047 | def visit_DictComp(self, node): 1048 | self.maybe_break(node) 1049 | self.paren_start('{') 1050 | self.visit(node.key) 1051 | self.write(self.COLON) 1052 | self.visit(node.value) 1053 | for comprehension in node.generators: 1054 | self.visit(comprehension) 1055 | self.paren_end('}') 1056 | 1057 | def visit_IfExp(self, node): 1058 | self.maybe_break(node) 1059 | self.prec_start(3, False) 1060 | self.visit(node.body) 1061 | self.write(' if ') 1062 | self.visit(node.test) 1063 | self.prec_middle(2) 1064 | self.write(' else ') 1065 | self.visit(node.orelse) 1066 | self.prec_end() 1067 | 1068 | def visit_Starred(self, node): 1069 | self.maybe_break(node) 1070 | self.write('*') 1071 | self.visit(node.value) 1072 | 1073 | def visit_Repr(self, node): 1074 | # XXX: python 2.6 only 1075 | self.maybe_break(node) 1076 | self.write('`') 1077 | self.visit(node.value) 1078 | self.write('`') 1079 | 1080 | # Helper Nodes 1081 | 1082 | def visit_alias(self, node): 1083 | # alias does not have line number information 1084 | self.write(node.name) 1085 | if node.asname is not None: 1086 | self.write(' as ' + node.asname) 1087 | 1088 | def visit_comprehension(self, node): 1089 | self.maybe_break(node.target) 1090 | self.write(' for ') 1091 | self.visit_bare(node.target) 1092 | self.write(' in ') 1093 | # workaround: lambda and ternary need to be within parenthesis here 1094 | self.prec_start(4) 1095 | self.visit(node.iter) 1096 | self.prec_end() 1097 | 1098 | for if_ in node.ifs: 1099 | self.write(' if ') 1100 | self.visit(if_) 1101 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to pytest-ast-back-to-python 2 | 3 | A plugin for pytest devs to view how assertion rewriting recodes the AST 4 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pytest-ast-back-to-python 2 | site_description: A plugin for pytest devs to view how assertion rewriting recodes the AST 3 | 4 | theme: readthedocs 5 | 6 | repo_url: https://github.com/tomviner/pytest-ast-back-to-python 7 | -------------------------------------------------------------------------------- /pytest_ast_back_to_python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | from _pytest.assertion.rewrite import rewrite_asserts 5 | from _pytest.monkeypatch import MonkeyPatch 6 | 7 | import codegen 8 | 9 | 10 | def pytest_addoption(parser): 11 | group = parser.getgroup('ast-back-to-python') 12 | group.addoption( 13 | '--show-ast-as-python', 14 | action='store_true', 15 | dest='ast_as_python', 16 | default=False, 17 | help='Show how assertion rewriting recoded the AST.' 18 | ) 19 | 20 | 21 | def pytest_configure(config): 22 | config._ast_as_python = AstAsPython() 23 | config.pluginmanager.register(config._ast_as_python) 24 | 25 | 26 | def make_replacement_rewrite_asserts(store): 27 | 28 | def replacement_rewrite_asserts(mod, *args, **kwargs): 29 | rewrite_asserts(mod, *args, **kwargs) 30 | store.append(codegen.to_source(mod)) 31 | 32 | return replacement_rewrite_asserts 33 | 34 | 35 | class AstAsPython(object): 36 | def __init__(self): 37 | self.store = [] 38 | 39 | def pytest_configure(self, config): 40 | if not config.getoption('ast_as_python'): 41 | return 42 | 43 | mp = MonkeyPatch() 44 | mp.setattr( 45 | '_pytest.assertion.rewrite.rewrite_asserts', 46 | make_replacement_rewrite_asserts(self.store)) 47 | 48 | # written pyc files will bypass our patch, so disable reading them 49 | mp.setattr( 50 | '_pytest.assertion.rewrite._read_pyc', 51 | lambda source, pyc, trace=None: None) 52 | 53 | config._cleanup.append(mp.undo) 54 | 55 | def pytest_terminal_summary(self, terminalreporter): 56 | if not terminalreporter.config.getoption('ast_as_python'): 57 | return 58 | 59 | for source in self.store: 60 | terminalreporter._tw.sep("=", "Rewritten AST as Python") 61 | terminalreporter.write(source) 62 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import codecs 6 | from setuptools import setup 7 | 8 | 9 | def read(fname): 10 | file_path = os.path.join(os.path.dirname(__file__), fname) 11 | return codecs.open(file_path, encoding='utf-8').read() 12 | 13 | 14 | setup( 15 | name='pytest-ast-back-to-python', 16 | version='1.2.0', 17 | author='Tom Viner', 18 | author_email='code@viner.tv', 19 | maintainer='Tom Viner', 20 | maintainer_email='code@viner.tv', 21 | license='BSD-3', 22 | url='https://github.com/tomviner/pytest-ast-back-to-python', 23 | description='A plugin for pytest devs to view how assertion rewriting recodes the AST', 24 | long_description=read('README.rst'), 25 | py_modules=['pytest_ast_back_to_python', 'codegen'], 26 | install_requires=['pytest'], 27 | classifiers=[ 28 | 'Development Status :: 4 - Beta', 29 | 'Intended Audience :: Developers', 30 | 'Topic :: Software Development :: Testing', 31 | 'Programming Language :: Python', 32 | 'Programming Language :: Python :: 2', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.3', 36 | 'Programming Language :: Python :: 3.4', 37 | 'Programming Language :: Python :: 3.5', 38 | 'Programming Language :: Python :: Implementation :: CPython', 39 | 'Programming Language :: Python :: Implementation :: PyPy', 40 | 'Operating System :: OS Independent', 41 | 'License :: OSI Approved :: BSD License', 42 | ], 43 | entry_points={ 44 | 'pytest11': [ 45 | 'ast-back-to-python = pytest_ast_back_to_python', 46 | ], 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = 'pytester' 2 | -------------------------------------------------------------------------------- /tests/test_ast_back_to_python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def test_ast_as_python_on(testdir): 5 | """Given I use the cmd line option, I should see rewritten AST as Python.""" 6 | 7 | # create a temporary pytest test module 8 | testdir.makepyfile(""" 9 | def test_ast_as_python_on(request): 10 | assert request.config.getoption('ast_as_python') 11 | """) 12 | 13 | # run pytest with the following cmd args 14 | result = testdir.runpytest( 15 | '--show-ast-as-python', 16 | '-v' 17 | ) 18 | 19 | # fnmatch_lines does an assertion internally 20 | result.stdout.fnmatch_lines([ 21 | '*::test_ast_as_python_on PASSED*', 22 | ]) 23 | # The expression within the assert statement should be broken down to 24 | # constituent parts like this: 25 | result.stdout.fnmatch_lines([ 26 | '*@py_assert1 = request.config', 27 | "*@py_assert3 = @py_assert1.getoption", 28 | "*@py_assert5 = 'ast_as_python'", 29 | '*@py_assert7 = @py_assert3(@py_assert5)', 30 | ]) 31 | 32 | # make sure that that we get a '0' exit code for the testsuite 33 | assert result.ret == 0 34 | 35 | def test_ast_as_python_off(testdir): 36 | """Given no cmd line option, I should see no rewritten AST as Python.""" 37 | 38 | # create a temporary pytest test module 39 | testdir.makepyfile(""" 40 | def test_ast_as_python_off(request): 41 | assert not request.config.getoption('ast_as_python') 42 | """) 43 | 44 | # run pytest with the following cmd args 45 | result = testdir.runpytest( 46 | '-v' 47 | ) 48 | 49 | # fnmatch_lines does an assertion internally 50 | result.stdout.fnmatch_lines([ 51 | '*::test_ast_as_python_off PASSED*', 52 | ]) 53 | assert '@py_assert' not in result.stdout.str() 54 | 55 | # make sure that that we get a '0' exit code for the testsuite 56 | assert result.ret == 0 57 | 58 | 59 | def test_help_message(testdir): 60 | result = testdir.runpytest( 61 | '--help', 62 | ) 63 | # fnmatch_lines does an assertion internally 64 | result.stdout.fnmatch_lines([ 65 | 'ast-back-to-python:', 66 | '*--show-ast-as-python*Show how assertion rewriting recoded the AST.', 67 | ]) 68 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # For more information about tox, see https://tox.readthedocs.org/en/latest/ 2 | [tox] 3 | envlist = 4 | py{27,34,35,36,37,38,39}-pytest{30,31,32,33,34,35,36,37,38,39,310,40,41,42,43,44,45,46} 5 | py{35,36,37,38,39}-pytest{50,51} 6 | 7 | 8 | [testenv] 9 | usedevelop = true 10 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 11 | deps = 12 | pytest30: pytest~=3.0.0 13 | pytest31: pytest~=3.1.0 14 | pytest32: pytest~=3.2.0 15 | pytest33: pytest~=3.3.0 16 | pytest34: pytest~=3.4.0 17 | pytest35: pytest~=3.5.0 18 | pytest{30,31,32,33,34,35}: pytest-cov<2.6.0 19 | pytest{36,37,38,39,310,40,41,42,43,44,45,46,50,51}: pytest-cov 20 | pytest36: pytest~=3.6.0 21 | pytest37: pytest~=3.7.0 22 | pytest38: pytest~=3.8.0 23 | pytest39: pytest~=3.9.0 24 | pytest310: pytest~=3.10.0 25 | pytest40: pytest~=4.0.0 26 | pytest41: pytest~=4.1.0 27 | pytest42: pytest~=4.2.0 28 | pytest43: pytest~=4.3.0 29 | pytest44: pytest~=4.4.0 30 | pytest45: pytest~=4.5.0 31 | pytest46: pytest~=4.6.0 32 | pytest50: pytest~=5.0.0 33 | pytest51: pytest~=5.1.0 34 | commands = pytest --cov pytest_ast_back_to_python --cov-report term-missing {posargs:tests} 35 | --------------------------------------------------------------------------------