├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.rst ├── flask_ext_migrate ├── __init__.py └── startup.py ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── test_fix.py └── test_script.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | env 5 | env* 6 | dist 7 | *.egg 8 | *.egg-info 9 | _mailinglist 10 | .tox 11 | *.swp 12 | .cache 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | 4 | python: 5 | - "2.7" 6 | - "pypy" 7 | - "3.4" 8 | - "3.5" 9 | 10 | install: 11 | - pip install tox>=1.8 flake8 12 | 13 | script: 14 | - tox -e \ 15 | $(echo py$TRAVIS_PYTHON_VERSION | tr -d . | sed -e 's/pypypy/pypy/') 16 | - flake8 17 | 18 | notifications: 19 | email: false 20 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | flask_ext_migrate is written and maintained by Keyan Pishdadian 2 | 3 | 4 | Other contributors are (in alphabetical order): 5 | * Gregory Vigo Torres 6 | * Ivan Cvitkovic 7 | * Markus Unterwaditzer 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 by Keyan Pishdadian. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. All advertising materials mentioning features or use of this software 12 | must display the following acknowledgement: 13 | This product includes software developed by Keyan Pishdadian. 14 | 4. Neither the name of the copyright holder nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ''AS IS'' 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | flask_ext_migrate: import migration tool 2 | ======================================== 3 | 4 | .. image:: https://img.shields.io/travis/pallets/flask-ext-migrate.svg 5 | :target: https://travis-ci.org/pallets/flask-ext-migrate 6 | .. image:: https://img.shields.io/pypi/v/flask_ext_migrate.svg 7 | :target: https://pypi.python.org/pypi/flask_ext_migrate 8 | 9 | This package allows for rapid fixing of old style Flask extension imports from 10 | the format `flask.ext.foo` to `flask_foo`. It also repairs any associated 11 | function calls. Although this tool has been tested extensively always check 12 | the output file to ensure correct functionality. 13 | 14 | Installation 15 | ------------ 16 | 17 | To install, simply: 18 | 19 | .. code-block:: bash 20 | 21 | $ pip install flask_ext_migrate 22 | 23 | Usage 24 | ----- 25 | 26 | :: 27 | 28 | $ flask_ext_migrate --help 29 | usage: flask_ext_migrate 30 | 31 | A source code migration tool for converting extension imports. 32 | -------------------------------------------------------------------------- 33 | https://github.com/pocoo/flask-ext-migrate 34 | 35 | required arguments: 36 | Either a single file or directory to be recursively converted 37 | 38 | optional arguments: 39 | --help Show this help message and exit 40 | 41 | For example to convert the imports in a file `app.py` use: 42 | 43 | .. code-block:: bash 44 | 45 | $ flask_ext_migrate app.py 46 | 47 | To convert all imports in all files within the directory `app/` (relative path) use: 48 | 49 | .. code-block:: bash 50 | 51 | $ flask_ext_migrate app 52 | 53 | # This also works. 54 | $ flask_ext_migrate app/ 55 | -------------------------------------------------------------------------------- /flask_ext_migrate/__init__.py: -------------------------------------------------------------------------------- 1 | from redbaron import RedBaron 2 | 3 | 4 | def read_source(input_file): 5 | """Parses the input_file into a RedBaron FST.""" 6 | with open(input_file, "r") as source_code: 7 | red = RedBaron(source_code.read()) 8 | return red 9 | 10 | 11 | def write_source(red, input_file): 12 | """Overwrites the input_file once the FST has been modified.""" 13 | with open(input_file, "w") as source_code: 14 | source_code.write(red.dumps()) 15 | 16 | 17 | def fix_imports(red): 18 | """Wrapper which fixes "from" style imports and then "import" style.""" 19 | red = fix_standard_imports(red) 20 | red = fix_from_imports(red) 21 | return red 22 | 23 | 24 | def fix_from_imports(red): 25 | """ 26 | Converts "from" style imports to not use "flask.ext". 27 | 28 | Handles (with or without parens or linebreaks): 29 | from flask.ext.foo import bam --> from flask_foo import bam 30 | from flask.ext.foo.bar import bam --> from flask_foo.bar import bam 31 | from flask.ext import foo --> import flask_foo as foo 32 | """ 33 | from_imports = red.find_all("FromImport") 34 | for node in from_imports: 35 | modules = node.value 36 | 37 | if (len(modules) < 2 or 38 | modules[0].value != 'flask' or 39 | modules[1].value != 'ext'): 40 | continue 41 | 42 | if len(modules) >= 3: 43 | name_str = '' 44 | if len(node.targets) == 1: 45 | name = node.targets[0].target 46 | module = node.targets[0].value 47 | 48 | if (name and name != module): 49 | name_str = '%s as %s' % (module, name) 50 | else: 51 | name_str = module 52 | else: 53 | for target in node.targets: 54 | name_str += target.value 55 | 56 | if not target.next: 57 | continue 58 | 59 | if (target.type == 'name_as_name' 60 | and target.next.type != 'right_parenthesis'): 61 | name_str += ', ' 62 | 63 | modules_str = '.'.join([i.value for i in modules[2:]]) 64 | 65 | node.replace('from flask_%s import %s' 66 | % (modules_str, name_str)) 67 | 68 | elif len(modules) == 2: 69 | module = node.modules()[0] 70 | node.replace("import flask_%s as %s" 71 | % (module, module)) 72 | return red 73 | 74 | 75 | def fix_standard_imports(red): 76 | """ 77 | Handles import modification in the form: 78 | import flask.ext.foo" --> import flask_foo 79 | """ 80 | imports = red.find_all("ImportNode") 81 | for x, node in enumerate(imports): 82 | try: 83 | if (node.value[0].value[0].value == 'flask' and 84 | node.value[0].value[1].value == 'ext'): 85 | package = node.value[0].value[2].value 86 | name = node.names()[0].split('.')[-1] 87 | if name == package: 88 | node.replace("import flask_%s" % (package)) 89 | else: 90 | node.replace("import flask_%s as %s" % (package, name)) 91 | except IndexError: 92 | pass 93 | 94 | return red 95 | 96 | 97 | def fix_function_calls(red): 98 | """ 99 | Modifies function calls in the source to reflect import changes. 100 | 101 | Searches the AST for AtomtrailerNodes and replaces them. 102 | """ 103 | atoms = red.find_all("Atomtrailers") 104 | for x, node in enumerate(atoms): 105 | try: 106 | if (node.value[0].value == 'flask' and 107 | node.value[1].value == 'ext'): 108 | params = _form_function_call(node) 109 | node.replace("flask_%s%s" % (node.value[2], params)) 110 | except IndexError: 111 | pass 112 | 113 | return red 114 | 115 | 116 | def _form_function_call(node): 117 | """ 118 | Reconstructs function call strings when making attribute access calls. 119 | """ 120 | node_vals = node.value 121 | output = "." 122 | for x, param in enumerate(node_vals[3::]): 123 | if param.dumps()[0] == "(": 124 | output = output[0:-1] + param.dumps() 125 | return output 126 | else: 127 | output += param.dumps() + "." 128 | 129 | 130 | def fix_tester(string): 131 | """Wrapper which allows for testing when not running from shell.""" 132 | ast = RedBaron(string) 133 | ast = fix_imports(ast) 134 | ast = fix_function_calls(ast) 135 | return ast.dumps() 136 | 137 | 138 | def fix(input_file): 139 | """Wrapper for user argument checking and import fixing.""" 140 | ast = read_source(input_file) 141 | ast = fix_imports(ast) 142 | ast = fix_function_calls(ast) 143 | write_source(ast, input_file) 144 | -------------------------------------------------------------------------------- /flask_ext_migrate/startup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from . import fix 6 | 7 | 8 | @click.command() 9 | @click.argument('input_source', required=True) 10 | def startup(input_source): 11 | if os.path.isdir(input_source): 12 | files_to_fix = get_source_files(input_source) 13 | elif os.path.isfile(input_source): 14 | files_to_fix = [input_source] 15 | else: 16 | raise click.UsageError( 17 | 'You must provide a valid filename or directory.' 18 | ) 19 | 20 | for filepath in files_to_fix: 21 | fix(filepath) 22 | 23 | 24 | def get_source_files(directory): 25 | for root, _, files in os.walk(directory): 26 | for filename in files: 27 | if filename.endswith('.py'): 28 | yield os.path.join(root, filename) 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='flask_ext_migrate', 7 | version='1.0.1', 8 | url='https://github.com/pallets/flask-ext-migrate', 9 | license='BSD', 10 | author='Keyan Pishdadian', 11 | author_email='kpishdadian@gmail.com', 12 | description='A sourcecode manipulation tool for converting imports.', 13 | long_description='This tool allows for rapid migration of extension ' 14 | 'imports away from the deprecated `.ext` format.', 15 | install_requires=['redbaron==0.6.2', 'baron==0.6.2', 'click'], 16 | tests_require=['nose'], 17 | packages=['flask_ext_migrate'], 18 | entry_points={ 19 | 'console_scripts': [ 20 | 'flask_ext_migrate = flask_ext_migrate.startup:startup'] 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/flask-ext-migrate/913e598e602979959a095d93057a6f7e0f3e0e05/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | import pytest 3 | 4 | 5 | @pytest.fixture(scope='function') 6 | def runner(request): 7 | return CliRunner() 8 | -------------------------------------------------------------------------------- /tests/test_fix.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import flask_ext_migrate as migrate 4 | 5 | 6 | def test_simple_from_import(): 7 | output = migrate.fix_tester("from flask.ext import foo") 8 | assert output == "import flask_foo as foo" 9 | 10 | 11 | def test_non_flask_import_unchanged(): 12 | output = migrate.fix_tester("import requests") 13 | assert output == "import requests" 14 | 15 | 16 | def test_base_flask_import_unchanged(): 17 | output = migrate.fix_tester("import flask") 18 | assert output == "import flask" 19 | 20 | 21 | def test_base_flask_from_import_unchanged(): 22 | output = migrate.fix_tester("from flask import Flask") 23 | assert output == "from flask import Flask" 24 | 25 | 26 | def test_base_non_flask_from_import_doesnt_raise(): 27 | try: 28 | migrate.fix_tester("from foo import bar") 29 | except Exception as e: 30 | pytest.fail(e) 31 | 32 | 33 | def test_base_non_flask_from_import_unchanged(): 34 | output = migrate.fix_tester("from foo import bar") 35 | assert output == "from foo import bar" 36 | 37 | 38 | def test_invalid_import_doesnt_raise(): 39 | try: 40 | migrate.fix_tester("import adjfsjdn") 41 | except Exception as e: 42 | pytest.fail(e) 43 | 44 | 45 | def test_invalid_import_unchanged(): 46 | output = migrate.fix_tester("import adjfsjdn") 47 | assert output == "import adjfsjdn" 48 | 49 | 50 | def test_from_to_from_import(): 51 | output = migrate.fix_tester("from flask.ext.foo import bar") 52 | assert output == "from flask_foo import bar" 53 | 54 | 55 | def test_from_to_from_named_import(): 56 | output = migrate.fix_tester("from flask.ext.foo import bar as baz") 57 | assert output == "from flask_foo import bar as baz" 58 | 59 | 60 | def test_from_to_from_samename_import(): 61 | output = migrate.fix_tester("from flask.ext.foo import bar as bar") 62 | assert output == "from flask_foo import bar" 63 | 64 | 65 | def test_from_to_from_samename_subpackages_import(): 66 | output = migrate.fix_tester("from flask.ext.foo.bar import baz as baz") 67 | assert output == "from flask_foo.bar import baz" 68 | 69 | 70 | def test_multiple_import(): 71 | output = migrate.fix_tester( 72 | "from flask.ext.foo import bar, foobar, something" 73 | ) 74 | assert output == "from flask_foo import bar, foobar, something" 75 | 76 | 77 | def test_multiline_import(): 78 | output = migrate.fix_tester("from flask.ext.foo import \ 79 | bar,\ 80 | foobar,\ 81 | something") 82 | assert output == "from flask_foo import bar, foobar, something" 83 | 84 | 85 | def test_module_import(): 86 | output = migrate.fix_tester("import flask.ext.foo") 87 | assert output == "import flask_foo" 88 | 89 | 90 | def test_named_module_import(): 91 | output = migrate.fix_tester("import flask.ext.foo as foobar") 92 | assert output == "import flask_foo as foobar" 93 | 94 | 95 | def test_named_from_import(): 96 | output = migrate.fix_tester("from flask.ext.foo import bar as baz") 97 | assert output == "from flask_foo import bar as baz" 98 | 99 | 100 | def test_parens_import(): 101 | output = migrate.fix_tester("from flask.ext.foo import (bar, foo, foobar)") 102 | assert output == "from flask_foo import (bar, foo, foobar)" 103 | 104 | 105 | def test_from_subpackages_import(): 106 | output = migrate.fix_tester("from flask.ext.foo.bar import foobar") 107 | assert output == "from flask_foo.bar import foobar" 108 | 109 | 110 | def test_from_subpackages_named_import(): 111 | output = migrate.fix_tester( 112 | "from flask.ext.foo.bar import foobar as foobaz" 113 | ) 114 | assert output == "from flask_foo.bar import foobar as foobaz" 115 | 116 | 117 | def test_from_subpackages_parens_import(): 118 | output = migrate.fix_tester( 119 | "from flask.ext.foo.bar import (foobar, foobarz, foobarred)" 120 | ) 121 | assert output == "from flask_foo.bar import (foobar, foobarz, foobarred)" 122 | 123 | 124 | def test_multiline_from_subpackages_import(): 125 | output = migrate.fix_tester("from flask.ext.foo.bar import (foobar,\ 126 | foobarz,\ 127 | foobarred)") 128 | assert output == "from flask_foo.bar import (foobar, foobarz, foobarred)" 129 | 130 | 131 | def test_function_call_migration(): 132 | output = migrate.fix_tester("flask.ext.foo(var)") 133 | assert output == "flask_foo(var)" 134 | 135 | 136 | def test_nested_function_call_migration(): 137 | output = migrate.fix_tester("import flask.ext.foo\n\n" 138 | "flask.ext.foo.bar(var)") 139 | assert output == ("import flask_foo\n\n" 140 | "flask_foo.bar(var)") 141 | -------------------------------------------------------------------------------- /tests/test_script.py: -------------------------------------------------------------------------------- 1 | from flask_ext_migrate.startup import startup 2 | import flask_ext_migrate as migrate 3 | 4 | 5 | def test_single_file_input_runs_without_failures(runner, tmpdir): 6 | import_line = 'from flask.ext.foo import bar' 7 | temp_file = tmpdir.join('temp.py') 8 | temp_file.write(import_line) 9 | 10 | result = runner.invoke(startup, [str(temp_file)]) 11 | assert result.exit_code == 0 12 | assert temp_file.read() == migrate.fix_tester(import_line) 13 | 14 | 15 | def test_no_file_arg_fails(runner): 16 | result = runner.invoke(startup, []) 17 | assert result.exit_code != 0 18 | 19 | 20 | def test_single_file_run_modifies_file_properly(runner, tmpdir): 21 | import_line = 'from flask.ext.foo import bar' 22 | temp_file = tmpdir.join('temp.py') 23 | temp_file.write(import_line) 24 | 25 | result = runner.invoke(startup, [str(temp_file)]) 26 | assert result.exit_code == 0 27 | assert temp_file.read() == migrate.fix_tester(import_line) 28 | 29 | 30 | def test_recursive_runs_without_failures(runner, tmpdir): 31 | import_line = 'from flask.ext.foo import bar' 32 | 33 | temp_files = [] 34 | for x in range(2): 35 | temp_files.append(tmpdir.join('temp{}.py'.format(x))) 36 | 37 | for filepath in temp_files: 38 | filepath.write(import_line) 39 | 40 | result = runner.invoke(startup, [str(tmpdir)]) 41 | 42 | assert result.exit_code == 0 43 | 44 | 45 | def test_recursive_run_modifies_files_properly(runner, tmpdir): 46 | import_line = 'from flask.ext.foo import bar' 47 | expected_output = migrate.fix_tester(import_line) 48 | 49 | temp_files = [] 50 | for x in range(2): 51 | temp_files.append(tmpdir.join('temp{}.py'.format(x))) 52 | 53 | for filepath in temp_files: 54 | filepath.write(import_line) 55 | 56 | result = runner.invoke(startup, [str(tmpdir)]) 57 | 58 | assert result.exit_code == 0 59 | for filepath in temp_files: 60 | assert filepath.read() == expected_output 61 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,pypy,py34,py35 3 | 4 | [testenv] 5 | commands = 6 | py.test [] 7 | 8 | deps= 9 | pytest 10 | greenlet 11 | redbaron==0.6.2 12 | baron==0.6.2 13 | 14 | [flake8] 15 | exclude=.tox,examples,docs 16 | --------------------------------------------------------------------------------