├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── astexport ├── __init__.py ├── __main__.py ├── cli.py ├── export.py ├── parse.py └── version.py ├── setup.py └── tests ├── export_test.py ├── parse_test.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | *.egg 5 | *.py[cod] 6 | __pycache__/ 7 | *.so 8 | *~ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | cache: pip 6 | 7 | python: 8 | - 3.5 9 | - 3.6 10 | - 3.7 11 | - pypy3 12 | 13 | install: 14 | - pip install --upgrade pip 15 | - "pip install .[dev]" 16 | 17 | before_script: 18 | - pip freeze 19 | 20 | script: 21 | - make linter || true 22 | - nosetests 23 | - python -m astexport --help 24 | - echo "x = 5" | python -m astexport --pretty 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015, Federico Poli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | python -m nose 3 | 4 | install: 5 | python -m pip install . --upgrade 6 | 7 | run-help: 8 | python -m astexport --help 9 | 10 | linter: 11 | pep8 --ignore=E251 . 12 | 13 | dist-upload: clean linter test 14 | python setup.py sdist bdist_wheel 15 | twine upload dist/* 16 | 17 | clean: 18 | rm -rf dist 19 | rm -rf build 20 | rm -rf *.egg-info/ 21 | rm -rf */__pycache__ 22 | 23 | .PHONY: test linter dist-upload clean 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-astexport 2 | ================ 3 | 4 | .. image:: https://travis-ci.org/fpoli/python-astexport.svg?branch=master 5 | :target: https://travis-ci.org/fpoli/python-astexport 6 | 7 | Python command line application to export Python 3 AST as JSON. 8 | 9 | Python 2.7 AST was used up to version 0.2.1 of this library. 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | .. code-block:: bash 16 | 17 | $ pip install astexport 18 | 19 | 20 | Usage 21 | ----- 22 | 23 | .. code-block:: bash 24 | 25 | $ astexport < example.py > ast.json 26 | 27 | .. code-block:: bash 28 | 29 | $ astexport --help 30 | 31 | 32 | License 33 | ------- 34 | 35 | Copyright (c) 2015, Federico Poli 36 | 37 | Released under the MIT license. 38 | -------------------------------------------------------------------------------- /astexport/__init__.py: -------------------------------------------------------------------------------- 1 | from astexport.version import __version__ 2 | 3 | __prog_name__ = "astexport" 4 | __version__ = __version__ 5 | -------------------------------------------------------------------------------- /astexport/__main__.py: -------------------------------------------------------------------------------- 1 | from astexport.cli import main 2 | main() 3 | -------------------------------------------------------------------------------- /astexport/cli.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import argparse 3 | from astexport import __version__, __prog_name__ 4 | from astexport.parse import parse 5 | from astexport.export import export_json 6 | 7 | 8 | def create_parser(): 9 | parser = argparse.ArgumentParser( 10 | prog=__prog_name__, 11 | description="Python source code in, JSON AST out. (v{})".format( 12 | __version__ 13 | ) 14 | ) 15 | parser.add_argument( 16 | "-i", "--input", 17 | default="-", 18 | help="file to read from or '-' to use standard input (default)" 19 | ) 20 | parser.add_argument( 21 | "-p", "--pretty", 22 | action="store_true", 23 | help="print indented JSON" 24 | ) 25 | parser.add_argument( 26 | "-v", "--version", 27 | action="store_true", 28 | help="print version and exit" 29 | ) 30 | return parser 31 | 32 | 33 | def main(): 34 | """Read source from stdin, parse and export the AST as JSON""" 35 | parser = create_parser() 36 | args = parser.parse_args() 37 | if args.version: 38 | print("{} version {}".format(__prog_name__, __version__)) 39 | return 40 | source = "".join(fileinput.input(args.input)) 41 | tree = parse(source) 42 | json = export_json(tree, args.pretty) 43 | print(json) 44 | -------------------------------------------------------------------------------- /astexport/export.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import json 3 | 4 | 5 | def export_json(tree, pretty_print=False): 6 | if not isinstance(tree, ast.AST): 7 | raise ValueError( 8 | "The argument of export_json(..) must be of type AST, not '{}'".format( 9 | type(tree) 10 | ) 11 | ) 12 | return json.dumps( 13 | export_dict(tree), 14 | indent=4 if pretty_print else None, 15 | sort_keys=True, 16 | separators=(",", ": ") if pretty_print else (",", ":") 17 | ) 18 | 19 | 20 | def export_dict(tree): 21 | if not isinstance(tree, ast.AST): 22 | raise ValueError( 23 | "The argument of export_dict(..) must be of type AST, not '{}'".format( 24 | type(tree) 25 | ) 26 | ) 27 | return DictExportVisitor().visit(tree) 28 | 29 | 30 | class DictExportVisitor: 31 | ast_type_field = "ast_type" 32 | 33 | def visit(self, node): 34 | node_type = node.__class__.__name__ 35 | meth = getattr(self, "visit_" + node_type, self.default_visit) 36 | return meth(node) 37 | 38 | def default_visit(self, node): 39 | node_type = node.__class__.__name__ 40 | # Add node type 41 | args = { 42 | self.ast_type_field: node_type 43 | } 44 | # Visit fields 45 | for field in node._fields: 46 | assert field != self.ast_type_field 47 | meth = getattr( 48 | self, "visit_field_" + node_type + "_" + field, 49 | self.default_visit_field 50 | ) 51 | args[field] = meth(getattr(node, field)) 52 | # Visit attributes 53 | for attr in node._attributes: 54 | assert attr != self.ast_type_field 55 | meth = getattr( 56 | self, "visit_attribute_" + node_type + "_" + attr, 57 | self.default_visit_field 58 | ) 59 | # Use None as default when lineno/col_offset are not set 60 | args[attr] = meth(getattr(node, attr, None)) 61 | return args 62 | 63 | def default_visit_field(self, val): 64 | if isinstance(val, ast.AST): 65 | return self.visit(val) 66 | if isinstance(val, (list, tuple)): 67 | return [self.visit(x) for x in val] 68 | return val 69 | 70 | # Special visitors 71 | 72 | def visit_str(self, val): 73 | return str(val) 74 | 75 | def visit_Bytes(self, val): 76 | return str(val.s) 77 | 78 | def visit_NoneType(self, val): 79 | del val # Unused 80 | return None 81 | 82 | def visit_field_NameConstant_value(self, val): 83 | return str(val) 84 | 85 | def visit_field_Num_n(self, val): 86 | if isinstance(val, int): 87 | return { 88 | self.ast_type_field: "int", 89 | "n": val, 90 | # JavaScript integers are limited to 2**53 - 1 bits, 91 | # so we add a string representation of the integer 92 | "n_str": str(val), 93 | } 94 | if isinstance(val, float): 95 | return { 96 | self.ast_type_field: "float", 97 | "n": val 98 | } 99 | if isinstance(val, complex): 100 | return { 101 | self.ast_type_field: "complex", 102 | "n": val.real, 103 | "i": val.imag 104 | } 105 | -------------------------------------------------------------------------------- /astexport/parse.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | 4 | def parse(source): 5 | if not isinstance(source, str): 6 | raise ValueError( 7 | "The argument of parse(..) must be of type string, not '{}'".format( 8 | type(source) 9 | ) 10 | ) 11 | tree = ast.parse(source) 12 | return tree 13 | -------------------------------------------------------------------------------- /astexport/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.3.0" 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | from codecs import open 5 | from os import path 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | description = "Python command line application to export Python AST as Json." 10 | 11 | main_ns = {} 12 | with open("astexport/version.py") as ver_file: 13 | exec(ver_file.read(), main_ns) 14 | 15 | with open(path.join(here, "README.rst"), encoding="utf-8") as f: 16 | long_descr = f.read() 17 | 18 | setup( 19 | name = "astexport", 20 | version = main_ns['__version__'], 21 | 22 | description = description, 23 | long_description = long_descr, 24 | license = "MIT", 25 | url = "https://github.com/fpoli/python-astexport", 26 | 27 | author = "Federico Poli", 28 | author_email = "federpoli@gmail.com", 29 | 30 | packages = find_packages(exclude=["tests"]), 31 | 32 | entry_points = { 33 | "console_scripts": [ 34 | "astexport = astexport.cli:main" 35 | ] 36 | }, 37 | 38 | install_requires = [ 39 | ], 40 | extras_require = { 41 | "dev": [ 42 | "twine", 43 | "nose == 1.3.3", 44 | "pep8 == 1.4.6" 45 | ] 46 | }, 47 | 48 | classifiers = [ 49 | "Development Status :: 4 - Beta", 50 | "Environment :: Console", 51 | "Intended Audience :: Developers", 52 | "Topic :: Software Development :: Compilers", 53 | "Topic :: Software Development :: Disassemblers", 54 | "License :: OSI Approved :: MIT License", 55 | "Operating System :: OS Independent", 56 | "Programming Language :: Python :: 3", 57 | "Programming Language :: Python :: 3.5", 58 | "Programming Language :: Python :: 3.6" 59 | ] 60 | ) 61 | -------------------------------------------------------------------------------- /tests/export_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | import unittest 4 | import ast 5 | import json 6 | from astexport.export import export_json 7 | from test import TestIO 8 | 9 | 10 | class TestExportJson(unittest.TestCase): 11 | maxDiff = None 12 | 13 | def test_export_json(self): 14 | for i, test in enumerate(self.tests): 15 | with self.subTest(test=i): 16 | result = json.loads(export_json(test.input)) 17 | expected = test.output 18 | self.assertEqual(result, expected) 19 | 20 | tests = [ 21 | TestIO( 22 | input = ast.Module( 23 | body = [ 24 | ast.Assign( 25 | targets = [ 26 | ast.Name( 27 | ctx = ast.Store(), 28 | id = "x" 29 | ) 30 | ], 31 | value = ast.Num(n = 5) 32 | ) 33 | ] 34 | ), 35 | output = { 36 | "ast_type": "Module", 37 | "body": [ 38 | { 39 | "ast_type": "Assign", 40 | "col_offset": None, 41 | "lineno": None, 42 | "targets": [ 43 | { 44 | "ast_type": "Name", 45 | "col_offset": None, 46 | "ctx": { 47 | "ast_type": "Store" 48 | }, 49 | "id": "x", 50 | "lineno": None 51 | } 52 | ], 53 | "value": { 54 | "ast_type": "Num", 55 | "col_offset": None, 56 | "lineno": None, 57 | "n": { 58 | "ast_type": "int", 59 | "n": 5, 60 | "n_str": "5" 61 | } 62 | } 63 | } 64 | ] 65 | } 66 | ), 67 | TestIO( 68 | input = ast.Module( 69 | body = [ 70 | ast.Expr( 71 | col_offset = 0, 72 | lineno = 1, 73 | value = ast.Call( 74 | col_offset = 0, 75 | lineno = 1, 76 | func = ast.Name( 77 | col_offset = 0, 78 | lineno = 1, 79 | id = "foobar", 80 | ctx = ast.Load() 81 | ), 82 | args = [], 83 | keywords = [], 84 | starargs = None, 85 | kwargs = None 86 | ) 87 | ) 88 | ] 89 | ), 90 | output = { 91 | "ast_type": "Module", 92 | "body": [ 93 | { 94 | "ast_type": "Expr", 95 | "col_offset": 0, 96 | "lineno": 1, 97 | "value": { 98 | "args": [], 99 | "ast_type": "Call", 100 | "col_offset": 0, 101 | "func": { 102 | "ast_type": "Name", 103 | "col_offset": 0, 104 | "ctx": { 105 | "ast_type": "Load" 106 | }, 107 | "id": "foobar", 108 | "lineno": 1 109 | }, 110 | "keywords": [], 111 | "lineno": 1 112 | } 113 | } 114 | ] 115 | } 116 | ), 117 | TestIO( 118 | input = ast.NameConstant(None), 119 | output = { 120 | "ast_type": "NameConstant", 121 | "col_offset": None, 122 | "lineno": None, 123 | "value": "None" 124 | } 125 | ), 126 | TestIO( 127 | input = ast.NameConstant(True), 128 | output = { 129 | "ast_type": "NameConstant", 130 | "col_offset": None, 131 | "lineno": None, 132 | "value": "True" 133 | } 134 | ), 135 | TestIO( 136 | input = ast.NameConstant(False), 137 | output = { 138 | "ast_type": "NameConstant", 139 | "col_offset": None, 140 | "lineno": None, 141 | "value": "False" 142 | } 143 | ), 144 | TestIO( 145 | input = ast.Module( 146 | body = [ 147 | ast.Global( 148 | names = ["x"], 149 | lineno = 1, 150 | col_offset = 0 151 | ) 152 | ] 153 | ), 154 | output = { 155 | "body": [ 156 | { 157 | "lineno": 1, 158 | "col_offset": 0, 159 | "ast_type": "Global", 160 | "names": ["x"] 161 | } 162 | ], 163 | "ast_type": "Module" 164 | } 165 | ), 166 | TestIO( 167 | input = ast.FunctionDef( 168 | name = "function", 169 | args = ast.arguments( 170 | args = [], 171 | vararg = None, 172 | kwonlyargs = [ 173 | ast.arg( 174 | arg = "x", 175 | annotation = None, 176 | lineno = 1, 177 | col_offset = 16 178 | ) 179 | ], 180 | kw_defaults = [None], 181 | kwarg = None, 182 | defaults = [] 183 | ), 184 | body = [ 185 | ast.Pass(lineno = 1, col_offset = 20) 186 | ], 187 | decorator_list = [], 188 | returns = None, 189 | lineno = 1, 190 | col_offset = 0 191 | ), 192 | output = { 193 | "args": { 194 | "args": [], 195 | "ast_type": "arguments", 196 | "defaults": [], 197 | "kw_defaults": [None], 198 | "kwarg": None, 199 | "kwonlyargs": [ 200 | { 201 | "annotation": None, 202 | "arg": "x", 203 | "ast_type": "arg", 204 | "col_offset": 16, 205 | "lineno": 1 206 | } 207 | ], 208 | "vararg": None 209 | }, 210 | "ast_type": "FunctionDef", 211 | "body": [ 212 | { 213 | "ast_type": "Pass", 214 | "col_offset": 20, 215 | "lineno": 1 216 | } 217 | ], 218 | "col_offset": 0, 219 | "decorator_list": [], 220 | "lineno": 1, 221 | "name": "function", 222 | "returns": None 223 | } 224 | ), 225 | ] 226 | -------------------------------------------------------------------------------- /tests/parse_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import unittest 5 | import ast 6 | from astexport.parse import parse 7 | from test import TestIO 8 | 9 | 10 | class TestParse(unittest.TestCase): 11 | 12 | def test_parse(self): 13 | for i, test in enumerate(self.tests): 14 | with self.subTest(test=i): 15 | result = parse(test.input) 16 | expected = ast.fix_missing_locations(test.output) 17 | self.assertEqual( 18 | ast.dump(result, include_attributes=True), 19 | ast.dump(expected, include_attributes=True) 20 | ) 21 | 22 | tests = [ 23 | TestIO( 24 | input = "x = 5", 25 | output = ast.Module( 26 | body = [ 27 | ast.Assign( 28 | targets = [ 29 | ast.Name( 30 | ctx = ast.Store(), 31 | id = "x" 32 | ) 33 | ], 34 | value = ast.Num( 35 | n = 5, 36 | col_offset=4 37 | ) 38 | ) 39 | ] 40 | ) 41 | ), 42 | TestIO( 43 | input = "foobar()", 44 | output = ast.Module( 45 | body = [ 46 | ast.Expr( 47 | col_offset = 0, 48 | lineno = 1, 49 | value = ast.Call( 50 | col_offset = 0, 51 | lineno = 1, 52 | func = ast.Name( 53 | col_offset = 0, 54 | lineno = 1, 55 | id = "foobar", 56 | ctx = ast.Load() 57 | ), 58 | args = [], 59 | keywords = [], 60 | starargs = None, 61 | kwargs = None 62 | ) 63 | ) 64 | ] 65 | ) 66 | ), 67 | TestIO( 68 | input = "global x", 69 | output = ast.Module( 70 | body = [ 71 | ast.Global( 72 | names = ["x"], 73 | lineno = 1, 74 | col_offset = 0 75 | ) 76 | ] 77 | ) 78 | ), 79 | TestIO( 80 | input = "def function(*, x): pass", 81 | output = ast.Module( 82 | body = [ 83 | ast.FunctionDef( 84 | name = "function", 85 | args = ast.arguments( 86 | args = [], 87 | vararg = None, 88 | kwonlyargs = [ 89 | ast.arg( 90 | arg = "x", 91 | annotation = None, 92 | lineno = 1, 93 | col_offset = 16 94 | ) 95 | ], 96 | kw_defaults = [None], 97 | kwarg = None, 98 | defaults = [] 99 | ), 100 | body = [ 101 | ast.Pass(lineno = 1, col_offset = 20) 102 | ], 103 | decorator_list = [], 104 | returns = None, 105 | lineno = 1, 106 | col_offset = 0 107 | ) 108 | ] 109 | ) 110 | ) 111 | ] 112 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from collections import namedtuple 5 | 6 | TestIO = namedtuple("Test", "input output") 7 | --------------------------------------------------------------------------------