├── __init__.py ├── MANIFEST.in ├── nfy.py ├── tests ├── __init__.py ├── test_unsafe_transforms.py ├── test_safe_transforms.py └── test_source_emission.py ├── .coveragerc ├── README.md ├── .gitignore ├── setup.py ├── Python.asdl ├── docs ├── make.bat ├── Makefile ├── index.rst └── conf.py ├── Grammar ├── LICENSE └── mnfy.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py 2 | include docs/*.rst 3 | LICENSE 4 | -------------------------------------------------------------------------------- /nfy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.4 2 | import runpy 3 | 4 | runpy.run_module('mnfy', run_name='__main__') 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Use ``python3 -m unittest discover tests`` from the top-level project 2 | # directory to execute all tests. 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | # Regexes for lines to exclude from consideration 3 | exclude_lines = 4 | pragma: no cover 5 | self.fail 6 | if __name__ == '__main__': 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mnfy 2 | ==== 3 | 4 | Minify Python code 5 | 6 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 7 | 8 | [Docs at Read the Docs](http://mnfy.rtfd.org) 9 | 10 | [On PyPI](https://pypi.python.org/pypi/mnfy) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | *.swp 3 | *.pyc 4 | *.pyo 5 | .coverage 6 | coverage 7 | htmlcov 8 | dist 9 | venv 10 | 11 | *.py[cod] 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Packages 17 | *.egg 18 | *.egg-info 19 | dist 20 | build 21 | eggs 22 | parts 23 | bin 24 | var 25 | sdist 26 | develop-eggs 27 | .installed.cfg 28 | lib 29 | lib64 30 | 31 | # Installer logs 32 | pip-log.txt 33 | 34 | # Unit test / coverage reports 35 | .coverage 36 | .tox 37 | nosetests.xml 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Mr Developer 43 | .mr.developer.cfg 44 | .project 45 | .pydevproject 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | import sys 4 | 5 | with open('docs/index.rst') as file: 6 | index_text = file.read() 7 | 8 | long_description = index_text.partition('.. END README')[0] 9 | long_description += index_text.rpartition('.. LINKS')[2] 10 | 11 | version = '34.0.0' 12 | assert version.startswith('{}{}'.format(*sys.version_info)) 13 | 14 | py_modules = ['mnfy'] # Don't install test code since not in a package. 15 | if os.environ.get('MNFY_RICHARD_JONES'): 16 | py_modules.append('nfy') 17 | 18 | setup( 19 | name='mnfy', 20 | # First digit is the Massive/feature version of Python, rest are 21 | # feature/bugfix for mnfy. 22 | version=version, 23 | author='Brett Cannon', 24 | author_email='brett@python.org', 25 | url='http://mnfy.rtfd.org/', 26 | py_modules=py_modules, 27 | license='Apache Licence 2.0', 28 | description='Minify/obfuscate Python 3 source code', 29 | long_description=long_description, 30 | classifiers=[ 31 | 'Development Status :: 5 - Production/Stable', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: Apache Software License', 34 | 'Programming Language :: Python :: 3 :: Only', 35 | 'Programming Language :: Python :: 3.4', 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /tests/test_unsafe_transforms.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | import mnfy 5 | 6 | 7 | class FunctionToLambdaTests(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.transform = mnfy.FunctionToLambda() 11 | 12 | def _test_failure(self, fxn_code): 13 | fxn_ast = ast.parse(fxn_code) 14 | new_ast = self.transform.visit(fxn_ast) 15 | new_fxn = new_ast.body[0] 16 | self.assertIsInstance(new_fxn, ast.FunctionDef, 17 | '{} not an instance of ast.FunctionDef'.format(new_ast.__class__)) 18 | 19 | def test_decorator_fail(self): 20 | self._test_failure('@dec\ndef X(): return') 21 | 22 | def test_returns_annotation_fail(self): 23 | self._test_failure('def X()->None: return') 24 | 25 | def test_body_too_long_fail(self): 26 | self._test_failure('def X(): x = 2 + 3; return x') 27 | 28 | def test_body_not_return_fail(self): 29 | self._test_failure('def X(): Y()') 30 | 31 | def test_no_vararg_annotation_fail(self): 32 | self._test_failure('def X(*arg:None): return') 33 | 34 | def test_no_kwarg_annotation_fail(self): 35 | self._test_failure('def X(**kwargs:None): return') 36 | 37 | def test_no_arg_annotation_fail(self): 38 | self._test_failure('def X(a, b:None, c): return') 39 | 40 | def test_success(self): 41 | module = ast.parse('def identity(): return 42') 42 | fxn = module.body[0] 43 | new_ast = self.transform.visit(module) 44 | assign = new_ast.body[0] 45 | self.assertIsInstance(assign, ast.Assign) 46 | self.assertEqual(len(assign.targets), 1) 47 | target = assign.targets[0] 48 | self.assertIsInstance(target, ast.Name) 49 | self.assertEqual(target.id, 'identity') 50 | self.assertIsInstance(target.ctx, ast.Store) 51 | lmda = assign.value 52 | self.assertIsInstance(lmda, ast.Lambda) 53 | self.assertIs(lmda.args, fxn.args) 54 | self.assertIs(lmda.body, fxn.body[0].value) 55 | 56 | def test_return_None(self): 57 | # If a function has a bare return then the lambda should return None. 58 | module = ast.parse('def fxn(): return') 59 | new_ast = self.transform.visit(module) 60 | lambda_ = new_ast.body[0].value 61 | self.assertIsInstance(lambda_.body, ast.Name) 62 | self.assertEqual(lambda_.body.id, 'None') 63 | self.assertIsInstance(lambda_.body.ctx, ast.Load) 64 | 65 | unittest.skip('not implemented') 66 | def test_empty_return(self): 67 | pass 68 | 69 | 70 | if __name__ == '__main__': 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /Python.asdl: -------------------------------------------------------------------------------- 1 | -- ASDL's six builtin types are identifier, int, string, bytes, object, singleton 2 | 3 | module Python 4 | { 5 | mod = Module(stmt* body) 6 | | Interactive(stmt* body) 7 | | Expression(expr body) 8 | 9 | -- not really an actual node but useful in Jython's typesystem. 10 | | Suite(stmt* body) 11 | 12 | stmt = FunctionDef(identifier name, arguments args, 13 | stmt* body, expr* decorator_list, expr? returns) 14 | | ClassDef(identifier name, 15 | expr* bases, 16 | keyword* keywords, 17 | expr? starargs, 18 | expr? kwargs, 19 | stmt* body, 20 | expr* decorator_list) 21 | | Return(expr? value) 22 | 23 | | Delete(expr* targets) 24 | | Assign(expr* targets, expr value) 25 | | AugAssign(expr target, operator op, expr value) 26 | 27 | -- use 'orelse' because else is a keyword in target languages 28 | | For(expr target, expr iter, stmt* body, stmt* orelse) 29 | | While(expr test, stmt* body, stmt* orelse) 30 | | If(expr test, stmt* body, stmt* orelse) 31 | | With(withitem* items, stmt* body) 32 | 33 | | Raise(expr? exc, expr? cause) 34 | | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 35 | | Assert(expr test, expr? msg) 36 | 37 | | Import(alias* names) 38 | | ImportFrom(identifier? module, alias* names, int? level) 39 | 40 | | Global(identifier* names) 41 | | Nonlocal(identifier* names) 42 | | Expr(expr value) 43 | | Pass | Break | Continue 44 | 45 | -- XXX Jython will be different 46 | -- col_offset is the byte offset in the utf8 string the parser uses 47 | attributes (int lineno, int col_offset) 48 | 49 | -- BoolOp() can use left & right? 50 | expr = BoolOp(boolop op, expr* values) 51 | | BinOp(expr left, operator op, expr right) 52 | | UnaryOp(unaryop op, expr operand) 53 | | Lambda(arguments args, expr body) 54 | | IfExp(expr test, expr body, expr orelse) 55 | | Dict(expr* keys, expr* values) 56 | | Set(expr* elts) 57 | | ListComp(expr elt, comprehension* generators) 58 | | SetComp(expr elt, comprehension* generators) 59 | | DictComp(expr key, expr value, comprehension* generators) 60 | | GeneratorExp(expr elt, comprehension* generators) 61 | -- the grammar constrains where yield expressions can occur 62 | | Yield(expr? value) 63 | | YieldFrom(expr value) 64 | -- need sequences for compare to distinguish between 65 | -- x < 4 < 3 and (x < 4) < 3 66 | | Compare(expr left, cmpop* ops, expr* comparators) 67 | | Call(expr func, expr* args, keyword* keywords, 68 | expr? starargs, expr? kwargs) 69 | | Num(object n) -- a number as a PyObject. 70 | | Str(string s) -- need to specify raw, unicode, etc? 71 | | Bytes(bytes s) 72 | | NameConstant(singleton value) 73 | | Ellipsis 74 | 75 | -- the following expression can appear in assignment context 76 | | Attribute(expr value, identifier attr, expr_context ctx) 77 | | Subscript(expr value, slice slice, expr_context ctx) 78 | | Starred(expr value, expr_context ctx) 79 | | Name(identifier id, expr_context ctx) 80 | | List(expr* elts, expr_context ctx) 81 | | Tuple(expr* elts, expr_context ctx) 82 | 83 | -- col_offset is the byte offset in the utf8 string the parser uses 84 | attributes (int lineno, int col_offset) 85 | 86 | expr_context = Load | Store | Del | AugLoad | AugStore | Param 87 | 88 | slice = Slice(expr? lower, expr? upper, expr? step) 89 | | ExtSlice(slice* dims) 90 | | Index(expr value) 91 | 92 | boolop = And | Or 93 | 94 | operator = Add | Sub | Mult | Div | Mod | Pow | LShift 95 | | RShift | BitOr | BitXor | BitAnd | FloorDiv 96 | 97 | unaryop = Invert | Not | UAdd | USub 98 | 99 | cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn 100 | 101 | comprehension = (expr target, expr iter, expr* ifs) 102 | 103 | excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) 104 | attributes (int lineno, int col_offset) 105 | 106 | arguments = (arg* args, arg? vararg, arg* kwonlyargs, expr* kw_defaults, 107 | arg? kwarg, expr* defaults) 108 | 109 | arg = (identifier arg, expr? annotation) 110 | attributes (int lineno, int col_offset) 111 | 112 | -- keyword arguments supplied to call 113 | keyword = (identifier arg, expr value) 114 | 115 | -- import name with optional 'as' alias. 116 | alias = (identifier name, identifier? asname) 117 | 118 | withitem = (expr context_expr, expr? optional_vars) 119 | } 120 | 121 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\mnfy.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\mnfy.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/mnfy.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/mnfy.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/mnfy" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/mnfy" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /Grammar: -------------------------------------------------------------------------------- 1 | # Grammar for Python 2 | 3 | # Note: Changing the grammar specified in this file will most likely 4 | # require corresponding changes in the parser module 5 | # (../Modules/parsermodule.c). If you can't make the changes to 6 | # that module yourself, please co-ordinate the required changes 7 | # with someone who can; ask around on python-dev for help. Fred 8 | # Drake will probably be listening there. 9 | 10 | # NOTE WELL: You should also follow all the steps listed in PEP 306, 11 | # "How to Change Python's Grammar" 12 | 13 | # Start symbols for the grammar: 14 | # single_input is a single interactive statement; 15 | # file_input is a module or sequence of commands read from an input file; 16 | # eval_input is the input for the eval() functions. 17 | # NB: compound_stmt in single_input is followed by extra NEWLINE! 18 | single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE 19 | file_input: (NEWLINE | stmt)* ENDMARKER 20 | eval_input: testlist NEWLINE* ENDMARKER 21 | 22 | decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE 23 | decorators: decorator+ 24 | decorated: decorators (classdef | funcdef) 25 | funcdef: 'def' NAME parameters ['->' test] ':' suite 26 | parameters: '(' [typedargslist] ')' 27 | typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [',' 28 | ['*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef]] 29 | | '*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef) 30 | tfpdef: NAME [':' test] 31 | varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' 32 | ['*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef]] 33 | | '*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef) 34 | vfpdef: NAME 35 | 36 | stmt: simple_stmt | compound_stmt 37 | simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE 38 | small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt | 39 | import_stmt | global_stmt | nonlocal_stmt | assert_stmt) 40 | expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) | 41 | ('=' (yield_expr|testlist_star_expr))*) 42 | testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [','] 43 | augassign: ('+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | 44 | '<<=' | '>>=' | '**=' | '//=') 45 | # For normal assignments, additional restrictions enforced by the interpreter 46 | del_stmt: 'del' exprlist 47 | pass_stmt: 'pass' 48 | flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt 49 | break_stmt: 'break' 50 | continue_stmt: 'continue' 51 | return_stmt: 'return' [testlist] 52 | yield_stmt: yield_expr 53 | raise_stmt: 'raise' [test ['from' test]] 54 | import_stmt: import_name | import_from 55 | import_name: 'import' dotted_as_names 56 | # note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS 57 | import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+) 58 | 'import' ('*' | '(' import_as_names ')' | import_as_names)) 59 | import_as_name: NAME ['as' NAME] 60 | dotted_as_name: dotted_name ['as' NAME] 61 | import_as_names: import_as_name (',' import_as_name)* [','] 62 | dotted_as_names: dotted_as_name (',' dotted_as_name)* 63 | dotted_name: NAME ('.' NAME)* 64 | global_stmt: 'global' NAME (',' NAME)* 65 | nonlocal_stmt: 'nonlocal' NAME (',' NAME)* 66 | assert_stmt: 'assert' test [',' test] 67 | 68 | compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated 69 | if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] 70 | while_stmt: 'while' test ':' suite ['else' ':' suite] 71 | for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] 72 | try_stmt: ('try' ':' suite 73 | ((except_clause ':' suite)+ 74 | ['else' ':' suite] 75 | ['finally' ':' suite] | 76 | 'finally' ':' suite)) 77 | with_stmt: 'with' with_item (',' with_item)* ':' suite 78 | with_item: test ['as' expr] 79 | # NB compile.c makes sure that the default except clause is last 80 | except_clause: 'except' [test ['as' NAME]] 81 | suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT 82 | 83 | test: or_test ['if' or_test 'else' test] | lambdef 84 | test_nocond: or_test | lambdef_nocond 85 | lambdef: 'lambda' [varargslist] ':' test 86 | lambdef_nocond: 'lambda' [varargslist] ':' test_nocond 87 | or_test: and_test ('or' and_test)* 88 | and_test: not_test ('and' not_test)* 89 | not_test: 'not' not_test | comparison 90 | comparison: expr (comp_op expr)* 91 | # <> isn't actually a valid comparison operator in Python. It's here for the 92 | # sake of a __future__ import described in PEP 401 93 | comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not' 94 | star_expr: '*' expr 95 | expr: xor_expr ('|' xor_expr)* 96 | xor_expr: and_expr ('^' and_expr)* 97 | and_expr: shift_expr ('&' shift_expr)* 98 | shift_expr: arith_expr (('<<'|'>>') arith_expr)* 99 | arith_expr: term (('+'|'-') term)* 100 | term: factor (('*'|'/'|'%'|'//') factor)* 101 | factor: ('+'|'-'|'~') factor | power 102 | power: atom trailer* ['**' factor] 103 | atom: ('(' [yield_expr|testlist_comp] ')' | 104 | '[' [testlist_comp] ']' | 105 | '{' [dictorsetmaker] '}' | 106 | NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') 107 | testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) 108 | trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME 109 | subscriptlist: subscript (',' subscript)* [','] 110 | subscript: test | [test] ':' [test] [sliceop] 111 | sliceop: ':' [test] 112 | exprlist: (expr|star_expr) (',' (expr|star_expr))* [','] 113 | testlist: test (',' test)* [','] 114 | dictorsetmaker: ( (test ':' test (comp_for | (',' test ':' test)* [','])) | 115 | (test (comp_for | (',' test)* [','])) ) 116 | 117 | classdef: 'class' NAME ['(' [arglist] ')'] ':' suite 118 | 119 | arglist: (argument ',')* (argument [','] 120 | |'*' test (',' argument)* [',' '**' test] 121 | |'**' test) 122 | # The reason that keywords are test nodes instead of NAME is that using NAME 123 | # results in an ambiguity. ast.c makes sure it's a NAME. 124 | argument: test [comp_for] | test '=' test # Really [keyword '='] test 125 | comp_iter: comp_for | comp_if 126 | comp_for: 'for' exprlist 'in' or_test [comp_iter] 127 | comp_if: 'if' test_nocond [comp_iter] 128 | 129 | # not used in grammar, but may appear in "node" passed from Parser to Compiler 130 | encoding_decl: NAME 131 | 132 | yield_expr: 'yield' [yield_arg] 133 | yield_arg: 'from' test | testlist 134 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ``mnfy`` — minify/obfuscate Python 3 source code 2 | ================================================= 3 | 4 | .. contents:: 5 | 6 | Web Pages 7 | --------- 8 | 9 | * `Documentation `__ 10 | * `Project site `__ (`issue tracker`_) 11 | * `PyPI/Cheeseshop `__ 12 | 13 | What the heck is mnfy for? 14 | -------------------------- 15 | 16 | The mnfy project was created for two reasons: 17 | 18 | * To show that shipping bytecode files without source, as a form of obfuscation, 19 | is not the best option available 20 | * Provide a minification of Python source code when total byte size of source 21 | code is paramount 22 | 23 | When people ship Python code as only bytecode files (i.e. only ``.pyo`` files 24 | and no ``.py`` files), there are couple drawbacks. First and foremost, it 25 | prevents users from using your code with all available Python interpreters such 26 | as Jython_ and IronPython_. Another drawback is that it is a poor form of 27 | obfuscation as projects like Meta_ allow you to take bytecode and 28 | reverse-engineer the original source code as enough details are kept that the 29 | only details missing are single-line comments. 30 | 31 | When the total number of bytes used to ship Python code is paramount, then 32 | you want to minify the source code. Bytecode files actually contain so much 33 | detail that the space savings can be miniscule (e.g. the ``decimal`` module from 34 | Python's standard libary, which is the largest single file in the stdlib, has a 35 | bytecode file that is only 5% smaller than its original source code). 36 | 37 | 38 | Usage 39 | ===== 40 | 41 | A note about version numbers and Python version compatibility 42 | ------------------------------------------------------------- 43 | 44 | The version number for mnfy is `PEP 386`_ compliant, taking the form of 45 | ``PPP.FFF.BBB``. The ``FFF.BBB`` represents the feature and bugfix version 46 | numbers of mnfy itself. The ``PPP`` portion of the version number represents the 47 | Python version that mnfy is compatible with: 48 | ``'{}{}'.format(*sys.version_info[:2])``. 49 | 50 | The Python version that mnfy is compatible with is directly embedded in the version 51 | number as Python's AST is not guaranteed to be backwards-compatible. This means 52 | that you should use each version of mnfy with a specific version of Python. 53 | Since mnfy works with source code and not bytecode you can safely use 54 | mnfy on code that must be backwards-compatible with older versions of Python, 55 | just make sure the interpreter you use with mnfy is correct and can parse the 56 | source code (e.g. just because 57 | the latest version of mnfy only works with Python 3.3 does not mean you cannot 58 | use that release against source code that must work with Python 3.2, just make 59 | sure to use a Python 3.3 interpreter with mnfy and that the source code can be 60 | read by a Python 3.3 interpreter). 61 | 62 | Command-line Usage 63 | ------------------ 64 | 65 | **TL;DR**: pass the file you want to minify as an argument to mnfy and it will 66 | print to stdout the source code minified such that the AST is **exactly** the 67 | same as the original source code. To get transformations that will change the 68 | AST to varying degrees you will need to specificy various flags. 69 | 70 | See the help message for the project for full instructions on usage:: 71 | 72 | python3 -m mnfy -h 73 | python3 mnfy.py -h 74 | 75 | .. END README 76 | 77 | If you happen to define the ``MNFY_RICHARD_JONES`` environment variable then not 78 | only will ``mnfy`` be installed, but so will ``nfy`` which just calls ``mnfy`` 79 | for you. This is so that you can use ``python -mnfy`` to invoke mnfy (i.e. 80 | minifying the "mnfy" name). The environment variable name is in honour of 81 | `Richard Jones`_ who first came up with the minifed name idea. 82 | 83 | .. _Richard Jones: http://mechanicalcat.net/richard 84 | 85 | 86 | Transformations 87 | =============== 88 | 89 | Source emission 90 | ---------------------------------------- 91 | 92 | .. _source emission: 93 | 94 | If you want no change to the AST compared to the original source code then you 95 | want mnfy's default behaviour of only emitting source code with not AST changes. 96 | Any tricks with source code formatting have been verified by passing Python's 97 | standard library through mnfy with only source emission used and comparing 98 | the result AST for no changes. 99 | 100 | As an example of what source emission does, this code (32 characters):: 101 | 102 | if True: 103 | x = 5 + 2 104 | y = 9 - 1 105 | 106 | becomes (19 characters):: 107 | 108 | if True:x=5+2;y=9-1 109 | 110 | 111 | Safe transformations 112 | ---------------------------------------------------- 113 | 114 | For a transformation to be considered safe it must semantically equivalent to 115 | running the code as ``python3 -OO`` but can lead to a change in the AST. As the 116 | changes are semantically safe there is only a single option to turn on these 117 | transformations. 118 | 119 | Combine imports 120 | +++++++++++++++ 121 | 122 | Take imports that are sequentially next to each other and put them on the same 123 | line **without** changing the import order. 124 | 125 | From:: 126 | 127 | import X # 8 characters 128 | import Y # 8 characters; 16 total 129 | 130 | to:: 131 | 132 | import X,Y # 10 characters 133 | 134 | From:: 135 | 136 | from X import y # 15 characters 137 | from X import z # 15 characters; 30 total 138 | 139 | to:: 140 | 141 | from X import y,z # 17 characters 142 | 143 | 144 | Combine ``with`` statements 145 | +++++++++++++++++++++++++++ 146 | 147 | As of Python 3.2, `contextlib.nested()`_ is syntactically supported. 148 | 149 | From:: 150 | 151 | with A: 152 | with B:pass 153 | 154 | to:: 155 | 156 | with A,B:pass 157 | 158 | 159 | Eliminate unused constants 160 | ++++++++++++++++++++++++++ 161 | 162 | If a constant isn't used then there is no need to keep it around. This primarily 163 | eliminates docstrings. If any block becomes completely empty then a ``pass`` 164 | statement is inserted. 165 | 166 | From:: 167 | 168 | def bacon(): 169 | """Docstring""" 170 | 171 | to:: 172 | 173 | def bacon():pass 174 | 175 | 176 | From:: 177 | 178 | if X:pass 179 | else:4+2 180 | 181 | to:: 182 | 183 | if X:pass 184 | 185 | 186 | Integer constants to power 187 | ++++++++++++++++++++++++++ 188 | 189 | For sufficiently large integer constants, it saves space to use the power 190 | operator (``**``). Only numbers of base 2 and 10 are used as that is what 191 | the `math module`_ supports. 192 | 193 | From:: 194 | 195 | 4294967296 196 | 197 | to:: 198 | 199 | 2**32 200 | 201 | 202 | Sane transformations 203 | ------------------------------------------------ 204 | 205 | For typical code, sane transformations should be fine (e.g. you are not 206 | introspecting local variables). Since these transformations are typically safe 207 | you can turn them all on with a single option, but they can also be switched on 208 | individually as desired. 209 | 210 | .. note:: 211 | Currently there are no sane transformations defined. See the 212 | `issue tracker`_ for some proposed transformations. 213 | 214 | Unsafe transformations 215 | ------------------------------------------ 216 | 217 | For the more adventurous who know what features of Python their code relies on, 218 | unsafe transformations can be used. Just be very aware of what your code depends 219 | on before using any specific transformation. For this reason each unsafe 220 | transformation must be switched on individually. 221 | 222 | 223 | Function to lambda 224 | ++++++++++++++++++ 225 | 226 | This is unsafe as lambda functions are not exactly like a function (e.g. 227 | lambda functions do not have a ``__name__`` attribute). 228 | 229 | From:: 230 | 231 | def identity(x):return x # 24 characters 232 | 233 | to:: 234 | 235 | identity=lambda x:x # 19 characters 236 | 237 | .. LINKS 238 | 239 | .. _Jython: http://www.jython.org 240 | .. _IronPython: http://ironpython.net/ 241 | .. _Meta: http://pypi.python.org/pypi/meta 242 | .. _PEP 386: http://python.org/dev/peps/pep-0386/ 243 | .. _contextlib.nested(): http://docs.python.org/2.7/library/contextlib.html#contextlib.nested 244 | .. _math module: http://docs.python.org/3/library/math.html 245 | .. _issue tracker: https://github.com/brettcannon/mnfy/issues?state=open 246 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # mnfy documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Nov 24 12:50:57 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'mnfy' 44 | copyright = u'2012, Brett Cannon' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '33.0.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '33.0.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'mnfydoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'mnfy.tex', u'mnfy Documentation', 187 | u'Brett Cannon', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'mnfy', u'mnfy Documentation', 217 | [u'Brett Cannon'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'mnfy', u'mnfy Documentation', 231 | u'Brett Cannon', 'mnfy', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /tests/test_safe_transforms.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | import mnfy 5 | 6 | 7 | class TransformTest(unittest.TestCase): 8 | 9 | """Base class for assisting in testing AST transformations.""" 10 | 11 | def check_transform(self, input_, expect): 12 | result = self.transform.visit(input_) 13 | if expect is None: 14 | if result is not None: 15 | self.fail('{} is not None'.format(ast.dump(result, False))) 16 | else: 17 | return 18 | expect_dump = ast.dump(expect, False) 19 | if result is None: 20 | self.fail('result is None, but expect is {}'.format(expect_dump)) 21 | elif not isinstance(result, ast.AST): 22 | self.fail('result should be an AST node, not {!r}'.format(result)) 23 | else: 24 | self.assertEqual(ast.dump(result, False), expect_dump) 25 | 26 | 27 | class CombineImportsTests(unittest.TestCase): 28 | 29 | def setUp(self): 30 | self.transform = mnfy.CombineImports() 31 | 32 | def test_one_Import(self): 33 | # Nothing should happen. 34 | imp = ast.Import(ast.alias('X', None)) 35 | module = ast.Module([imp]) 36 | new_ast = self.transform.visit(module) 37 | self.assertEqual(len(new_ast.body), 1) 38 | 39 | def test_combining_Import(self): 40 | # Should lead to a single Import with all of the aliases. 41 | to_import = ('X', None), ('Y', None) 42 | imports = [] 43 | for alias in to_import: 44 | imports.append(ast.Import([ast.alias(*alias)])) 45 | module = ast.Module(imports) 46 | new_ast = self.transform.visit(module) 47 | self.assertEqual(len(new_ast.body), 1) 48 | new_imp = new_ast.body[0] 49 | self.assertIsInstance(new_imp, ast.Import) 50 | self.assertEqual(len(new_imp.names), 2) 51 | for index, (name, alias) in enumerate(to_import): 52 | self.assertEqual(new_imp.names[index].name, name) 53 | self.assertEqual(new_imp.names[index].asname, alias) 54 | 55 | def test_interleaved_statements(self): 56 | # Do not combine if something between the Import statements. 57 | imp1 = ast.Import([ast.alias('X', None)]) 58 | imp2 = ast.Import([ast.alias('Y', None)]) 59 | from_import = ast.ImportFrom('Z', [ast.alias('W', None)], 0) 60 | module = ast.Module([imp1, from_import, imp2]) 61 | new_ast = self.transform.visit(module) 62 | self.assertEqual(len(new_ast.body), 3) 63 | for given, expect in zip(new_ast.body, 64 | (ast.Import, ast.ImportFrom, ast.Import)): 65 | self.assertIsInstance(given, expect) 66 | last_imp = new_ast.body[2] 67 | self.assertEqual(len(last_imp.names), 1) 68 | self.assertEqual(last_imp.names[0].name, 'Y') 69 | 70 | def test_one_ImportFrom(self): 71 | # Nothing changed. 72 | imp = ast.ImportFrom('X', [ast.alias('Y', None)], 0) 73 | module = ast.Module([imp]) 74 | new_ast = self.transform.visit(module) 75 | self.assertEqual(len(new_ast.body), 1) 76 | 77 | def test_combining_ImportFrom(self): 78 | # Combine ImportFrom when the 'from' clause matches. 79 | imp1 = ast.ImportFrom('X', [ast.alias('Y', None)], 1) 80 | imp2 = ast.ImportFrom('X', [ast.alias('Z', None)], 1) 81 | module = ast.Module([imp1, imp2]) 82 | new_ast = self.transform.visit(module) 83 | self.assertEqual(len(module.body), 1) 84 | imp = new_ast.body[0] 85 | self.assertEqual(len(imp.names), 2) 86 | for alias, (name, asname) in zip(imp.names, 87 | (('Y', None), ('Z', None))): 88 | self.assertEqual(alias.name, name) 89 | self.assertEqual(alias.asname, asname) 90 | 91 | def test_interleaved_ImportFrom(self): 92 | # Test prevention of statement merging. 93 | import_from1 = ast.ImportFrom('X', [ast.alias('Y', None)], 1) 94 | imp = ast.Import([ast.alias('X', None)]) 95 | # Separate by Import 96 | import_from2 = ast.ImportFrom('X', [ast.alias('Z', None)], 1) 97 | # Different level 98 | import_from3 = ast.ImportFrom('X', [ast.alias('W', None)], 2) 99 | # Different 'from' clause 100 | import_from4 = ast.ImportFrom('Z', [ast.alias('Y', None)], 2) 101 | module = ast.Module([import_from1, imp, import_from2, import_from3, 102 | import_from4]) 103 | new_ast = self.transform.visit(module) 104 | self.assertEqual(len(module.body), 5) 105 | 106 | 107 | class CombineWithStatementsTests(unittest.TestCase): 108 | 109 | """with A: 110 | with B: 111 | pass 112 | 113 | with A,B:pass 114 | """ 115 | 116 | A = ast.Name('A', ast.Load()) 117 | A_clause = ast.withitem(A, None) 118 | B = ast.Name('B', ast.Load()) 119 | B_clause = ast.withitem(B, None) 120 | C = ast.Name('C', ast.Load()) 121 | C_clause = ast.withitem(C, None) 122 | 123 | def setUp(self): 124 | self.transform = mnfy.CombineWithStatements() 125 | 126 | def test_deeply_nested(self): 127 | with_C = ast.With([self.C_clause], [ast.Pass()]) 128 | with_B = ast.With([self.B_clause], [with_C]) 129 | with_A = ast.With([self.A_clause], [with_B]) 130 | new_ast = self.transform.visit(with_A) 131 | expect = ast.With([self.A_clause, self.B_clause, self.C_clause], 132 | [ast.Pass()]) 133 | self.assertEqual(ast.dump(new_ast), ast.dump(expect)) 134 | 135 | def test_no_optimization(self): 136 | with_B = ast.With([self.B_clause], [ast.Pass()]) 137 | with_A = ast.With([self.A_clause], [with_B, ast.Pass()]) 138 | new_ast = self.transform.visit(with_A) 139 | self.assertEqual(new_ast, with_A) 140 | 141 | 142 | class UnusedConstantEliminationTests(TransformTest): 143 | 144 | def setUp(self): 145 | self.transform = mnfy.EliminateUnusedConstants() 146 | 147 | def test_do_no_over_step_bounds(self): 148 | assign = ast.Assign([ast.Name('a', ast.Store())], ast.Num(42)) 149 | self.check_transform(assign, assign) 150 | 151 | def test_unused_constants(self): 152 | number = ast.Num(1) 153 | string = ast.Str('A') 154 | bytes_ = ast.Bytes(b'A') 155 | module = ast.Module([ast.Expr(expr) 156 | for expr in (number, string, bytes_)]) 157 | new_ast = self.transform.visit(module) 158 | self.assertFalse(new_ast.body) 159 | 160 | def _test_empty_body(self, node): 161 | node.body.append(ast.Expr(ast.Str('dead code'))) 162 | module = ast.Module([node]) 163 | new_ast = self.transform.visit(module) 164 | self.assertEqual(len(new_ast.body), 1) 165 | block = new_ast.body[0].body 166 | self.assertEqual(len(block), 1) 167 | self.assertIsInstance(block[0], ast.Pass) 168 | return new_ast 169 | 170 | def test_empty_FunctionDef(self): 171 | function = ast.FunctionDef('X', ast.arguments(), [], [], None) 172 | self._test_empty_body(function) 173 | 174 | def test_empty_ClassDef(self): 175 | cls = ast.ClassDef('X', [], [], None, None, [], None) 176 | self._test_empty_body(cls) 177 | 178 | def test_empty_For(self): 179 | for_ = ast.For(ast.Name('X', ast.Store()), ast.Str('a'), [], []) 180 | self._test_empty_body(for_) 181 | # An empty 'else' clause should just go away 182 | for_else = ast.For(ast.Name('X', ast.Store()), ast.Str('a'), 183 | [ast.Pass()], [ast.Pass()]) 184 | expect = ast.For(ast.Name('X', ast.Store()), ast.Str('a'), 185 | [ast.Pass()], []) 186 | self.check_transform(for_else, expect) 187 | 188 | def test_empty_While(self): 189 | while_ = ast.While(ast.Num(42), [], []) 190 | self._test_empty_body(while_) 191 | # An empty 'else' clause should be eliminated. 192 | while_else = ast.While(ast.Num(42), [ast.Pass()], 193 | [ast.Pass()]) 194 | expect = ast.While(ast.Num(42), [ast.Pass()], []) 195 | self.check_transform(while_else, expect) 196 | 197 | def test_empty_If(self): 198 | if_ = ast.If(ast.Num(2), [], []) 199 | self._test_empty_body(if_) 200 | # An empty 'else' clause should be eliminated. 201 | if_else = ast.If(ast.Num(42), [ast.Pass()], [ast.Pass()]) 202 | expect = ast.If(ast.Num(42), [ast.Pass()], []) 203 | self.check_transform(if_else, expect) 204 | 205 | def test_empty_With(self): 206 | with_ = ast.With([ast.Name('X', ast.Load())], []) 207 | self._test_empty_body(with_) 208 | 209 | def test_Try(self): 210 | # try/except where 'except' is only dead code. 211 | exc_clause = ast.ExceptHandler(None, None, 212 | [ast.Expr(ast.Str('dead code'))]) 213 | try_exc = ast.Try([ast.Pass()], [exc_clause], [], []) 214 | expect = ast.Try([ast.Pass()], 215 | [ast.ExceptHandler(None, None, [ast.Pass()])], [], []) 216 | self.check_transform(try_exc, expect) 217 | 218 | 219 | class IntegerToPowerTests(TransformTest): 220 | 221 | """100000 -> 10**5""" 222 | 223 | def setUp(self): 224 | self.transform = mnfy.IntegerToPower() 225 | 226 | def verify(self, base, exponent): 227 | num = base**exponent 228 | node = ast.Num(num) 229 | expect = ast.BinOp(ast.Num(base), ast.Pow(), ast.Num(exponent)) 230 | self.check_transform(node, expect) 231 | 232 | def test_conversion(self): 233 | for num in (5, 12): 234 | self.verify(10, num) 235 | for num in (17, 20, 40): 236 | self.verify(2, num) 237 | 238 | def test_left_alone(self): 239 | for num in (10**5+1, float(10**5)): 240 | self.check_transform(ast.Num(num), ast.Num(num)) 241 | 242 | 243 | class DropPassTests(TransformTest): 244 | 245 | func = ast.Expr(ast.Call(ast.Name('func', ast.Load()), [], [], None, None)) 246 | 247 | @staticmethod 248 | def simple_stmt(): 249 | return [ast.Pass()] 250 | 251 | @classmethod 252 | def fancy_stmts(cls): 253 | return [ast.Pass(), cls.func, ast.Pass()] 254 | 255 | def setUp(self): 256 | self.transform = mnfy.DropPass() 257 | 258 | def _test_mod(self, node_type): 259 | basic = node_type(self.simple_stmt()) 260 | self.check_transform(basic, basic) 261 | fancy = node_type(self.fancy_stmts()) 262 | self.check_transform(fancy, node_type([self.func])) 263 | 264 | def test_Module(self): 265 | self._test_mod(ast.Module) 266 | 267 | def test_Interactive(self): 268 | self._test_mod(ast.Interactive) 269 | 270 | def test_Suite(self): 271 | self._test_mod(ast.Suite) 272 | 273 | def test_FunctionDef(self): 274 | basic = ast.FunctionDef(None, None, self.simple_stmt(), [], None) 275 | self.check_transform(basic, basic) 276 | fancy = ast.FunctionDef(None, None, self.fancy_stmts(), [], None) 277 | want = ast.FunctionDef(None, None, [self.func], [], None) 278 | self.check_transform(fancy, want) 279 | 280 | def test_ClassDef(self): 281 | basic = ast.ClassDef(None, [], [], None, None, self.simple_stmt(), []) 282 | self.check_transform(basic, basic) 283 | fancy = ast.ClassDef(None, [], [], None, None, self.fancy_stmts(), []) 284 | want = ast.ClassDef(None, [], [], None, None, [self.func], []) 285 | self.check_transform(fancy, want) 286 | 287 | def test_For(self): 288 | got = ast.For(None, None, self.simple_stmt(), self.simple_stmt()) 289 | want = ast.For(None, None, self.simple_stmt(), []) 290 | self.check_transform(got, want) 291 | got = ast.For(None, None, self.fancy_stmts(), self.fancy_stmts()) 292 | want = ast.For(None, None, [self.func], [self.func]) 293 | self.check_transform(got, want) 294 | 295 | def test_While(self): 296 | got = ast.While(None, self.simple_stmt(), self.simple_stmt()) 297 | want = ast.While(None, self.simple_stmt(), []) 298 | self.check_transform(got, want) 299 | got = ast.While(None, self.fancy_stmts(), self.fancy_stmts()) 300 | want = ast.While(None, [self.func], [self.func]) 301 | self.check_transform(got, want) 302 | 303 | def test_If(self): 304 | got = ast.If(None, self.simple_stmt(), self.simple_stmt()) 305 | want = ast.If(None, self.simple_stmt(), []) 306 | self.check_transform(got, want) 307 | got = ast.If(None, self.fancy_stmts(), self.fancy_stmts()) 308 | want = ast.If(None, [self.func], [self.func]) 309 | self.check_transform(got, want) 310 | 311 | def test_With(self): 312 | got = ast.With([], self.simple_stmt()) 313 | self.check_transform(got, got) 314 | got = ast.With([], self.fancy_stmts()) 315 | want = ast.With([], [self.func]) 316 | self.check_transform(got, want) 317 | 318 | def test_Try(self): 319 | except_ = ast.ExceptHandler(None, None, self.simple_stmt()) 320 | # Always keep try/except. 321 | got = ast.Try([self.func], [except_], [], []) 322 | want = got 323 | self.check_transform(got, want) 324 | # try/except/finally where 'finally' is simple -> try/except 325 | got = ast.Try([self.func], [except_], [], self.simple_stmt()) 326 | self.check_transform(got, want) 327 | # try/except/else where 'else' is simple -> try/except 328 | got = ast.Try([self.func], [except_], self.simple_stmt(), []) 329 | self.check_transform(got, want) 330 | # try/finally where 'finally' is simple -> unwrap 'try' 331 | got = ast.Module([ast.Try([self.func], [], [], self.simple_stmt())]) 332 | want = ast.Module([self.func]) 333 | self.check_transform(got, want) 334 | 335 | def test_ExceptHandler(self): 336 | got = ast.ExceptHandler(None, None, self.simple_stmt()) 337 | self.check_transform(got, got) 338 | got = ast.ExceptHandler(None, None, self.fancy_stmts()) 339 | want = ast.ExceptHandler(None, None, [self.func]) 340 | self.check_transform(got, want) 341 | 342 | 343 | if __name__ == '__main__': 344 | unittest.main() 345 | -------------------------------------------------------------------------------- /tests/test_source_emission.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import ast 3 | import inspect 4 | import math 5 | import sys 6 | import unittest 7 | 8 | import mnfy 9 | 10 | 11 | class SourceCodeEmissionTests(unittest.TestCase): 12 | 13 | """Test the emission of source code from AST nodes.""" 14 | 15 | operators = {ast.Add: '+', ast.Sub: '-', ast.Mult: '*', ast.Div: '/', 16 | ast.Mod: '%', ast.Pow: '**', ast.LShift: '<<', 17 | ast.RShift: '>>', ast.BitOr: '|', ast.BitXor: '^', 18 | ast.BitAnd: '&', ast.FloorDiv: '//'} 19 | 20 | 21 | def verify(self, given_ast, expect): 22 | visitor = mnfy.SourceCode() 23 | visitor.visit(given_ast) 24 | self.assertEqual(str(visitor), expect) 25 | 26 | def test_pop(self): 27 | visitor = mnfy.SourceCode() 28 | visitor._write('X') 29 | with self.assertRaises(ValueError): 30 | visitor._pop('sadf') 31 | visitor._write('X') 32 | self.assertEqual(visitor._pop(), 'X') 33 | 34 | def test_Ellipsis(self): 35 | self.verify(ast.Ellipsis(), '...') 36 | 37 | def verify_num(self, given, expect): 38 | assert given == eval(expect) 39 | self.verify(ast.Num(given), expect) 40 | 41 | def test_Num_integers(self): 42 | for num in (42, -13, 1.79769313486231e+308, 1.3, 10**12+0.5): 43 | self.verify_num(num, repr(num)) 44 | for num in (10**12+1, -10**12-1): 45 | self.verify_num(num, hex(num)) 46 | 47 | def test_Num_floats(self): 48 | self.verify_num(1.0, '1.') 49 | self.verify_num(10.0, '10.') 50 | self.verify_num(100.0, '1e2') 51 | self.verify_num(1230.0, '1230.') 52 | self.verify_num(123456.0, '123456.') 53 | self.verify_num(1234560.0, '1234560.') 54 | self.verify_num(12345600.0, '12345600.') 55 | self.verify_num(123456000.0, '1.23456e8') 56 | self.verify_num(210000.0, '2.1e5') 57 | self.verify_num(0.01, '.01') 58 | self.verify_num(0.001, '.001') 59 | self.verify_num(0.0001, '1e-4') 60 | self.verify_num(0.00015, '.00015') 61 | self.verify_num(0.000015, '1.5e-5') # Repr is 1.5e-05 62 | 63 | def test_Num_complex(self): 64 | self.verify_num(3j, '3j') 65 | self.verify_num(complex(3, 4), '(3+4j)') 66 | 67 | def test_Str(self): 68 | for text in ('string', '\n', r'\n'): 69 | self.verify(ast.Str(text), repr(text)) 70 | 71 | def test_Bytes(self): 72 | for thing in (b'1', b'123'): 73 | self.verify(ast.Bytes(thing), repr(thing)) 74 | 75 | def test_NameConstant(self): 76 | self.verify(ast.NameConstant(None), 'None') 77 | self.verify(ast.NameConstant(True), 'True') 78 | self.verify(ast.NameConstant(False), 'False') 79 | 80 | def test_Dict(self): 81 | self.verify(ast.Dict([], []), '{}') 82 | self.verify(ast.Dict([ast.Num(42)], [ast.Num(42)]), '{42:42}') 83 | self.verify(ast.Dict([ast.Num(2), ast.Num(3)], [ast.Num(4), 84 | ast.Num(6)]), '{2:4,3:6}') 85 | 86 | def test_Set(self): 87 | self.verify(ast.Set([ast.Num(42)]), '{42}') 88 | self.verify(ast.Set([ast.Num(2), ast.Num(3)]), '{2,3}') 89 | 90 | def test_Tuple(self): 91 | self.verify(ast.Tuple([], ast.Load()), '()') 92 | self.verify(ast.Tuple([ast.Num(42)], ast.Load()), '(42,)') 93 | self.verify(ast.Tuple([ast.Num(42), ast.Num(3)], ast.Load()), '(42,3)') 94 | 95 | def test_List(self): 96 | self.verify(ast.List([], ast.Load()), '[]') 97 | self.verify(ast.List([ast.Num(42), ast.Num(3)], ast.Load()), '[42,3]') 98 | 99 | def test_comprehension(self): 100 | comp = ast.comprehension(ast.Name('x', ast.Store()), ast.Name('y', 101 | ast.Load()), []) 102 | self.verify(comp, 'for x in y') 103 | comp.ifs = [ast.Num(2), ast.Num(3)] 104 | self.verify(comp, 'for x in y if 2 if 3') 105 | 106 | def seq_comp_test(self, node_type, ends): 107 | gen = ast.comprehension(ast.Name('x', ast.Store()), ast.Name('y', 108 | ast.Load()), [ast.Num(2)]) 109 | listcomp = node_type(ast.Name('w', ast.Load()), [gen]) 110 | self.verify(listcomp, '{}w for x in y if 2{}'.format(*ends)) 111 | gen2 = ast.comprehension(ast.Name('a', ast.Store()), ast.Name('b', 112 | ast.Load()), []) 113 | listcomp.generators.append(gen2) 114 | self.verify(listcomp, '{}w for x in y if 2 for a in b{}'.format(*ends)) 115 | return listcomp 116 | 117 | def test_ListComp(self): 118 | self.seq_comp_test(ast.ListComp, '[]') 119 | 120 | def test_SetComp(self): 121 | self.seq_comp_test(ast.SetComp, '{}') 122 | 123 | def test_GeneratorExp(self): 124 | self.seq_comp_test(ast.GeneratorExp, '()') 125 | 126 | def test_DictComp(self): 127 | gen = ast.comprehension(ast.Name('x', ast.Store()), ast.Name('y', 128 | ast.Load()), [ast.Num(2)]) 129 | dictcomp = ast.DictComp(ast.Name('v', ast.Load()), ast.Name('w', 130 | ast.Load()), [gen]) 131 | self.verify(dictcomp, '{v:w for x in y if 2}') 132 | gen2 = ast.comprehension(ast.Name('a', ast.Store()), ast.Name('b', 133 | ast.Load()), []) 134 | dictcomp.generators.append(gen2) 135 | self.verify(dictcomp, '{v:w for x in y if 2 for a in b}') 136 | 137 | def test_Name(self): 138 | self.verify(ast.Name('X', ast.Load()), 'X') 139 | 140 | def test_Attribute(self): 141 | self.verify(ast.Attribute(ast.Name('spam', ast.Load()), 'eggs', 142 | ast.Load()), 143 | 'spam.eggs') 144 | self.verify(ast.Attribute(ast.Attribute(ast.Name('spam', ast.Load()), 145 | 'eggs', ast.Load()), 'bacon', ast.Load()), 146 | 'spam.eggs.bacon') 147 | self.verify(ast.Attribute(ast.BinOp(ast.Name('X', ast.Load()), 148 | ast.Add(), ast.Num(2)), 'Y', ast.Load()), 149 | '(X+2).Y') 150 | 151 | def test_Assign(self): 152 | self.verify(ast.Assign([ast.Name('X', ast.Store())], ast.Num(42)), 153 | 'X=42') 154 | multi_assign = ast.Assign([ast.Name('X', ast.Store()), ast.Name('Y', 155 | ast.Store())], ast.Num(42)) 156 | self.verify(multi_assign, 'X=Y=42') 157 | self.verify(ast.Assign([ast.Tuple([ast.Name('X', ast.Store()), 158 | ast.Name('Y', ast.Store())], ast.Store())], 159 | ast.Name('Z', ast.Load())), 160 | 'X,Y=Z') 161 | self.verify(ast.Assign([ast.Name('X', ast.Store())], 162 | ast.Tuple([ast.Num(1), ast.Num(2)], ast.Load())), 'X=1,2') 163 | self.verify(ast.Assign([ast.Name('X', ast.Store())], 164 | ast.Tuple([], ast.Load())), 165 | 'X=()') 166 | 167 | def test_Delete(self): 168 | self.verify(ast.Delete([ast.Name('X', ast.Del()), ast.Name('Y', 169 | ast.Del())]), 170 | 'del X,Y') 171 | 172 | def test_Call(self): 173 | name = ast.Name('spam', ast.Load()) 174 | args = ([ast.Num(42)], '42'), ([], None) 175 | keywords = ([ast.keyword('X', ast.Num(42))], 'X=42'), ([], None) 176 | starargs = (ast.Name('args', ast.Load()), '*args'), (None, None) 177 | kwargs = (ast.Name('kwargs', ast.Load()), '**kwargs'), (None, None) 178 | for arg in args: 179 | for keyword in keywords: 180 | for stararg in starargs: 181 | for kwarg in kwargs: 182 | node = ast.Call(name, arg[0], keyword[0], stararg[0], 183 | kwarg[0]) 184 | expect = 'spam({})'.format(','.join(x for x in 185 | (arg[1], keyword[1], stararg[1], kwarg[1]) 186 | if x)) 187 | self.verify(node, expect) 188 | self.verify(ast.Call(name, [ast.Num(2), ast.Num(3)], [], None, None), 189 | 'spam(2,3)') 190 | self.verify(ast.Call(name, [], 191 | [ast.keyword('X', ast.Num(0)), 192 | ast.keyword('Y', ast.Num(1))], 193 | None, None), 194 | 'spam(X=0,Y=1)') 195 | # A single genexp doesn't need parentheses. 196 | genexp = self.seq_comp_test(ast.GeneratorExp, '()') 197 | self.verify(ast.Call(name, [genexp], [], None, None), 198 | 'spam(w for x in y if 2 for a in b)') 199 | self.verify(ast.Call(name, [genexp, genexp], [], None, None), 200 | 'spam((w for x in y if 2 for a in b),' 201 | '(w for x in y if 2 for a in b))') 202 | 203 | def test_Starred(self): 204 | self.verify(ast.Starred(ast.Name('X', ast.Store()), ast.Store()), 205 | '*X') 206 | 207 | def test_UnaryOp(self): 208 | self.verify(ast.UnaryOp(ast.Invert(), ast.Num(42)), '~42') 209 | self.verify(ast.UnaryOp(ast.Not(), ast.Num(42)), 'not 42') 210 | self.verify(ast.UnaryOp(ast.UAdd(), ast.Num(42)), '+42') 211 | self.verify(ast.UnaryOp(ast.USub(), ast.Num(42)), '-42') 212 | precedence = ast.UnaryOp(ast.Not(), ast.BoolOp(ast.Or(), [ast.Num(n=1), 213 | ast.Num(2)])) 214 | self.verify(precedence, 'not (1 or 2)') 215 | 216 | def test_BinOp(self): 217 | for node, op in self.operators.items(): 218 | self.verify(ast.BinOp(ast.Num(2), node(), ast.Num(3)), 219 | '2{}3'.format(op)) 220 | # 1 + 2 * 3 = BinOp(2 + BinOp(2 * 3)) 221 | mult = ast.BinOp(ast.Num(2), ast.Mult(), ast.Num(3)) 222 | expr = ast.BinOp(ast.Num(1), ast.Add(), mult) 223 | self.verify(expr, '1+2*3') 224 | # (1 + 2) * 3 = BinOp(BinOp(1 + 2) * 3) 225 | add = ast.BinOp(ast.Num(1), ast.Add(), ast.Num(2)) 226 | expr = ast.BinOp(add, ast.Mult(), ast.Num(3)) 227 | self.verify(expr, '(1+2)*3') 228 | # 2 * 3 + 1 = BinOp(BinOp(2 * 3) + 1) 229 | expr = ast.BinOp(mult, ast.Add(), ast.Num(1)) 230 | self.verify(expr, '2*3+1') 231 | # 3 * (1 + 2) = BinOp(3 * BinOp(1 + 2)) 232 | expr = ast.BinOp(ast.Num(3), ast.Mult(), add) 233 | self.verify(expr, '3*(1+2)') 234 | # 3 - (1 + 2) = BinOp(3 - (BinOp1 + 2)) 235 | expr = ast.BinOp(ast.Num(3), ast.Sub(), add) 236 | self.verify(expr, '3-(1+2)') 237 | # Deal with Pow's "special" precedence compared to unary operators. 238 | self.verify(ast.BinOp(ast.Num(-1), ast.Pow(), ast.Num(2)), '(-1)**2') 239 | self.verify(ast.UnaryOp(ast.USub(), ast.BinOp(ast.Num(1), ast.Pow(), 240 | ast.Num(2))), '-1**2') 241 | self.verify(ast.BinOp(ast.Num(1), ast.Pow(), ast.UnaryOp(ast.USub(), 242 | ast.Num(2))), '1**(-2)') 243 | 244 | def test_Compare(self): 245 | comparisons = {ast.Eq: '==', ast.NotEq: '!=', ast.Lt: '<', 246 | ast.LtE: '<=', ast.Gt: '>', ast.GtE: '>=', 247 | ast.Is: ' is ', ast.IsNot: ' is not ', ast.In: ' in ', 248 | ast.NotIn: ' not in '} 249 | for ast_cls, syntax in comparisons.items(): 250 | self.verify(ast.Compare(ast.Num(3), [ast_cls()], [ast.Num(2)]), 251 | '3{}2'.format(syntax)) 252 | # 2 < 3 < 4 253 | three_way = ast.Compare(ast.Num(2), [ast.Lt(), ast.Lt()], [ast.Num(3), 254 | ast.Num(4)]) 255 | self.verify(three_way, '2<3<4') 256 | # (2 < 3) < 4 257 | simple = ast.Compare(ast.Num(2), [ast.Lt()], [ast.Num(3)]) 258 | left_heavy = ast.Compare(simple, [ast.Lt()], [ast.Num(4)]) 259 | self.verify(left_heavy, '(2<3)<4') 260 | # 2 < (3 < 4) 261 | right_heavy = ast.Compare(ast.Num(2), [ast.Lt()], [simple]) 262 | self.verify(right_heavy, '2<(2<3)') 263 | 264 | def test_Slice(self): 265 | self.verify(ast.Slice(None, None, None), ':') 266 | self.verify(ast.Slice(ast.Num(42), None, None), '42:') 267 | self.verify(ast.Slice(None, ast.Num(42), None), ':42') 268 | self.verify(ast.Slice(None, None, ast.Num(42)), '::42') 269 | self.verify(ast.Slice(ast.Num(1), ast.Num(2), None), '1:2') 270 | self.verify(ast.Slice(ast.Num(1), None, ast.Num(2)), '1::2') 271 | 272 | def test_ExtSlice(self): 273 | slice1 = ast.Index(ast.Num(42)) 274 | slice2 = ast.Slice(None, None, ast.Num(6)) 275 | self.verify(ast.ExtSlice([slice1, slice2]), '42,::6') 276 | 277 | def test_Subscript(self): 278 | sub = ast.Subscript(ast.Name('X', ast.Load()), [], ast.Load()) 279 | # Index 280 | slice1 = ast.Index(ast.Num(42)) 281 | sub.slice = slice1 282 | self.verify(sub, 'X[42]') 283 | # Slice 284 | slice2 = ast.Slice(None, None, ast.Num(2)) 285 | sub.slice = slice2 286 | self.verify(sub, 'X[::2]') 287 | # ExtSlice 288 | sub.slice = ast.ExtSlice([slice1, slice2]) 289 | self.verify(sub, 'X[42,::2]') 290 | # Issue #20 291 | expect = ast.Expr( 292 | value=ast.Subscript( 293 | value=ast.BinOp( 294 | left=ast.Name(id='p', ctx=ast.Load()), 295 | op=ast.Add(), 296 | right=ast.List(elts=[ast.Num(n=1), ast.Num(n=2), 297 | ast.Num(n=3), ast.Num(n=4)], 298 | ctx=ast.Load())), 299 | slice=ast.Slice(lower=None, upper=ast.Num(n=4), step=None), 300 | ctx=ast.Load())) 301 | self.verify(expect, '(p+[1,2,3,4])[:4]') 302 | 303 | @staticmethod 304 | def create_arg(arg, annotation=None): 305 | return ast.arg(arg, annotation) 306 | 307 | @staticmethod 308 | def create_arguments(args=[], vararg=None, kwonlyargs=[], kw_defaults=[], 309 | kwarg=None, defaults=[]): 310 | return ast.arguments( 311 | args, vararg, kwonlyargs, kw_defaults, kwarg, defaults) 312 | 313 | def test_arg(self): 314 | self.verify(self.create_arg('spam'), 'spam') 315 | self.verify(self.create_arg('spam', ast.Num(42)), 'spam:42') 316 | 317 | def test_arguments(self): 318 | args = [self.create_arg('spam')] 319 | self.verify(self.create_arguments(args),'spam') 320 | args = [self.create_arg('spam'), self.create_arg('eggs')] 321 | self.verify(self.create_arguments(args=args), 'spam,eggs') 322 | self.verify(self.create_arguments(args=[self.create_arg('spam')], 323 | defaults=[ast.Num(42)]), 324 | 'spam=42') 325 | self.verify(self.create_arguments(args=args, 326 | defaults=[ast.Num(3), ast.Num(2)]), 327 | 'spam=3,eggs=2') 328 | self.verify(self.create_arguments(args=args, defaults=[ast.Num(42)]), 329 | 'spam,eggs=42') 330 | self.verify(self.create_arguments(vararg=self.create_arg('spam')), 331 | '*spam') 332 | self.verify(self.create_arguments( 333 | vararg=self.create_arg('spam', ast.Num(42))), 334 | '*spam:42') 335 | self.verify(self.create_arguments(kwonlyargs=[self.create_arg('spam')]), 336 | '*,spam') 337 | self.verify(self.create_arguments(kwonlyargs=[self.create_arg('spam')], 338 | kw_defaults=[ast.Num(42)]), 339 | '*,spam=42') 340 | self.verify( 341 | self.create_arguments(args=[self.create_arg('spam')], 342 | kwonlyargs=[self.create_arg('eggs')]), 343 | 'spam,*,eggs') 344 | self.verify( 345 | self.create_arguments(vararg=self.create_arg('spam'), 346 | kwonlyargs=[self.create_arg('eggs')]), 347 | '*spam,eggs') 348 | self.verify(self.create_arguments(kwarg=self.create_arg('spam')), 349 | '**spam') 350 | self.verify( 351 | self.create_arguments(args=[self.create_arg('spam')], 352 | vararg=self.create_arg('eggs')), 353 | 'spam,*eggs') 354 | self.verify( 355 | self.create_arguments(args=[self.create_arg('spam')], 356 | vararg=self.create_arg('eggs'), 357 | kwonlyargs=[self.create_arg('bacon')]), 358 | 'spam,*eggs,bacon') 359 | self.verify( 360 | self.create_arguments(args=[self.create_arg('spam')], 361 | kwarg=self.create_arg('eggs')), 362 | 'spam,**eggs') 363 | args = [self.create_arg('spam'), self.create_arg('eggs'), 364 | self.create_arg('bacon')] 365 | self.verify( 366 | self.create_arguments(kwonlyargs=args, 367 | kw_defaults=[None, ast.Num(42), None]), 368 | '*,spam,eggs=42,bacon') 369 | 370 | def test_Lambda(self): 371 | self.verify(ast.Lambda(self.create_arguments(), ast.Num(42)), 372 | 'lambda:42') 373 | args = self.create_arguments([self.create_arg('spam')]) 374 | self.verify(ast.Lambda(args, ast.Num(42)), 375 | 'lambda spam:42') 376 | 377 | def test_Pass(self): 378 | self.verify(ast.Pass(), 'pass') 379 | 380 | def test_Break(self): 381 | self.verify(ast.Break(), 'break') 382 | 383 | def test_Continue(self): 384 | self.verify(ast.Continue(), 'continue') 385 | 386 | def test_Raise(self): 387 | self.verify(ast.Raise(None, None), 'raise') 388 | self.verify(ast.Raise(ast.Name('X', ast.Load()), None), 'raise X') 389 | raise_ast = ast.Raise(ast.Name('X', ast.Load()), 390 | ast.Name('Y', ast.Load())) 391 | self.verify(raise_ast, 'raise X from Y') 392 | 393 | def test_Return(self): 394 | self.verify(ast.Return(None), 'return') 395 | self.verify(ast.Return(ast.Num(42)), 'return 42') 396 | self.verify(ast.Return(ast.Tuple([ast.Num(1), ast.Num(2)], ast.Load())), 397 | 'return 1,2') 398 | 399 | def test_Yield(self): 400 | self.verify(ast.Yield(None), 'yield') 401 | self.verify(ast.Yield(ast.Num(42)), 'yield 42') 402 | self.verify(ast.Yield(ast.Tuple([ast.Num(1), ast.Num(2)], ast.Load())), 403 | 'yield 1,2') 404 | 405 | def test_YieldFrom(self): 406 | self.verify(ast.YieldFrom(ast.Num(42)), 'yield from 42') 407 | 408 | def test_Import(self): 409 | self.verify(ast.Import([ast.alias('spam', None)]), 'import spam') 410 | self.verify(ast.Import([ast.alias('spam', 'bacon')]), 411 | 'import spam as bacon') 412 | self.verify(ast.Import([ast.alias('spam', None), 413 | ast.alias('bacon', 'bacn'), 414 | ast.alias('eggs', None)]), 415 | 'import spam,bacon as bacn,eggs') 416 | 417 | def test_ImportFrom(self): 418 | # from X import Y 419 | from_X = ast.ImportFrom('X', [ast.alias('Y', None)], 0) 420 | self.verify(from_X, 'from X import Y') 421 | # from . import Y 422 | from_dot = ast.ImportFrom(None, [ast.alias('Y', None)], 1) 423 | self.verify(from_dot, 'from . import Y') 424 | # from .X import Y 425 | from_dot_X = ast.ImportFrom('X', [ast.alias('Y', None)], 1) 426 | self.verify(from_dot_X, 'from .X import Y') 427 | # from X import Y, Z 428 | from_X_multi = ast.ImportFrom('X', [ast.alias('Y', None), 429 | ast.alias('Z', None)], 0) 430 | self.verify(from_X_multi, 'from X import Y,Z') 431 | 432 | def test_BoolOp(self): 433 | and_op = ast.BoolOp(ast.And(), [ast.Num(2), ast.Num(3)]) 434 | self.verify(and_op, '2 and 3') 435 | or_op = ast.BoolOp(ast.Or(), [ast.Num(2), ast.Num(3)]) 436 | self.verify(or_op, '2 or 3') 437 | many_args = ast.BoolOp(ast.And(), [ast.Num(1), ast.Num(2), ast.Num(3)]) 438 | self.verify(many_args, '1 and 2 and 3') 439 | no_precedence = ast.BoolOp(ast.Or(), [ast.BoolOp(ast.And(), 440 | [ast.Num(2), ast.Num(3)]), ast.Num(1)]) 441 | self.verify(no_precedence, '2 and 3 or 1') 442 | no_precedence2 = ast.BoolOp(ast.Or(), [ast.Num(2), ast.BoolOp(ast.And(), 443 | [ast.Num(3), ast.Num(1)])]) 444 | self.verify(no_precedence2, '2 or 3 and 1') 445 | precedence = ast.BoolOp(ast.And(), [ast.Num(1), ast.BoolOp(ast.Or(), 446 | [ast.Num(2), ast.Num(3)])]) 447 | self.verify(precedence, '1 and (2 or 3)') 448 | 449 | def test_IfExp(self): 450 | if_expr = ast.IfExp(ast.Num(1), ast.Num(2), ast.Num(3)) 451 | self.verify(if_expr, '2 if 1 else 3') 452 | 453 | def test_If(self): 454 | # 'if' only 455 | if_ = ast.If(ast.Num(42), [ast.Pass()], []) 456 | self.verify(if_, 'if 42:pass') 457 | # if/else 458 | if_else = ast.If(ast.Num(42), [ast.Pass()], [ast.Pass()]) 459 | self.verify(if_else, 'if 42:pass\nelse:pass') 460 | # if/elif/else 461 | if_elif_else = ast.If(ast.Num(6), [ast.Pass()], [if_else]) 462 | self.verify(if_elif_else, 'if 6:pass\n' 463 | 'elif 42:pass\n' 464 | 'else:pass') 465 | # if/else w/ a leading 'if' clause + extra 466 | if_else_extra = ast.If(ast.Num(6), [ast.Pass()], [if_, ast.Pass()]) 467 | self.verify(if_else_extra, 'if 6:pass\n' 468 | 'else:\n' 469 | ' if 42:pass\n' 470 | ' pass') 471 | # 'if' w/ a leading simple stmt but another non-simple stmt 472 | if_fancy_body = ast.If(ast.Num(6), [ast.Pass(), if_], []) 473 | self.verify(if_fancy_body, 'if 6:\n pass\n if 42:pass') 474 | 475 | def test_For(self): 476 | for_ = ast.For(ast.Name('target', ast.Load()), 477 | ast.Name('iter_', ast.Load()), [ast.Pass()], []) 478 | self.verify(for_, 'for target in iter_:pass') 479 | for_.orelse = [ast.Pass()] 480 | self.verify(for_, 'for target in iter_:pass\nelse:pass') 481 | for_.target = ast.Tuple([ast.Name('X', ast.Store()), ast.Name('Y', 482 | ast.Store())], ast.Store()) 483 | for_.orelse = [] 484 | self.verify(for_, 'for X,Y in iter_:pass') 485 | 486 | def test_While(self): 487 | while_ = ast.While(ast.Name('True', ast.Load()), [ast.Pass()], None) 488 | self.verify(while_, 'while True:pass') 489 | while_.orelse = [ast.Pass()] 490 | self.verify(while_, 'while True:pass\nelse:pass') 491 | 492 | def test_With(self): 493 | # with A: pass 494 | A = ast.Name('A', ast.Load()) 495 | A_clause = ast.withitem(A, None) 496 | with_A = ast.With([A_clause], [ast.Pass()]) 497 | self.verify(with_A, 'with A:pass') 498 | # with A as a: pass 499 | a = ast.Name('a', ast.Store()) 500 | a_clause = ast.withitem(A, a) 501 | with_a = ast.With([a_clause], [ast.Pass()]) 502 | self.verify(with_a, 'with A as a:pass') 503 | # with A as A, B: pass 504 | B = ast.Name('B', ast.Load()) 505 | B_clause = ast.withitem(B, None) 506 | with_B = ast.With([a_clause, B_clause], [ast.Pass()]) 507 | self.verify(with_B, 'with A as a,B:pass') 508 | # with A as A, B as b: pass 509 | b = ast.Name('b', ast.Store()) 510 | b_clause = ast.withitem(B, b) 511 | with_b = ast.With([a_clause, b_clause], [ast.Pass()]) 512 | self.verify(with_b, 'with A as a,B as b:pass') 513 | 514 | def test_ExceptHandler(self): 515 | except_ = ast.ExceptHandler(None, None, [ast.Pass()]) 516 | self.verify(except_, 'except:pass') 517 | except_.type = ast.Name('Exception', ast.Load()) 518 | self.verify(except_, 'except Exception:pass') 519 | except_.name = ast.Name('exc', ast.Store()) 520 | self.verify(except_, 'except Exception as exc:pass') 521 | 522 | def test_Try(self): 523 | # except 524 | exc_clause = ast.ExceptHandler(ast.Name('X', ast.Load()), None, 525 | [ast.Pass()]) 526 | exc_clause_2 = ast.ExceptHandler(None, None, [ast.Pass()]) 527 | try_except = ast.Try([ast.Pass()], [exc_clause, exc_clause_2], None, None) 528 | self.verify(try_except, 'try:pass\nexcept X:pass\nexcept:pass') 529 | # except/else 530 | try_except_else = ast.Try([ast.Pass()], [exc_clause, exc_clause_2], 531 | [ast.Pass()], None) 532 | self.verify(try_except_else, 533 | 'try:pass\nexcept X:pass\nexcept:pass\nelse:pass') 534 | # except/finally 535 | exc_clause = ast.ExceptHandler(None, None, [ast.Pass()]) 536 | try_except_finally = ast.Try([ast.Pass()], [exc_clause_2], None, 537 | [ast.Pass()]) 538 | self.verify(try_except_finally, 'try:pass\nexcept:pass\nfinally:pass') 539 | # except/else/finally 540 | try_except_else_finally = ast.Try([ast.Pass()], [exc_clause_2], 541 | [ast.Pass()], [ast.Pass()]) 542 | self.verify(try_except_else_finally, 543 | 'try:pass\nexcept:pass\nelse:pass\nfinally:pass') 544 | # else/finally 545 | try_else_finally = ast.Try([ast.Pass()], None, [ast.Pass()], 546 | [ast.Pass()]) 547 | self.verify(try_else_finally, 'try:pass\nelse:pass\nfinally:pass') 548 | # finally 549 | try_finally = ast.Try([ast.Pass()], None, None, [ast.Pass()]) 550 | self.verify(try_finally, 'try:pass\nfinally:pass') 551 | 552 | def test_AugAssign(self): 553 | for cls, op in self.operators.items(): 554 | aug_assign = ast.AugAssign(ast.Name('X', ast.Store()), cls(), 555 | ast.Num(1)) 556 | self.verify(aug_assign, 'X{}=1'.format(op)) 557 | self.verify(ast.AugAssign(ast.Name('X', ast.Store()), ast.Add(), 558 | ast.Tuple([ast.Num(1), ast.Num(2)], ast.Load())), 559 | 'X+=1,2') 560 | 561 | def test_Assert(self): 562 | assert_ = ast.Assert(ast.Num(42), None) 563 | self.verify(assert_, 'assert 42') 564 | assert_msg = ast.Assert(ast.Num(42), ast.Num(6)) 565 | self.verify(assert_msg, 'assert 42,6') 566 | 567 | def test_Global(self): 568 | glbl = ast.Global(['x']) 569 | self.verify(glbl, 'global x') 570 | many_glbl = ast.Global(['x', 'y']) 571 | self.verify(many_glbl, 'global x,y') 572 | 573 | def test_Nonlocal(self): 574 | nonlocal_ = ast.Nonlocal(['X']) 575 | self.verify(nonlocal_, 'nonlocal X') 576 | many_nonlocal = ast.Nonlocal(['X', 'Y']) 577 | self.verify(many_nonlocal, 'nonlocal X,Y') 578 | 579 | def test_FunctionDef(self): 580 | # Arguments 581 | args = self.create_arguments([self.create_arg('Y')]) 582 | with_args = ast.FunctionDef('X', args, [ast.Pass()], [], None) 583 | self.verify(with_args, 'def X(Y):pass') 584 | # Decorators 585 | decorated = ast.FunctionDef('X', self.create_arguments(), [ast.Pass()], 586 | [ast.Name('dec1', ast.Load()), ast.Name('dec2', ast.Load()), 587 | ast.Name('dec3', ast.Load())], 588 | None) 589 | self.verify(decorated, '@dec1\n@dec2\n@dec3\ndef X():pass') 590 | # Return annotation 591 | annotated = ast.FunctionDef('X', self.create_arguments(), [ast.Pass()], 592 | [], ast.Num(42)) 593 | self.verify(annotated, 'def X()->42:pass') 594 | 595 | def test_ClassDef(self): 596 | # class X: pass 597 | cls = ast.ClassDef('X', [], [], None, None, [ast.Pass()], []) 598 | self.verify(cls, 'class X:pass') 599 | # class X(Y): pass 600 | cls = ast.ClassDef('X', [ast.Name('Y', ast.Load())], [], None, None, 601 | [ast.Pass()], []) 602 | self.verify(cls, 'class X(Y):pass') 603 | # class X(Y=42): pass 604 | cls = ast.ClassDef('X', [], [ast.keyword('Y', ast.Num(42))], None, 605 | None, [ast.Pass()], []) 606 | self.verify(cls, 'class X(Y=42):pass') 607 | # class X(Z, Y=42): pass 608 | cls.bases.append(ast.Name('Z', ast.Load())) 609 | self.verify(cls, 'class X(Z,Y=42):pass') 610 | # class X(*args): pass 611 | cls = ast.ClassDef('X', [], [], ast.Name('args', ast.Load()), None, 612 | [ast.Pass()], []) 613 | self.verify(cls, 'class X(*args):pass') 614 | # class X(Y, *args): pass 615 | cls.bases.append(ast.Name('Y', ast.Load())) 616 | self.verify(cls, 'class X(Y,*args):pass') 617 | # class X(**kwargs): pass 618 | cls = ast.ClassDef('X', [], [], None, ast.Name('kwargs', ast.Load()), 619 | [ast.Pass()], []) 620 | self.verify(cls, 'class X(**kwargs):pass') 621 | # class X(Y, **kwargs): pass 622 | cls.bases.append(ast.Name('Y', ast.Load())) 623 | self.verify(cls, 'class X(Y,**kwargs):pass') 624 | # Decorators 625 | cls = ast.ClassDef('X', [], [], None, None, [ast.Pass()], 626 | [ast.Name('dec1', ast.Load()), 627 | ast.Name('dec2', ast.Load())]) 628 | self.verify(cls, '@dec1\n@dec2\nclass X:pass') 629 | 630 | def test_simple_statements(self): 631 | # Simple statements can be put on a single line as long as the scope 632 | # has not changed. 633 | for body, expect in [(ast.Expr(ast.Num(42)), '42'), 634 | (ast.Import([ast.alias('a', None)]), 'import a'), 635 | (ast.ImportFrom('b', [ast.alias('a', None)], 1), 636 | 'from .b import a'), 637 | (ast.Break(), 'break'), 638 | (ast.Continue(), 'continue'), 639 | (ast.Pass(), 'pass'), 640 | (ast.Assign([ast.Name('X', ast.Store())], ast.Num(42)), 641 | 'X=42'), 642 | (ast.Delete([ast.Name('X', ast.Del())]), 'del X'), 643 | (ast.Raise(None, None), 'raise'), 644 | (ast.Return(None), 'return'), 645 | (ast.AugAssign(ast.Name('X', ast.Store()), ast.Add(), 646 | ast.Num(42)), 'X+=42'), 647 | (ast.Assert(ast.Num(42), None), 'assert 42'), 648 | (ast.Global(['x']), 'global x'), 649 | (ast.Nonlocal(['x']), 'nonlocal x'), 650 | ]: 651 | if_simple = ast.If(ast.Num(42), [body], None) 652 | self.verify(if_simple, 'if 42:{}'.format(expect)) 653 | 654 | if_multiple_simples = ast.If(ast.Num(42), [ast.Pass(), ast.Pass()], 655 | None) 656 | self.verify(if_multiple_simples, 'if 42:pass;pass') 657 | inner_if = ast.If(ast.Num(6), [ast.Pass()], None) 658 | funky_if = ast.If(ast.Num(42), [ast.Break(), ast.Continue(), inner_if, 659 | ast.Break(), ast.Continue()], 660 | None) 661 | self.verify(funky_if, 662 | 'if 42:\n break;continue\n if 6:pass\n break;continue') 663 | 664 | def test_Interactive(self): 665 | self.verify(ast.Interactive([ast.Pass()]), 'pass') 666 | 667 | def test_Suite(self): 668 | self.verify(ast.Suite([ast.Pass()]), 'pass') 669 | 670 | def test_Expression(self): 671 | self.verify(ast.Expression(ast.Num(42)), '42') 672 | 673 | def test_coverage(self): 674 | # Make sure no expr and up node types are unimplemented. 675 | for type_name in dir(ast): 676 | type_ = getattr(ast, type_name) 677 | if hasattr(type_, '_fields') and len(type_._fields) > 0: 678 | if not hasattr(mnfy.SourceCode, 'visit_{}'.format(type_name)): 679 | self.fail('{} lacks a visitor method'.format(type_name)) 680 | 681 | @staticmethod 682 | def format_ast_compare_failure(reason, minified, original): # pragma: no cover 683 | min_details = ast.dump(minified) 684 | orig_details = ast.dump(original, include_attributes=True) 685 | return '{}: {} != {}'.format(reason, min_details, orig_details) 686 | 687 | @classmethod 688 | def compare_ast_nodes(cls, minified, original): # pragma: no cover 689 | """Check the node fields that can be a builtin type are correct.""" 690 | if minified.__class__ != original.__class__: 691 | raise TypeError(cls.format_ast_compare_failure('Type mismatch', 692 | minified, original)) 693 | elif isinstance(minified, (ast.FunctionDef, ast.ClassDef)): 694 | assert minified.name == original.name 695 | elif isinstance(minified, ast.ImportFrom): 696 | assert minified.module == original.module 697 | assert minified.level == original.level 698 | elif isinstance(minified, (ast.Global, ast.Nonlocal)): 699 | assert minified.names == original.names 700 | elif isinstance(minified, ast.Num): 701 | if minified.n != original.n: 702 | raise ValueError(cls.format_ast_compare_failure( 703 | 'Unequal numbers', minified, original)) 704 | elif isinstance(minified, (ast.Str, ast.Bytes)): 705 | assert minified.s == original.s 706 | elif isinstance(minified, ast.Attribute): 707 | assert minified.attr == original.attr 708 | elif isinstance(minified, ast.Name): 709 | assert minified.id == original.id 710 | elif isinstance(minified, ast.excepthandler): 711 | assert minified.name == original.name 712 | elif isinstance(minified, ast.arg): 713 | assert minified.arg == original.arg 714 | assert minified.annotation == original.annotation 715 | elif isinstance(minified, ast.keyword): 716 | assert minified.arg == original.arg 717 | elif isinstance(minified, ast.alias): 718 | assert minified.name == original.name 719 | assert minified.asname == original.asname 720 | 721 | @classmethod 722 | def test_roundtrip(cls, source=None): 723 | if source is None: # pragma: no cover 724 | try: 725 | source = inspect.getsource(mnfy) 726 | except IOError: 727 | pass 728 | original_ast = ast.parse(source) 729 | minifier = mnfy.SourceCode() 730 | minifier.visit(original_ast) 731 | minified_source = str(minifier) 732 | minified_ast = ast.parse(minified_source) 733 | node_pairs = zip(ast.walk(minified_ast), ast.walk(original_ast)) 734 | for minified, original in node_pairs: 735 | cls.compare_ast_nodes(minified, original) 736 | return minified_source 737 | 738 | 739 | if __name__ == '__main__': 740 | import os 741 | import sys 742 | import tokenize 743 | 744 | 745 | if len(sys.argv) > 2: 746 | raise RuntimeError('no more than one argument supported') 747 | elif len(sys.argv) == 2: 748 | if sys.argv[1] == '-': 749 | arg = os.path.dirname(os.__file__) 750 | else: 751 | arg = sys.argv[1] 752 | if os.path.isdir(arg): 753 | filenames = filter(lambda x: x.endswith('.py'), os.listdir(arg)) 754 | filenames = (os.path.join(arg, x) 755 | for x in os.listdir(arg) if x.endswith('.py')) 756 | else: 757 | filenames = [arg] 758 | 759 | source_total = 0 760 | minified_total = 0 761 | 762 | for filename in filenames: 763 | with open(filename, 'rb') as file: 764 | encoding = tokenize.detect_encoding(file.readline)[0] 765 | with open(filename, 'r', encoding=encoding) as file: 766 | source = file.read() 767 | print('Verifying', filename, '... ', end='') 768 | try: 769 | minified_source = SourceCodeEmissionTests.test_roundtrip(source) 770 | except: 771 | print() 772 | raise 773 | source_size = len(source.strip().encode('utf-8')) 774 | if source_size == 0: 775 | continue 776 | minified_size = len(minified_source.strip().encode('utf-8')) 777 | if minified_size > source_size: 778 | print() # Easier to see what file failed 779 | raise ValueError('minified source larger than original source; ' 780 | '{} > {}'.format(minified_size, source_size)) 781 | source_total += source_size 782 | minified_total += minified_size 783 | print('{}% smaller'.format(100 - int(minified_size/source_size * 100))) 784 | print('-' * 80) 785 | print('{:,} bytes (minified) vs. {:,} bytes (original)'.format(minified_total, source_total)) 786 | print('{}% smaller overall'.format(100 - int(minified_total/source_total * 100))) 787 | else: 788 | unittest.main() 789 | -------------------------------------------------------------------------------- /mnfy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.4 2 | """Minify Python source code.""" 3 | import ast 4 | import contextlib 5 | import functools 6 | import math 7 | import sys 8 | 9 | _python_version = 3, 4 10 | if sys.version_info[:2] != _python_version: # pragma: no cover 11 | raise ImportError('mnfy only supports Python {}.{}'.format(*_python_version)) 12 | 13 | 14 | _simple_nodes = { 15 | # Unary 16 | ast.Invert: '~', ast.Not: 'not ', ast.UAdd: '+', ast.USub: '-', 17 | # Binary 18 | ast.Add: '+', ast.Sub: '-', ast.Mult: '*', ast.Div: '/', ast.Mod: '%', 19 | ast.Pow: '**', ast.LShift: '<<', ast.RShift: '>>', ast.BitOr: '|', 20 | ast.BitXor: '^', ast.BitAnd: '&', ast.FloorDiv: '//', 21 | # Boolean 22 | ast.And: ' and ', ast.Or: ' or ', 23 | # Comparison 24 | ast.Eq: '==', ast.NotEq: '!=', ast.Lt: '<', ast.LtE: '<=', ast.Gt: '>', 25 | ast.GtE: '>=', ast.Is: ' is ', ast.IsNot: ' is not ', ast.In: ' in ', 26 | ast.NotIn: ' not in ', 27 | # One-word statements 28 | ast.Pass: 'pass', ast.Break: 'break', ast.Continue: 'continue', 29 | ast.Ellipsis: '...', 30 | } 31 | 32 | _simple_stmts = (ast.Expr, ast.Delete, ast.Pass, ast.Import, ast.ImportFrom, 33 | ast.Global, ast.Nonlocal, ast.Assert, ast.Break, ast.Continue, 34 | ast.Return, ast.Raise, ast.Assign, ast.AugAssign) 35 | 36 | _precedence = {expr: level for level, expr_list in enumerate([ 37 | [ast.Lambda], 38 | [ast.IfExp], 39 | [ast.Or], 40 | [ast.And], 41 | [ast.Not], 42 | #[ast.In, ast.NotIn, ast.Is, ast.IsNot, ast.Lt, ast.LtE, ast.Gt, 43 | # ast.GtE, ast.NotEq, ast.Eq], 44 | [ast.Compare], 45 | [ast.BitOr], 46 | [ast.BitXor], 47 | [ast.BitAnd], 48 | [ast.LShift, ast.RShift], 49 | [ast.Add, ast.Sub], 50 | [ast.Mult, ast.Div, ast.FloorDiv, ast.Mod], 51 | [ast.UAdd, ast.USub, ast.Invert], 52 | # The power operator ** binds less tightly than an arithmetic 53 | # or bitwise unary operator on its right, that is, 2**-1 is 0.5. 54 | [ast.Pow], 55 | [ast.Subscript, ast.Call, ast.Attribute], 56 | [ast.Tuple, ast.List, ast.Dict, ast.Set], 57 | ]) for expr in expr_list} 58 | 59 | 60 | def _create_visit_meth(ast_cls, op): 61 | """Create a method closure for an operator visitor.""" 62 | def visit_meth(self, node): 63 | self._write(op) 64 | visit_meth.__name__ = 'visit_' + ast_cls.__name__ 65 | return visit_meth 66 | 67 | 68 | def _add_simple_methods(cls): 69 | """Class decorator to add simple visit methods.""" 70 | for details in _simple_nodes.items(): 71 | meth = _create_visit_meth(*details) 72 | assert not hasattr(cls, meth.__name__), meth.__name__ 73 | setattr(cls, meth.__name__, meth) 74 | return cls 75 | 76 | 77 | @_add_simple_methods 78 | class SourceCode(ast.NodeVisitor): 79 | 80 | """Output minified source code for an AST tree. 81 | 82 | Any AST created by the source code generated from this class must be 83 | **exactly** the same (i.e., the AST can be roundtripped). 84 | 85 | """ 86 | 87 | # Some node types which are lacking any fields are purposefully skipped. 88 | 89 | def __init__(self): 90 | super().__init__() 91 | self._buffer = [] 92 | self._indent_level = 0 93 | 94 | def __str__(self): 95 | """Return the source code with no leading or trailing whitespace.""" 96 | return ''.join(self._buffer).strip() 97 | 98 | def _write(self, token): 99 | """Write a token into the source code buffer.""" 100 | assert isinstance(token, str), token # Keep calling instead of visit() 101 | self._buffer.append(token) 102 | 103 | def _peek(self): 104 | return self._buffer[-1] if self._buffer else None 105 | 106 | def _pop(self, expect=None): 107 | """Pop the last item off the buffer, optionally verifying what is 108 | popped was expected.""" 109 | popped = self._buffer.pop() 110 | if expect is not None: 111 | if popped != expect: 112 | msg = 'expected to pop {!r}, not {!r}'.format(expect, popped) 113 | raise ValueError(msg) 114 | return popped 115 | 116 | def _conditional_visit(self, *stuff): 117 | """Conditionally write/visit arguments if all of them are truth-like 118 | values. 119 | 120 | Used to conditionally write/visit arguments only when all passed-in AST 121 | node values actually have a value to work with. 122 | 123 | """ 124 | if not all(stuff): 125 | return 126 | else: 127 | self._visit_and_write(*stuff) 128 | 129 | def _visit_and_write(self, *stuff): 130 | """Visit/write arguments based on whether arguments are an AST node or 131 | not.""" 132 | for thing in stuff: 133 | if isinstance(thing, ast.AST): 134 | self.visit(thing) 135 | else: 136 | self._write(thing) 137 | 138 | def _seq_visit(self, nodes, sep=None): 139 | """Visit every node in the sequence.""" 140 | if not nodes: 141 | return 142 | for node in nodes: 143 | self.visit(node) 144 | if sep: 145 | self._write(sep) 146 | if sep: 147 | self._pop(sep) 148 | 149 | def _indent(self): 150 | """Indent a statement.""" 151 | self._write(' ' * self._indent_level) 152 | 153 | def _find_precedence(self, node): 154 | work_with = getattr(self, 155 | '_{}_precedence'.format(node.__class__.__name__), 156 | lambda x: x) 157 | return _precedence.get(work_with(node).__class__, None) 158 | 159 | def _visit_expr(self, node, scope, *, break_tie=False): 160 | """Visit a node, adding parentheses as needed for proper precedence. 161 | 162 | If break_tie is True then add parentheses for 'node' even when 163 | precedence is equal between the node and 'scope'. 164 | 165 | """ 166 | node_precedence = self._find_precedence(node) 167 | scope_precedence = self._find_precedence(scope) 168 | if (node_precedence is not None and scope_precedence is not None and 169 | (node_precedence < scope_precedence or 170 | (node_precedence == scope_precedence and break_tie))): 171 | self._visit_and_write('(', node, ')') 172 | else: 173 | self.visit(node) 174 | 175 | def _visit_body(self, body, *, indent=True): 176 | """Visit the list of statements that represent the body of a block 177 | statement. 178 | 179 | If all statements are simple, then separate the statements using 180 | semi-colons, else use newlines for all statement and indent properly 181 | when handling simple statements (block statements handle their own 182 | indenting). 183 | 184 | """ 185 | self._indent_level += 1 if indent else 0 186 | if all(map(lambda x: isinstance(x, _simple_stmts), body)): 187 | self._seq_visit(body, ';') 188 | else: 189 | all_stmts = [] 190 | simples = [] 191 | for node in body: 192 | # Drop indent level check to needlessly put ALL simple 193 | # statements on the same line when possible. 194 | if isinstance(node, _simple_stmts) and self._indent_level: 195 | simples.append(node) 196 | else: 197 | if simples: 198 | all_stmts.append(simples) 199 | simples = [] 200 | all_stmts.append(node) 201 | if simples: 202 | all_stmts.append(simples) 203 | for thing in all_stmts: 204 | if self._peek() != '\n': 205 | self._write('\n') 206 | if isinstance(thing, list): 207 | self._indent() 208 | self._seq_visit(thing, ';') 209 | else: 210 | self.visit(thing) 211 | if self._peek() != '\n': 212 | self._write('\n') 213 | self._indent_level -= 1 if indent else 0 214 | 215 | def visit_Module(self, node): 216 | self._visit_body(node.body, indent=False) 217 | 218 | def visit_Interactive(self, node): 219 | self._visit_body(node.body, indent=False) 220 | 221 | def visit_Suite(self, node): 222 | self._visit_body(node.body, indent=False) 223 | 224 | def visit_Expression(self, node): 225 | self._visit_expr(node.body, None) 226 | 227 | def _Num_precedence(self, node): 228 | if node.n < 0: # For Pow's left operator 229 | return ast.USub() 230 | else: 231 | return node 232 | 233 | def _write_int(self, num): 234 | if abs(num) >= 10**12: 235 | num_str = hex(num) 236 | else: 237 | num_str = str(num) 238 | self._write(num_str) 239 | 240 | def _write_float(self, num): 241 | # Work with the string representation to avoid floating point quirks. 242 | num_str = str(num) 243 | # Scientific notation with a positive mantissa where appropriate, 244 | # else strip the trailing zero. 245 | if num_str.endswith('.0'): 246 | as_int = num_str[:-2] 247 | num_str = num_str[:-1] 248 | if as_int != '0' and as_int.endswith('0'): 249 | mantissa = 1 # Known by endswith() check in the 'if' guard 250 | while as_int[-mantissa-1] == '0': 251 | mantissa += 1 252 | coefficient_digits = as_int[:-mantissa] 253 | if len(coefficient_digits) == 1: 254 | coefficient = coefficient_digits 255 | else: 256 | coefficient = '{}.{}'.format(coefficient_digits[0], 257 | coefficient_digits[1:]) 258 | mantissa += len(coefficient_digits[1:]) 259 | sci_notation = '{}e{}'.format(coefficient, mantissa) 260 | if len(sci_notation) < len(num_str): 261 | num_str = sci_notation 262 | # Scientific notation with a negative mantissa where appropriate, else 263 | # strip the leading zero. 264 | elif num_str.startswith('0.'): 265 | num_str = num_str[1:] 266 | if num_str.startswith('.00'): 267 | mantissa = -3 268 | while num_str[-mantissa] == '0': 269 | mantissa -= 1 270 | coefficient_digits = num_str[-mantissa:] 271 | if len(coefficient_digits) == 1: 272 | coefficient = coefficient_digits 273 | else: 274 | coefficient = '{}.{}'.format(coefficient_digits[0], 275 | coefficient_digits[1:]) 276 | sci_notation = '{}e{}'.format(coefficient, mantissa) 277 | if len(sci_notation) < len(num_str): 278 | num_str = sci_notation 279 | # Scientific notation from Python with a single digit, negative mantissa 280 | # has an unneeded leading zero. 281 | elif 'e' in num_str: 282 | coefficient, _, mantissa = num_str.partition('e') 283 | if mantissa[:2] == '-0': 284 | mantissa = mantissa[2:] 285 | num_str = '{}e-{}'.format(coefficient, mantissa) 286 | self._write(num_str) 287 | 288 | def visit_Num(self, node): 289 | num = node.n 290 | if isinstance(num, int): 291 | self._write_int(num) 292 | elif isinstance(num, float): 293 | self._write_float(num) 294 | elif isinstance(num, complex): 295 | self._write(str(num)) 296 | else: 297 | raise TypeError('Num must be in, float, or complex, not {}'.format(num)) 298 | 299 | def visit_Str(self, node): 300 | self._write(repr(node.s)) 301 | 302 | def visit_Bytes(self, node): 303 | self._write(repr(node.s)) 304 | 305 | def visit_NameConstant(self, node): 306 | self._write(repr(node.value)) 307 | 308 | def visit_Starred(self, node): 309 | self._visit_and_write('*', node.value) 310 | 311 | def visit_Dict(self, node): 312 | """ 313 | {1: 1, 2: 2} 314 | {1:1,2:2} 315 | 316 | """ 317 | self._write('{') 318 | for key, value in zip(node.keys, node.values): 319 | self._visit_and_write(key, ':', value, ',') 320 | if node.keys: 321 | self._pop(',') 322 | self._write('}') 323 | 324 | def visit_Set(self, node): 325 | """ 326 | {1, 2} 327 | {1,2} 328 | 329 | """ 330 | self._write('{') 331 | self._seq_visit(node.elts, ',') 332 | self._write('}') 333 | 334 | def visit_Tuple(self, node, *, parens=True): 335 | """ 336 | (1, 2, 3) 337 | (1,2,3) 338 | 339 | """ 340 | if parens or not node.elts: 341 | self._write('(') 342 | if len(node.elts) == 1: 343 | self.visit(node.elts[0]) 344 | self._write(',') 345 | else: 346 | self._seq_visit(node.elts, ',') 347 | if parens or not node.elts: 348 | self._write(')') 349 | 350 | def visit_List(self, node): 351 | """ 352 | [1, 2, 3] 353 | [1,2,3] 354 | 355 | """ 356 | self._write('[') 357 | self._seq_visit(node.elts, ',') 358 | self._write(']') 359 | 360 | def visit_comprehension(self, node): 361 | self._visit_and_write(' for ', node.target, ' in ', node.iter) 362 | for if_ in node.ifs: 363 | self._visit_and_write(' if ', if_) 364 | 365 | def _visit_seq_comp(self, node, ends): 366 | self._visit_and_write(ends[0], node.elt) 367 | self._seq_visit(node.generators) 368 | self._write(ends[1]) 369 | 370 | def visit_ListComp(self, node): 371 | self._visit_seq_comp(node, '[]') 372 | 373 | def visit_SetComp(self, node): 374 | self._visit_seq_comp(node, '{}') 375 | 376 | def visit_GeneratorExp(self, node): 377 | self._visit_seq_comp(node, '()') 378 | 379 | def visit_DictComp(self, node): 380 | self._visit_and_write('{', node.key, ':', node.value) 381 | self._seq_visit(node.generators) 382 | self._write('}') 383 | 384 | def visit_Name(self, node): 385 | self._write(node.id) 386 | 387 | def visit_Assign(self, node): 388 | """ 389 | X = Y 390 | X=Y 391 | 392 | """ 393 | for target in node.targets: 394 | if isinstance(target, ast.Tuple): 395 | self.visit_Tuple(target, parens=False) 396 | else: 397 | self.visit(target) 398 | self._write('=') 399 | if isinstance(node.value, ast.Tuple): 400 | self.visit_Tuple(node.value, parens=False) 401 | else: 402 | self.visit(node.value) 403 | 404 | def visit_AugAssign(self, node): 405 | """ 406 | X += 1 407 | X+=1 408 | 409 | """ 410 | self._visit_and_write(node.target, node.op, '=') 411 | if isinstance(node.value, ast.Tuple): 412 | self.visit_Tuple(node.value, parens=False) 413 | else: 414 | self.visit(node.value) 415 | 416 | def visit_Delete(self, node): 417 | """ 418 | del X, Y 419 | del X,Y 420 | 421 | """ 422 | self._write('del ') 423 | self._seq_visit(node.targets, ',') 424 | 425 | def visit_Raise(self, node): 426 | self._write('raise') 427 | self._conditional_visit(' ', node.exc) 428 | self._conditional_visit(' from ', node.cause) 429 | 430 | def visit_Assert(self, node): 431 | """ 432 | assert X, Y 433 | assert X,Y 434 | 435 | """ 436 | self._visit_and_write('assert ', node.test) 437 | self._conditional_visit(',', node.msg) 438 | 439 | def _UnaryOp_precedence(self, node): 440 | return node.op 441 | 442 | def visit_UnaryOp(self, node): 443 | self.visit(node.op) 444 | self._visit_expr(node.operand, node) 445 | 446 | def _BinOp_precedence(self, node): 447 | return node.op 448 | 449 | def visit_BinOp(self, node): 450 | """ 451 | 2 + 3 452 | 2+3 453 | 454 | """ 455 | self._visit_expr(node.left, node) 456 | self.visit(node.op) 457 | self._visit_expr(node.right, node, break_tie=True) 458 | 459 | def visit_Compare(self, node): 460 | """ 461 | 2 < 3 < 4 462 | 2<3<4 463 | 464 | """ 465 | self._visit_expr(node.left, node, break_tie=True) 466 | for comparator, value in zip(node.ops, node.comparators): 467 | self.visit(comparator) 468 | self._visit_expr(value, node, break_tie=True) 469 | 470 | def visit_Attribute(self, node): 471 | self._visit_expr(node.value, node) 472 | self._visit_and_write('.', node.attr) 473 | 474 | def visit_keyword(self, node): 475 | self._visit_and_write(node.arg, '=', node.value) 476 | 477 | def visit_Call(self, node): 478 | """ 479 | fxn(1, 2) 480 | fxn(1,2) 481 | 482 | """ 483 | self.visit(node.func) 484 | genexp_only = (len(node.args) == 1 and not node.keywords and 485 | not node.starargs and not node.kwargs and 486 | isinstance(node.args[0], ast.GeneratorExp)) 487 | if genexp_only: 488 | # visit_GeneratorExp will handle the parentheses. 489 | self.visit_GeneratorExp(node.args[0]) 490 | else: 491 | self._write('(') 492 | self._seq_visit(node.args, ',') 493 | wrote = len(node.args) > 0 494 | if node.keywords: 495 | if wrote: 496 | self._write(',') 497 | self._seq_visit(node.keywords, ',') 498 | wrote = True 499 | if node.starargs: 500 | if wrote: 501 | self._write(',') 502 | self._visit_and_write('*', node.starargs) 503 | wrote = True 504 | if node.kwargs: 505 | if wrote: 506 | self._write(',') 507 | self._visit_and_write('**', node.kwargs) 508 | self._write(')') 509 | 510 | def visit_IfExp(self, node): 511 | self._visit_and_write(node.body, ' if ', node.test, ' else ', 512 | node.orelse) 513 | 514 | def visit_Slice(self, node): 515 | self._conditional_visit(node.lower) 516 | self._write(':') 517 | self._conditional_visit(node.upper) 518 | self._conditional_visit(':', node.step) 519 | 520 | def visit_ExtSlice(self, node): 521 | self._seq_visit(node.dims, ',') 522 | 523 | def visit_Index(self, node): 524 | self.visit(node.value) 525 | 526 | def visit_Subscript(self, node): 527 | self._visit_expr(node.value, ast.Subscript()) 528 | self._visit_and_write('[', node.slice, ']') 529 | 530 | def visit_arg(self, node): 531 | self._write(node.arg) 532 | self._conditional_visit(':', node.annotation) 533 | 534 | def _write_args(self, args, defaults): 535 | """Write out an arguments node's positional and default arguments.""" 536 | slice_bound = -len(defaults) or len(args) 537 | non_defaults = args[:slice_bound] 538 | args_with_defaults = zip(args[slice_bound:], defaults) 539 | self._seq_visit(non_defaults, ',') 540 | if non_defaults and defaults: 541 | self._write(',') 542 | for arg, default in args_with_defaults: 543 | # Thanks to len(kw_defaults) == len(kwonlyargs) 544 | if default is not None: 545 | self._visit_and_write(arg, '=', default, ',') 546 | else: 547 | self._visit_and_write(arg, ',') 548 | if defaults: 549 | self._pop(',') 550 | 551 | def visit_arguments(self, node): 552 | """ 553 | x, y 554 | x,y 555 | 556 | """ 557 | self._write_args(node.args, node.defaults) 558 | wrote = bool(node.args) 559 | if node.vararg or node.kwonlyargs: 560 | if wrote: 561 | self._write(',') 562 | self._write('*') 563 | wrote=True 564 | if node.vararg: 565 | self.visit(node.vararg) 566 | if node.kwonlyargs: 567 | self._write(',') 568 | wrote = True 569 | self._write_args(node.kwonlyargs, node.kw_defaults) 570 | if node.kwarg: 571 | if wrote: 572 | self._write(',') 573 | self._write('**') 574 | self.visit(node.kwarg) 575 | 576 | def visit_Lambda(self, node): 577 | """ 578 | lambda: x 579 | lambda:x 580 | 581 | """ 582 | args = node.args 583 | if args.args or args.vararg or args.kwonlyargs or args.kwarg: 584 | self._visit_and_write('lambda ', args) 585 | else: 586 | self._write('lambda') 587 | self._visit_and_write(':', node.body) 588 | 589 | def visit_alias(self, node): 590 | self._write(node.name) 591 | self._conditional_visit(' as ', node.asname) 592 | 593 | # Needed for proper simple statement EOL formatting. 594 | def visit_Expr(self, node): 595 | self.visit(node.value) 596 | 597 | def visit_Return(self, node): 598 | self._write('return') 599 | if node.value: 600 | self._write(' ') 601 | if isinstance(node.value, ast.Tuple): 602 | self.visit_Tuple(node.value, parens=False) 603 | else: 604 | self.visit(node.value) 605 | 606 | def visit_Yield(self, node): 607 | self._write('yield') 608 | if node.value: 609 | self._write(' ') 610 | if isinstance(node.value, ast.Tuple): 611 | self.visit_Tuple(node.value, parens=False) 612 | else: 613 | self.visit(node.value) 614 | 615 | def visit_YieldFrom(self, node): 616 | # Python 3.3.0 claims 'value' is optional, but grammar prevents that. 617 | self._visit_and_write('yield from ', node.value) 618 | 619 | def _global_nonlocal_visit(self, node): 620 | """ 621 | X, Y 622 | X,Y 623 | 624 | """ 625 | self._write(node.__class__.__name__.lower() + ' ') 626 | self._write(','.join(node.names)) 627 | 628 | # Not ``visit_Global = global_nonlocal_visit`` for benefit of decorators. 629 | def visit_Global(self, node): 630 | self._global_nonlocal_visit(node) 631 | 632 | # Not ``visit_Nonlocal = global_nonlocal_visit`` for benefit of decorators. 633 | def visit_Nonlocal(self, node): 634 | self._global_nonlocal_visit(node) 635 | 636 | def visit_Import(self, node): 637 | """ 638 | import X, Y 639 | import X,Y 640 | 641 | """ 642 | self._write('import ') 643 | self._seq_visit(node.names, ',') 644 | 645 | def visit_ImportFrom(self, node): 646 | """ 647 | from . import x, y 648 | from . import x,y 649 | 650 | """ 651 | self._write('from ') 652 | if node.level: 653 | self._write('.' * node.level) 654 | if node.module: 655 | self._write(node.module) 656 | self._write(' import ') 657 | self._seq_visit(node.names, ',') 658 | 659 | def _BoolOp_precedence(self, node): 660 | return node.op 661 | 662 | def visit_BoolOp(self, node): 663 | if isinstance(node.op, ast.And): 664 | op = ' and ' 665 | else: # ast.Or 666 | op = ' or ' 667 | left = node.values[0] 668 | self._visit_expr(left, node) 669 | for value in node.values[1:]: 670 | self._write(op) 671 | self._visit_expr(value, node, break_tie=True) 672 | 673 | def visit_If(self, node, *, elif_=False): 674 | """ 675 | if X: 676 | if Y: 677 | pass 678 | if X: 679 | if Y:pass 680 | 681 | """ 682 | self._indent() 683 | self._visit_and_write('if ' if not elif_ else 'elif ', node.test, ':') 684 | self._visit_body(node.body) 685 | if node.orelse: 686 | if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If): 687 | self.visit_If(node.orelse[0], elif_=True) 688 | else: 689 | self._indent() 690 | self._write('else:') 691 | self._visit_body(node.orelse) 692 | 693 | def visit_For(self, node): 694 | """ 695 | for x in y: pass 696 | for x in y:pass 697 | 698 | """ 699 | self._indent() 700 | self._write('for ') 701 | if isinstance(node.target, ast.Tuple): 702 | self.visit_Tuple(node.target, parens=False) 703 | else: 704 | self.visit(node.target) 705 | self._visit_and_write(' in ', node.iter, ':') 706 | self._visit_body(node.body) 707 | if node.orelse: 708 | self._indent() 709 | self._write('else:') 710 | self._visit_body(node.orelse) 711 | 712 | def visit_While(self, node): 713 | """ 714 | while x: pass 715 | while x:pass 716 | 717 | """ 718 | self._indent() 719 | self._visit_and_write('while ', node.test, ':') 720 | self._visit_body(node.body) 721 | if node.orelse: 722 | self._indent() 723 | self._write('else:') 724 | self._visit_body(node.orelse) 725 | 726 | def visit_withitem(self, node): 727 | self.visit(node.context_expr) 728 | self._conditional_visit(' as ', node.optional_vars) 729 | 730 | def visit_With(self, node): 731 | """ 732 | with X as x: pass 733 | 734 | with X as x:pass 735 | 736 | """ 737 | self._indent() 738 | self._write('with ') 739 | self._seq_visit(node.items, ',') 740 | self._write(':') 741 | self._visit_body(node.body) 742 | 743 | def visit_ExceptHandler(self, node): 744 | """ 745 | except X as Y: pass 746 | except X as Y:pass 747 | 748 | """ 749 | self._indent() 750 | self._write('except') 751 | self._conditional_visit(' ', node.type) 752 | self._conditional_visit(' as ', node.name) 753 | self._write(':') 754 | self._visit_body(node.body) 755 | 756 | def visit_Try(self, node): 757 | self._indent() 758 | self._write('try:') 759 | self._visit_body(node.body) 760 | if node.handlers: 761 | self._seq_visit(node.handlers) 762 | if node.orelse: 763 | self._indent() 764 | self._write('else:') 765 | self._visit_body(node.orelse) 766 | if node.finalbody: 767 | self._indent() 768 | self._write('finally:') 769 | self._visit_body(node.finalbody) 770 | 771 | def visit_FunctionDef(self, node): 772 | """ 773 | def X() -> Y: pass 774 | def X()->Y:pass 775 | 776 | """ 777 | for decorator in node.decorator_list: 778 | self._indent() 779 | self._visit_and_write('@', decorator, '\n') 780 | self._indent() 781 | self._visit_and_write('def ', node.name, '(', node.args, ')') 782 | self._conditional_visit('->', node.returns) 783 | self._write(':') 784 | self._visit_body(node.body) 785 | 786 | def visit_ClassDef(self, node): 787 | """ 788 | class X(W, *V, W=1): pass 789 | class X(W,*V,W=1):pass 790 | 791 | """ 792 | need_parens = node.bases or node.keywords or node.starargs or node.kwargs 793 | for decorator in node.decorator_list: 794 | self._indent() 795 | self._visit_and_write('@', decorator, '\n') 796 | self._indent() 797 | self._visit_and_write('class ', node.name) 798 | if need_parens: 799 | self._write('(') 800 | wrote = False 801 | if node.bases: 802 | wrote = True 803 | self._seq_visit(node.bases, ',') 804 | if node.keywords: 805 | if wrote: 806 | self._write(',') 807 | self._seq_visit(node.keywords, ',') 808 | wrote = True 809 | if node.starargs: 810 | if wrote: 811 | self._write(',') 812 | self._visit_and_write('*', node.starargs) 813 | wrote = True 814 | if node.kwargs: 815 | if wrote: 816 | self._write(',') 817 | self._visit_and_write('**', node.kwargs) 818 | wrote = True 819 | if need_parens: 820 | self._write(')') 821 | self._write(':') 822 | self._visit_body(node.body) 823 | 824 | 825 | class CombineImports(ast.NodeTransformer): 826 | 827 | """Combine import statements immediately following each other into a single 828 | statement:: 829 | 830 | import X 831 | import Y 832 | 833 | import X,Y # Savings: 7 834 | 835 | Also do the same for ``from ... import ...`` statements:: 836 | 837 | from X import Y 838 | from X import Z 839 | 840 | from X import Y,Z # Savings: 14 841 | 842 | Re-ordering is not performed to prevent import side-effects from being 843 | executed in a different order (e.g., triggering a circular import). 844 | 845 | """ 846 | 847 | def __init__(self): 848 | self._last_stmt = None 849 | super().__init__() 850 | 851 | def visit(self, node): 852 | node = super().visit(node) 853 | if node is not None and isinstance(node, ast.stmt): 854 | self._last_stmt = node 855 | return node 856 | 857 | def _possible(self, want): 858 | if self._last_stmt is not None and isinstance(self._last_stmt, want): 859 | return True 860 | else: 861 | return False 862 | 863 | def visit_Import(self, node): 864 | """Combine imports with any directly preceding ones (when possible).""" 865 | if not self._possible(ast.Import): 866 | return node 867 | self._last_stmt.names.extend(node.names) 868 | return None 869 | 870 | def visit_ImportFrom(self, node): 871 | """Combine ``from ... import`` when consecutive and have the same 872 | 'from' clause.""" 873 | if not self._possible(ast.ImportFrom): 874 | return node 875 | elif (node.module != self._last_stmt.module or 876 | node.level != self._last_stmt.level): 877 | return node 878 | self._last_stmt.names.extend(node.names) 879 | return None 880 | 881 | 882 | class CombineWithStatements(ast.NodeTransformer): 883 | 884 | """Nest 'with' statements. 885 | 886 | with A: 887 | with B: 888 | pass 889 | 890 | with A,B: 891 | pass # savings: 4 per additional statement 892 | 893 | """ 894 | 895 | def visit_With(self, node): 896 | self.generic_visit(node) 897 | if len(node.body) == 1 and isinstance(node.body[0], ast.With): 898 | child_with = node.body[0] 899 | node.items.extend(child_with.items) 900 | node.body = child_with.body 901 | return node 902 | 903 | 904 | class EliminateUnusedConstants(ast.NodeTransformer): 905 | 906 | """Remove any side-effect-free constant used as a statement. 907 | 908 | This will primarily remove docstrings (the equivalent of running Python 909 | with `-OO`). 910 | 911 | Any blocks which end up empty by this transformation will have a 'pass' 912 | statement inserted to keep them syntactically correct. 913 | 914 | """ 915 | 916 | constants = ast.Num, ast.Str, ast.Bytes 917 | 918 | def visit_Expr(self, node): 919 | """Return None if the Expr contains a side-effect-free constant.""" 920 | return None if isinstance(node.value, self.constants) else node 921 | 922 | def visit_Pass(self, node): 923 | """The 'pass' statement is the epitome of an unused constant.""" 924 | return None 925 | 926 | def _visit_body(self, node): 927 | """Generically guarantee at least *some* statement exists in a block 928 | body to stay syntactically correct while allowing side-effects outside 929 | the body to continue to exist (e.g. the guard of an 'if' statement).""" 930 | node = self.generic_visit(node) 931 | if len(node.body) == 0: 932 | node.body.append(ast.Pass()) 933 | return node 934 | 935 | visit_FunctionDef = _visit_body 936 | visit_ClassDef = _visit_body 937 | visit_For = _visit_body 938 | visit_While = _visit_body 939 | visit_If = _visit_body 940 | visit_With = _visit_body 941 | visit_ExceptHandler = _visit_body 942 | visit_Try = _visit_body 943 | 944 | 945 | class IntegerToPower(ast.NodeTransformer): 946 | 947 | """Transform integers of a large enough size to a power of 2 or 10. 948 | 949 | 10**5 -> 10000 950 | """ 951 | 952 | def visit_Num(self, node): 953 | num = node.n 954 | if not isinstance(num, int): 955 | return node 956 | if num >= 10**5 and not math.log10(num) % 1: 957 | power_10 = int(math.log10(num)) 958 | return ast.BinOp(ast.Num(10), ast.Pow(), ast.Num(power_10)) 959 | elif num >= 2**17 and not math.log2(num) % 1: 960 | power_2 = int(math.log2(num)) 961 | return ast.BinOp(ast.Num(2), ast.Pow(), ast.Num(power_2)) 962 | else: 963 | return node 964 | 965 | 966 | class DropPass(ast.NodeTransformer): 967 | 968 | """Drops any and all 'pass' statements which are not by themselves.""" 969 | 970 | def _filter(self, body): 971 | if len(body) == 1: 972 | return body 973 | return [stmt for stmt in body if not isinstance(stmt, ast.Pass)] 974 | 975 | def _can_blank(self, fields, node): 976 | for field in fields: 977 | stmts = getattr(node, field) 978 | if len(stmts) == 1 and isinstance(stmts[0], ast.Pass): 979 | setattr(node, field, []) 980 | return node 981 | 982 | def _visit_node(self, node_type, stmt_fields, node): 983 | node = self.generic_visit(node) 984 | new_fields = node.__dict__.copy() 985 | for field in stmt_fields: 986 | new_fields[field] = self._filter(getattr(node, field)) 987 | return node_type(**new_fields) 988 | 989 | def _visit_body(self, node_type, node): 990 | return self._visit_node(node_type, {'body'}, node) 991 | 992 | def _visit_body_orelse(self, node_type, node): 993 | return self._visit_node(node_type, {'body', 'orelse'}, node) 994 | 995 | def visit_Module(self, node): 996 | return self._visit_body(ast.Module, node) 997 | 998 | def visit_Interactive(self, node): 999 | return self._visit_body(ast.Interactive, node) 1000 | 1001 | def visit_Suite(self, node): 1002 | return self._visit_body(ast.Suite, node) 1003 | 1004 | def visit_FunctionDef(self, node): 1005 | return self._visit_body(ast.FunctionDef, node) 1006 | 1007 | def visit_ClassDef(self, node): 1008 | return self._visit_body(ast.ClassDef, node) 1009 | 1010 | def visit_For(self, node): 1011 | node = self._visit_body_orelse(ast.For, node) 1012 | return self._can_blank({'orelse'}, node) 1013 | 1014 | def visit_While(self, node): 1015 | node = self._visit_body_orelse(ast.While, node) 1016 | return self._can_blank({'orelse'}, node) 1017 | 1018 | def visit_If(self, node): 1019 | node = self._visit_body_orelse(ast.If, node) 1020 | return self._can_blank({'orelse'}, node) 1021 | 1022 | def visit_With(self, node): 1023 | return self._visit_body(ast.With, node) 1024 | 1025 | def visit_Try(self, node): 1026 | extra_fields = frozenset(['orelse', 'finalbody']) 1027 | fields = extra_fields.union(['body']) 1028 | node = self._visit_node(ast.Try, fields, node) 1029 | node = self._can_blank(extra_fields, node) 1030 | if len(node.handlers) == 0: 1031 | if all(len(getattr(node, stmts)) == 0 for stmts in extra_fields): 1032 | return node.body 1033 | return node 1034 | 1035 | def visit_ExceptHandler(self, node): 1036 | return self._visit_body(ast.ExceptHandler, node) 1037 | 1038 | 1039 | safe_transforms = [ 1040 | CombineImports, 1041 | CombineWithStatements, 1042 | EliminateUnusedConstants, 1043 | IntegerToPower, 1044 | # Run last so other transforms can just use 'pass' without 1045 | # worrying. 1046 | DropPass 1047 | ] 1048 | 1049 | 1050 | class FunctionToLambda(ast.NodeTransformer): 1051 | 1052 | """Tranform a (qualifying) function definition into a lambda assigned to a 1053 | variable of the same name.:: 1054 | 1055 | def identity(x):return x 1056 | 1057 | identity=lambda x:x # Savings: 5 1058 | 1059 | This is an UNSAFE tranformation as lambdas are not exactly the same as a 1060 | function object (e.g., lack of a __name__ attribute). 1061 | 1062 | """ 1063 | 1064 | def visit_FunctionDef(self, node): 1065 | """Return a lambda instead of a function if the body is only a Return 1066 | node, there are no decorators, and no annotations.""" 1067 | # Can't have decorators or a returns annotation. 1068 | if node.decorator_list or node.returns: 1069 | return node 1070 | # Body must be of length 1 and consist of a Return node; 1071 | # can't translate a body consisting of only an Expr node as that would 1072 | # lead to an improper return value (i.e., something other than None 1073 | # potentially). 1074 | if len(node.body) > 1 or not isinstance(node.body[0], ast.Return): 1075 | return node 1076 | args = node.args 1077 | # No annotations for *args or **kwargs. 1078 | if ((args.vararg and args.vararg.annotation) or 1079 | (args.kwarg and args.kwarg.annotation)): 1080 | return node 1081 | # No annotations on any other parameters. 1082 | if any(arg.annotation for arg in args.args): 1083 | return node 1084 | # In the clear! 1085 | return_ = node.body[0].value 1086 | if return_ is None: 1087 | return_ = ast.Name('None', ast.Load()) 1088 | lambda_ = ast.Lambda(args, return_) 1089 | return ast.Assign([ast.Name(node.name, ast.Store())], lambda_) 1090 | 1091 | 1092 | if __name__ == '__main__': 1093 | import argparse 1094 | 1095 | arg_parser = argparse.ArgumentParser( 1096 | description='Minify Python source code') 1097 | arg_parser.add_argument('filename', 1098 | help='path to Python source file') 1099 | arg_parser.add_argument('--safe-transforms', action='store_true', 1100 | default=False, 1101 | help='Perform safe transformations on the AST; ' 1102 | "equivalent of Python's `-OO` (default)") 1103 | arg_parser.add_argument('--function-to-lambda', action='append_const', 1104 | dest='unsafe_transforms', const=FunctionToLambda, 1105 | help='Transform functions to lambdas ' 1106 | '(UNSAFE: lambda objects differ slightly ' 1107 | 'from function objects)') 1108 | args = arg_parser.parse_args() 1109 | 1110 | with open(args.filename, 'rb') as source_file: 1111 | source = source_file.read() 1112 | source_ast = ast.parse(source) 1113 | if args.unsafe_transforms: 1114 | for transform in args.unsafe_transforms: 1115 | transformer = transform() 1116 | source_ast = transformer.visit(source_ast) 1117 | if args.safe_transforms: 1118 | for transform in safe_transforms: 1119 | transformer = transform() 1120 | source_ast = transformer.visit(source_ast) 1121 | minifier = SourceCode() 1122 | minifier.visit(source_ast) 1123 | print(str(minifier)) 1124 | --------------------------------------------------------------------------------