├── .github └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── .readthedocs.yml ├── CHANGELOG.rst ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── conf.py ├── fixers.rst ├── index.rst └── make.bat ├── modernize ├── __init__.py ├── __main__.py ├── fixes │ ├── __init__.py │ ├── fix_basestring.py │ ├── fix_classic_division.py │ ├── fix_dict_six.py │ ├── fix_file.py │ ├── fix_filter.py │ ├── fix_import.py │ ├── fix_imports_six.py │ ├── fix_input_six.py │ ├── fix_int_long_tuple.py │ ├── fix_itertools_imports_six.py │ ├── fix_itertools_six.py │ ├── fix_map.py │ ├── fix_metaclass.py │ ├── fix_next.py │ ├── fix_open.py │ ├── fix_print.py │ ├── fix_raise.py │ ├── fix_raise_six.py │ ├── fix_unichr.py │ ├── fix_unicode.py │ ├── fix_unicode_future.py │ ├── fix_unicode_type.py │ ├── fix_urllib_six.py │ ├── fix_xrange_six.py │ └── fix_zip.py ├── main.py └── utils.py ├── pyproject.toml ├── setup.cfg └── tests ├── test_fix_basestring.py ├── test_fix_classic_division.py ├── test_fix_dict_six.py ├── test_fix_file.py ├── test_fix_filter.py ├── test_fix_import.py ├── test_fix_imports_six.py ├── test_fix_input_six.py ├── test_fix_int_long_tuple.py ├── test_fix_itertools_imports_six.py ├── test_fix_map.py ├── test_fix_metaclass.py ├── test_fix_next.py ├── test_fix_open.py ├── test_fix_print.py ├── test_fix_raise.py ├── test_fix_unichr_type.py ├── test_fix_unicode.py ├── test_fix_unicode_type.py ├── test_fix_urllib_six.py ├── test_fix_xrange_six.py ├── test_fix_zip.py ├── test_fixes.py ├── test_future_behaviour.py ├── test_main.py ├── test_raise_six.py └── utils.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - v* 8 | pull_request: 9 | 10 | jobs: 11 | tox: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [3.6, 3.7, 3.8, 3.9] 17 | os: [macOS-latest, ubuntu-latest, windows-latest] 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | 23 | - name: Set Up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Get pip cache dir 29 | id: pip-cache 30 | run: | 31 | echo "::set-output name=dir::$(pip cache dir)" 32 | 33 | - name: pip cache 34 | uses: actions/cache@v2 35 | with: 36 | path: ${{ steps.pip-cache.outputs.dir }} 37 | key: 38 | ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml', 'setup.py', 39 | 'setup.cfg') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pip- 42 | 43 | - name: Install 44 | run: | 45 | pip install tox 46 | 47 | - name: tox 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | run: tox -e py,lint,coveralls,release 51 | 52 | - name: upload dist 53 | uses: actions/upload-artifact@v2 54 | with: 55 | name: ${{ matrix.os }}_${{ matrix.python-version}}_dist 56 | path: dist 57 | 58 | all-successful: 59 | # https://github.community/t/is-it-possible-to-require-all-github-actions-tasks-to-pass-without-enumerating-them/117957/4?u=graingert 60 | runs-on: ubuntu-latest 61 | needs: [tox] 62 | steps: 63 | - name: Download dists for PyPI 64 | uses: actions/download-artifact@v2 65 | with: 66 | name: ubuntu-latest_3.8_dist 67 | path: dist 68 | 69 | - name: Display structure of donwloaded files 70 | run: ls -R 71 | 72 | - name: Publish package 73 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 74 | uses: pypa/gh-action-pypi-publish@master 75 | with: 76 | user: __token__ 77 | password: ${{ secrets.pypi_password }} 78 | 79 | - name: note that all tests succeeded 80 | run: echo "🎉" 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v2.7.4 4 | hooks: 5 | - id: pyupgrade 6 | args: ["--py36-plus"] 7 | 8 | - repo: https://github.com/psf/black 9 | rev: 20.8b1 10 | hooks: 11 | - id: black 12 | args: ["--target-version", "py36"] 13 | 14 | - repo: https://gitlab.com/pycqa/flake8 15 | rev: 3.8.4 16 | hooks: 17 | - id: flake8 18 | additional_dependencies: [flake8-2020] 19 | 20 | - repo: https://github.com/pycqa/isort 21 | rev: 5.7.0 22 | hooks: 23 | - id: isort 24 | 25 | - repo: https://github.com/pre-commit/pygrep-hooks 26 | rev: v1.7.0 27 | hooks: 28 | - id: python-check-blanket-noqa 29 | 30 | - repo: https://github.com/pre-commit/pre-commit-hooks 31 | rev: v3.4.0 32 | hooks: 33 | - id: check-merge-conflict 34 | - id: check-toml 35 | - id: check-yaml 36 | - id: mixed-line-ending 37 | 38 | - repo: https://github.com/pre-commit/mirrors-prettier 39 | rev: v2.2.1 40 | hooks: 41 | - id: prettier 42 | args: [--prose-wrap=always, --print-width=88] 43 | 44 | - repo: https://github.com/myint/autoflake 45 | rev: v1.4 46 | hooks: 47 | - id: autoflake 48 | args: 49 | - --in-place 50 | - --remove-all-unused-imports 51 | - --expand-star-imports 52 | - --remove-duplicate-keys 53 | - --remove-unused-variables 54 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - &modernize 2 | id: modernize 3 | name: modernize 4 | description: Modernizes Python code for eventual Python 3 migration. 5 | entry: modernize 6 | args: [--write, --fix=default, --nobackups] 7 | language: python 8 | types: [python] 9 | - <<: *modernize 10 | id: python-modernize 11 | name: modernize (deprecated) 12 | description: "Deprecated in favour of `id: modernize`" 13 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | fail_on_warning: true 5 | 6 | python: 7 | version: 3.8 8 | install: 9 | - method: pip 10 | path: . 11 | extra_requirements: 12 | - docs 13 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.9rc1 (unreleased) 2 | =================== 3 | 4 | - Nothing changed yet. 5 | 6 | 7 | 0.9rc0 (2020-10-01) 8 | =================== 9 | 10 | - move all code under the ``modernize`` namespace. 11 | - use flit to create PyPI distributions. 12 | 13 | 14 | 0.8.0 (2020-09-27) 15 | ================== 16 | 17 | Features 18 | -------- 19 | 20 | * add ``modernize`` console_script 21 | 22 | Breaking 23 | -------- 24 | * use ``fissix`` instead of deprecated ``lib2to3`` https://github.com/PyCQA/modernize/pull/203 25 | modernize itself will no-longer run under Python 2, or Python <3.6, but will 26 | always be able to process Python 2 code. 27 | 28 | Bugfixes 29 | -------- 30 | * Fix for ``dict.viewitems()``, ``dict.iteritems()`` etc in chained calls https://github.com/PyCQA/modernize/pull/181 31 | * Fix for SLASHEQUAL ``/=`` in fix_classic_divivion https://github.com/PyCQA/modernize/pull/197 32 | 33 | Docs/tests/meta 34 | --------------- 35 | * Travis CI: Add Python 3.7, 3.8 and more flake8 tests https://github.com/PyCQA/modernize/pull/199 36 | * ``six`` documentation has moved to https://six.readthedocs.io/ https://github.com/PyCQA/modernize/pull/198 37 | * Fix typo in help string for --enforce option https://github.com/PyCQA/modernize/pull/191 38 | * move project to https://github.com/PyCQA/modernize/ https://github.com/PyCQA/modernize/pull/220 https://github.com/PyCQA/modernize/pull/215 39 | * switch from Travis CI to Github Actions https://github.com/PyCQA/modernize/pull/224 40 | * use tox, pre-commit, pyupgrade and black https://github.com/PyCQA/modernize/pull/216 41 | 42 | 43 | 0.8rc4 (2020-09-27) 44 | =================== 45 | 46 | - Nothing changed yet. 47 | 48 | 49 | 0.8rc2 (2020-09-22) 50 | =================== 51 | 52 | Features 53 | -------- 54 | 55 | * add ``modernize`` console_script 56 | 57 | Meta 58 | ---- 59 | 60 | * move project to https://github.com/PyCQA/modernize/ https://github.com/PyCQA/modernize/pull/220 https://github.com/PyCQA/modernize/pull/215 61 | * switch from Travis CI to Github Actions https://github.com/PyCQA/modernize/pull/224 62 | * use tox, pre-commit, pyupgrade and black https://github.com/PyCQA/modernize/pull/216 63 | 64 | Version 0.8rc1 65 | ============== 66 | 67 | Released 2020-07-20. 68 | 69 | Breaking 70 | -------- 71 | * use ``fissix`` instead of deprecated ``lib2to3`` https://github.com/PyCQA/modernize/pull/203 72 | modernize itself will no-longer run under Python 2, or Python <3.6, but will 73 | always be able to process Python 2 code. 74 | 75 | Bugfixes 76 | -------- 77 | * Fix for ``dict.viewitems()``, ``dict.iteritems()`` etc in chained calls https://github.com/PyCQA/modernize/pull/181 78 | * Fix for SLASHEQUAL ``/=`` in fix_classic_divivion https://github.com/PyCQA/modernize/pull/197 79 | 80 | Docs/tests/meta 81 | --------------- 82 | * Travis CI: Add Python 3.7, 3.8 and more flake8 tests https://github.com/PyCQA/modernize/pull/199 83 | * ``six`` documentation has moved to https://six.readthedocs.io/ https://github.com/PyCQA/modernize/pull/198 84 | * Fix typo in help string for --enforce option https://github.com/PyCQA/modernize/pull/191 85 | 86 | Version 0.5-0.7 87 | =============== 88 | 89 | * Added the opt-in classic_division fixer. 90 | * Updated the ``dict_six`` fixer to support ``six.viewitems()`` and friends. 91 | * New fixer for ``unichr``, changed to ``six.unichr``. 92 | * Documentation corrections. 93 | 94 | 95 | Version 0.4 96 | =========== 97 | 98 | Released 2014-10-14. 99 | 100 | * `Documentation`_ has been added. 101 | * All fixers are now idempotent, which allows modernize to safely be applied 102 | more than once to the same source code. 103 | * The option to include default fixers when ``-f`` options are used is now 104 | spelled ``-f default``, rather than ``-f all``. 105 | * Added a ``--version`` option to the modernize command. 106 | * Calls to ``zip``, ``map``, and ``filter`` are now wrapped with ``list()`` 107 | in non-iterator contexts, to preserve Python 2 semantics. 108 | * Improved fixer for ``xrange`` using ``six.moves.range``. 109 | * Simplified use of ``six.with_metaclass`` for classes with more than 110 | one base class. 111 | * New fixer for imports of renamed standard library modules, using 112 | ``six.moves``. 113 | * New fixer to add ``from __future__ import absolute_import`` to all 114 | files with imports, and change any implicit relative imports to explicit 115 | (see PEP 328). 116 | * New fixer for ``input()`` and ``raw_input()``, changed to ``eval(input())`` 117 | and ``input()`` respectively. 118 | * New fixer for ``file()``, changed to ``open()``. There is also an 119 | opt-in fixer that changes both of these to ``io.open()``. 120 | * New fixer for ``(int, long)`` or ``(long, int)``, changed to 121 | ``six.integer_types``. Other references to ``long`` are changed to ``int``. 122 | * New fixer for ``basestring``, changed to ``six.string_types``. 123 | * New fixer for ``unicode``, changed to ``six.text_type``. 124 | * The ``fix_next`` fixer uses the ``next()`` builtin rather than 125 | ``six.advance_iterator``. 126 | * There is test coverage for all ``libmodernize`` fixers. 127 | * Simplified the implementation of many ``libmodernize`` fixers by extending 128 | similar fixers from ``lib2to3``. 129 | * Fixed a bug where ``fix_raise_six`` was adding an incorrect import 130 | statement. 131 | * Support for targeting Python 2.5 or lower has been officially dropped. 132 | (Previously some fixers did output constructs that were only added in 133 | Python 2.6, such as the ``except ... as`` construct, but this was not 134 | documented.) 135 | 136 | .. _Documentation: https://modernize.readthedocs.org/en/latest/ 137 | 138 | 139 | Version 0.3 140 | =========== 141 | 142 | Released 2014-08-12. 143 | 144 | * New fixer for ``raise E, V, T``, changed to ``six.reraise(E, V, T)``. 145 | * New fixer for metaclasses, using ``six.with_metaclass``. 146 | * Avoid adding redundant parentheses to ``print(x)``. 147 | * modernize can now be installed and run on Python 3. 148 | * Fixed a bug where ``__future__`` imports were added multiple times. 149 | * Fixed a bug where fixer for ``zip()`` was recognising ``map()``. 150 | * The default is now to leave Unicode literals unchanged. 151 | (In previous versions this required the ``--compat-unicode`` option, 152 | which has now been removed.) A new ``--six-unicode`` option has been 153 | added to obtain the previous behaviour of adding ``six.u`` wrappers 154 | around Unicode literals. 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 by Armin Ronacher. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | 15 | * The names of the contributors may not be used to endorse or 16 | promote products derived from this software without specific 17 | prior written permission. 18 | 19 | Parts of this software package are built on fissix which is 20 | a backport of lib2to3 both licensed under the PSF license. 21 | For more information refer to the 22 | Python license: https://docs.python.org/license.html 23 | and the fissix license https://github.com/jreese/fissix/blob/master/LICENSE 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | :: 2 | 3 | Python _ _ 4 | _ __ ___ __| |___ _ _ _ _ (_)______ 5 | | ' \/ _ \/ _` / -_) '_| ' \| |_ / -_) 6 | |_|_|_\___/\__,_\___|_| |_||_|_/__\___| 7 | 8 | 9 | .. image:: https://img.shields.io/coveralls/github/PyCQA/modernize?label=coveralls&logo=coveralls 10 | :alt: Coveralls 11 | :target: https://coveralls.io/github/PyCQA/modernize 12 | .. image:: https://img.shields.io/readthedocs/modernize?logo=read-the-docs 13 | :alt: Read the Docs 14 | :target: https://modernize.readthedocs.io/en/latest/ 15 | .. image:: https://img.shields.io/github/workflow/status/PyCQA/modernize/CI?label=GitHub%20Actions&logo=github 16 | :alt: GitHub Actions 17 | :target: https://github.com/PyCQA/modernize 18 | .. image:: https://img.shields.io/pypi/v/modernize?logo=pypi 19 | :alt: PyPI 20 | :target: https://pypi.org/project/modernize/ 21 | 22 | Modernize is a Python program that reads Python 2 source code 23 | and applies a series of fixers to transform it into source code 24 | that is valid on both Python 3 and Python 2.7. 25 | 26 | This allows you to run your test suite on Python 2.7 and Python 3 27 | so you can gradually port your code to being fully Python 3 28 | compatible without slowing down development of your Python 2 29 | project. 30 | 31 | The ``python -m modernize`` command works like 32 | ``python -m fissix``, see `fissix `_. 33 | Here's how you'd rewrite a 34 | single file:: 35 | 36 | python -m modernize -w example.py 37 | 38 | It does not guarantee, but it attempts to spit out a codebase compatible 39 | with Python 2.6+ or Python 3. The code that it generates has a runtime 40 | dependency on `six `_, unless the 41 | ``--no-six`` option is used. Version 1.9.0 or later of ``six`` is 42 | recommended. Some of the fixers output code that is not compatible with 43 | Python 2.5 or lower. 44 | 45 | Once your project is ready to run in production on Python 3 it's 46 | recommended to drop Python 2.7 support using 47 | `pyupgrade `_ 48 | 49 | **Documentation:** `modernize.readthedocs.io 50 | `_. 51 | 52 | See the ``LICENSE`` file for the license of ``modernize``. 53 | Using this tool does not affect licensing of the modernized code. 54 | 55 | This library is a very thin wrapper around `fissix 56 | `_, a fork of lib2to3. 57 | -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/modernize.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/modernize.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/modernize" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/modernize" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # modernize documentation build configuration file, created by 3 | # sphinx-quickstart on Fri Sep 26 08:36:35 2014. 4 | # 5 | # This file is execfile()d with the current directory set to its 6 | # 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 | from __future__ import generator_stop 15 | 16 | # -- General configuration ------------------------------------------------ 17 | 18 | # If your documentation needs a minimal Sphinx version, state it here. 19 | # needs_sphinx = '1.0' 20 | 21 | # Add any Sphinx extension module names here, as strings. They can be 22 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 23 | # ones. 24 | extensions = ["sphinx.ext.intersphinx"] 25 | 26 | # Add any paths that contain templates here, relative to this directory. 27 | templates_path = ["_templates"] 28 | 29 | # The suffix of source filenames. 30 | source_suffix = ".rst" 31 | 32 | # The encoding of source files. 33 | # source_encoding = 'utf-8-sig' 34 | 35 | # The master toctree document. 36 | master_doc = "index" 37 | 38 | # General information about the project. 39 | project = "modernize" 40 | copyright = "2014, python-modernize team; 2020 pycqa/modernize team" 41 | 42 | # The version info for the project you're documenting, acts as replacement for 43 | # |version| and |release|, also used in various other places throughout the 44 | # built documents. 45 | # 46 | # The short X.Y version. 47 | version = "0.7" 48 | # The full version, including alpha/beta/rc tags. 49 | release = version 50 | 51 | # The language for content autogenerated by Sphinx. Refer to documentation 52 | # for a list of supported languages. 53 | # language = None 54 | 55 | # There are two options for replacing |today|: either, you set today to some 56 | # non-false value, then it is used: 57 | # today = '' 58 | # Else, today_fmt is used as the format for a strftime call. 59 | # today_fmt = '%B %d, %Y' 60 | 61 | # List of patterns, relative to source directory, that match files and 62 | # directories to ignore when looking for source files. 63 | exclude_patterns = ["_build"] 64 | 65 | # The reST default role (used for this markup: `text`) to use for all 66 | # documents. 67 | # default_role = None 68 | 69 | # If true, '()' will be appended to :func: etc. cross-reference text. 70 | # add_function_parentheses = True 71 | 72 | # If true, the current module name will be prepended to all description 73 | # unit titles (such as .. function::). 74 | # add_module_names = True 75 | 76 | # If true, sectionauthor and moduleauthor directives will be shown in the 77 | # output. They are ignored by default. 78 | # show_authors = False 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = "sphinx" 82 | 83 | # A list of ignored prefixes for module index sorting. 84 | # modindex_common_prefix = [] 85 | 86 | # If true, keep warnings as "system message" paragraphs in the built documents. 87 | # keep_warnings = False 88 | 89 | intersphinx_mapping = { 90 | "python": ("https://docs.python.org/3", None), 91 | "python2": ("https://docs.python.org/2", None), 92 | "six": ("https://six.readthedocs.io/", None), 93 | "fissix": ("https://fissix.readthedocs.io/en/latest/", None), 94 | } 95 | 96 | 97 | # -- Options for HTML output ---------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = "default" 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | # html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | # html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | # html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | # html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | # html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | # html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = [] 131 | 132 | # Add any extra paths that contain custom files (such as robots.txt or 133 | # .htaccess) here, relative to this directory. These files are copied 134 | # directly to the root of the documentation. 135 | # html_extra_path = [] 136 | 137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 138 | # using the given strftime format. 139 | # html_last_updated_fmt = '%b %d, %Y' 140 | 141 | # If true, SmartyPants will be used to convert quotes and dashes to 142 | # typographically correct entities. 143 | # html_use_smartypants = True 144 | 145 | # Custom sidebar templates, maps document names to template names. 146 | # html_sidebars = {} 147 | 148 | # Additional templates that should be rendered to pages, maps page names to 149 | # template names. 150 | # html_additional_pages = {} 151 | 152 | # If false, no module index is generated. 153 | # html_domain_indices = True 154 | 155 | # If false, no index is generated. 156 | # html_use_index = True 157 | 158 | # If true, the index is split into individual pages for each letter. 159 | # html_split_index = False 160 | 161 | # If true, links to the reST sources are added to the pages. 162 | # html_show_sourcelink = True 163 | 164 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 165 | # html_show_sphinx = True 166 | 167 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 168 | # html_show_copyright = True 169 | 170 | # If true, an OpenSearch description file will be output, and all pages will 171 | # contain a tag referring to it. The value of this option must be the 172 | # base URL from which the finished HTML is served. 173 | # html_use_opensearch = '' 174 | 175 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 176 | # html_file_suffix = None 177 | 178 | # Output file base name for HTML help builder. 179 | htmlhelp_basename = "modernizedoc" 180 | 181 | 182 | # -- Options for LaTeX output --------------------------------------------- 183 | 184 | latex_elements = { 185 | # The paper size ('letterpaper' or 'a4paper'). 186 | # 'papersize': 'letterpaper', 187 | # The font size ('10pt', '11pt' or '12pt'). 188 | # 'pointsize': '10pt', 189 | # Additional stuff for the LaTeX preamble. 190 | # 'preamble': '', 191 | } 192 | 193 | # Grouping the document tree into LaTeX files. List of tuples 194 | # (source start file, target name, title, 195 | # author, documentclass [howto, manual, or own class]). 196 | latex_documents = [ 197 | ( 198 | "index", 199 | "modernize.tex", 200 | "modernize Documentation", 201 | "pycqa/modernize team", 202 | "manual", 203 | ) 204 | ] 205 | 206 | # The name of an image file (relative to this directory) to place at the top of 207 | # the title page. 208 | # latex_logo = None 209 | 210 | # For "manual" documents, if this is true, then toplevel headings are parts, 211 | # not chapters. 212 | # latex_use_parts = False 213 | 214 | # If true, show page references after internal links. 215 | # latex_show_pagerefs = False 216 | 217 | # If true, show URL addresses after external links. 218 | # latex_show_urls = False 219 | 220 | # Documents to append as an appendix to all manuals. 221 | # latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | # latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output --------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ("index", "modernize", "modernize Documentation", ["pycqa/modernize team"], 1) 233 | ] 234 | 235 | # If true, show URL addresses after external links. 236 | # man_show_urls = False 237 | 238 | 239 | # -- Options for Texinfo output ------------------------------------------- 240 | 241 | # Grouping the document tree into Texinfo files. List of tuples 242 | # (source start file, target name, title, author, 243 | # dir menu entry, description, category) 244 | texinfo_documents = [ 245 | ( 246 | "index", 247 | "modernize", 248 | "modernize Documentation", 249 | "pycqa/modernize team", 250 | "modernize", 251 | "One line description of project.", 252 | "Miscellaneous", 253 | ) 254 | ] 255 | 256 | # Documents to append as an appendix to all manuals. 257 | # texinfo_appendices = [] 258 | 259 | # If false, no module index is generated. 260 | # texinfo_domain_indices = True 261 | 262 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 263 | # texinfo_show_urls = 'footnote' 264 | 265 | # If true, do not generate a @detailmenu in the "Top" node's menu. 266 | # texinfo_no_detailmenu = False 267 | -------------------------------------------------------------------------------- /docs/fixers.rst: -------------------------------------------------------------------------------- 1 | Fixers 2 | ====== 3 | 4 | Fixers come in two types: Default_ and Opt-in_. Default fixers should not break 5 | code except for corner cases, and are idempotent (applying them more than once 6 | to given source code will make no changes after the first application). Opt-in 7 | fixers are allowed to break these rules. 8 | 9 | Python 2 code from Python 2.6 and older will be upgraded to code that is 10 | compatible with Python 2.6, 2.7, and Python 3. 11 | 12 | If code is using a feature unique to Python 2.7, it will not be downgraded to 13 | work with Python 2.6. For example, ``dict.viewitems()`` usage will not be 14 | removed to make the code compatible with Python 2.6. 15 | 16 | Some fixers rely on the latest release of the `six project`_ to work 17 | (see `Fixers requiring six`_). 18 | If you wish to turn off these fixers to avoid an external dependency on ``six``, 19 | then use the ``--no-six`` flag. 20 | 21 | Fixers use the API defined by fissix. For details of how this works, and how to 22 | implement your own fixers, see `Creating a fixer, at python3porting.com 23 | `_. 24 | ``python -m modernize`` will try to load fixers whose full dotted-path is specified 25 | as a ``-f`` argument, but will fail if they are not found. By default, fixers 26 | will not be found in the current directory; use ``--fixers-here`` to make 27 | ``python -m modernize`` look for them there, or see the `Python tutorial on 28 | modules `_ (in particular, 29 | the parts on the `search path 30 | `_ 31 | and `packages `_) 32 | for more info on how Python finds modules. 33 | 34 | 35 | Default 36 | ------- 37 | 38 | A default fixer will be enabled when: 39 | 40 | - Either no ``-f``/``--fix`` options are used, or ``-f default``/``--fix=default`` 41 | is used, or the fixer is listed explicitly in an ``-f``/``--fix`` option; and 42 | - The fixer is not listed in an ``-x``/``--nofix`` option; and 43 | - For fixers that are dependent on the `six project`_, ``--no-six`` is *not* specified 44 | (see `Fixers requiring six`_). 45 | 46 | The ``-x``/``--nofix`` and ``--no-six`` options always override fixers specified 47 | using ``-f``/``--fix``. The ``--six-unicode`` and ``--future-unicode`` options 48 | also disable fixers that are not applicable for those options. 49 | 50 | 51 | Fixers requiring six 52 | ++++++++++++++++++++ 53 | 54 | The `six project`_ provides the ``six`` module which contains various tidbits in 55 | helping to support Python 2/3 code. All ``six``-related fixers assume the latest 56 | version of ``six`` is installed. 57 | 58 | .. attribute:: basestring 59 | 60 | Replaces all references to :func:`basestring` with :data:`six.string_types`. 61 | 62 | .. versionadded:: 0.4 63 | 64 | .. attribute:: dict_six 65 | 66 | Fixes various methods on the ``dict`` type for getting all keys, values, or 67 | items. E.g.:: 68 | 69 | x.values() 70 | x.itervalues() 71 | x.viewvalues() 72 | 73 | becomes:: 74 | 75 | list(x.values()) 76 | six.itervalues(x) 77 | six.viewvalues(x) 78 | 79 | Care is taken to only call ``list()`` when not in an iterating context 80 | (e.g. not the iterable for a ``for`` loop). 81 | 82 | .. attribute:: filter 83 | 84 | When a call to :func:`filter ` is discovered, ``from six.moves import filter`` is 85 | added to the module. Wrapping the use in a call to ``list()`` is done when 86 | necessary. 87 | 88 | .. attribute:: imports_six 89 | 90 | Uses :mod:`six.moves` to fix various renamed modules, e.g.:: 91 | 92 | import ConfigParser 93 | ConfigParser.ConfigParser() 94 | 95 | becomes:: 96 | 97 | import six.moves.configparser 98 | six.moves.configparser.ConfigParser() 99 | 100 | The modules in Python 2 whose renaming in Python 3 is supported are: 101 | 102 | - ``__builtin__`` 103 | - ``_winreg`` 104 | - ``BaseHTTPServer`` 105 | - ``CGIHTTPServer`` 106 | - ``ConfigParser`` 107 | - ``copy_reg`` 108 | - ``Cookie`` 109 | - ``cookielib`` 110 | - ``cPickle`` 111 | - ``Dialog`` 112 | - ``dummy_thread`` 113 | - ``FileDialog`` 114 | - ``gdbm`` 115 | - ``htmlentitydefs`` 116 | - ``HTMLParser`` 117 | - ``httplib`` 118 | - ``Queue`` 119 | - ``repr`` 120 | - ``robotparser`` 121 | - ``ScrolledText`` 122 | - ``SimpleDialog`` 123 | - ``SimpleHTTPServer`` 124 | - ``SimpleXMLRPCServer`` 125 | - ``SocketServer`` 126 | - ``thread`` 127 | - ``Tix`` 128 | - ``tkColorChooser`` 129 | - ``tkCommonDialog`` 130 | - ``Tkconstants`` 131 | - ``Tkdnd`` 132 | - ``tkFileDialog`` 133 | - ``tkFont`` 134 | - ``Tkinter`` 135 | - ``tkMessageBox`` 136 | - ``tkSimpleDialog`` 137 | - ``ttk`` 138 | - ``xmlrpclib`` 139 | 140 | .. versionadded:: 0.4 141 | 142 | .. attribute:: input_six 143 | 144 | Changes:: 145 | 146 | input(x) 147 | raw_input(x) 148 | 149 | to:: 150 | 151 | from six.moves import input 152 | eval(input(x)) 153 | input(x) 154 | 155 | .. versionadded:: 0.4 156 | 157 | .. attribute:: int_long_tuple 158 | 159 | Changes ``(int, long)`` or ``(long, int)`` to :data:`six.integer_types`. 160 | 161 | .. versionadded:: 0.4 162 | 163 | .. attribute:: map 164 | 165 | If a call to :func:`map ` is discovered, ``from six.moves import map`` is added to 166 | the module. Wrapping the use in a call to ``list()`` is done when necessary. 167 | 168 | .. attribute:: metaclass 169 | 170 | Changes:: 171 | 172 | class Foo: 173 | __metaclass__ = Meta 174 | 175 | to:: 176 | 177 | import six 178 | class Foo(six.with_metaclass(Meta)): 179 | pass 180 | 181 | .. seealso:: 182 | :func:`six.with_metaclass` 183 | 184 | .. attribute:: raise_six 185 | 186 | Changes ``raise E, V, T`` to ``six.reraise(E, V, T)``. 187 | 188 | .. attribute:: unicode_type 189 | 190 | Changes all reference of :func:`unicode ` to 191 | :data:`six.text_type`. 192 | 193 | .. attribute:: urllib_six 194 | 195 | Changes:: 196 | 197 | from urllib import quote_plus 198 | quote_plus('hello world') 199 | 200 | to:: 201 | 202 | from six.moves.urllib.parse import quote_plus 203 | quote_plus('hello world') 204 | 205 | .. attribute:: unichr 206 | 207 | Changes all reference of :func:`unichr ` to 208 | :data:`six.unichr`. 209 | 210 | .. attribute:: xrange_six 211 | 212 | Changes:: 213 | 214 | w = xrange(x) 215 | y = range(z) 216 | 217 | to:: 218 | 219 | from six.moves import range 220 | w = range(x) 221 | y = list(range(z)) 222 | 223 | Care is taken not to call ``list()`` when ``range()`` is used in an iterating 224 | context. 225 | 226 | .. attribute:: zip 227 | 228 | If :func:`zip ` is called, ``from six.moves import zip`` is added to the module. 229 | Wrapping the use in a call to ``list()`` is done when necessary. 230 | 231 | 232 | ``fissix`` fixers 233 | +++++++++++++++++ 234 | 235 | Some :doc:`fixers from fissix ` 236 | in Python's standard library are run by default unmodified as their 237 | transformations are Python 2 compatible. 238 | 239 | - :attr:`apply ` 240 | - :attr:`except ` 241 | - :attr:`exec ` 242 | - :attr:`execfile ` 243 | - :attr:`exitfunc ` 244 | - :attr:`funcattrs ` 245 | - :attr:`has_key ` 246 | - :attr:`idioms ` 247 | - :attr:`long ` 248 | - :attr:`methodattrs ` 249 | - :attr:`ne ` 250 | - :attr:`numliterals ` 251 | - :attr:`operator ` 252 | - :attr:`paren ` 253 | - :attr:`reduce ` 254 | - :attr:`repr ` 255 | - :attr:`set_literal ` 256 | - :attr:`standarderror ` 257 | - :attr:`sys_exc ` 258 | - :attr:`throw ` 259 | - :attr:`tuple_params ` 260 | - :attr:`types ` 261 | - :attr:`ws_comma ` 262 | - :attr:`xreadlines ` 263 | 264 | Fixers with no dependencies 265 | +++++++++++++++++++++++++++ 266 | 267 | .. attribute:: file 268 | 269 | Changes all calls to :func:`file ` to :func:`open `. 270 | 271 | .. versionadded:: 0.4 272 | 273 | .. attribute:: import 274 | 275 | Changes implicit relative imports to explicit relative imports and adds 276 | ``from __future__ import absolute_import``. 277 | 278 | .. versionadded:: 0.4 279 | 280 | .. attribute:: next 281 | 282 | Changes all method calls from ``x.next()`` to ``next(x)``. 283 | 284 | .. attribute:: print 285 | 286 | Changes all usage of the ``print`` statement to use the :func:`print` function 287 | and adds ``from __future__ import print_function``. 288 | 289 | .. attribute:: raise 290 | 291 | Changes comma-based ``raise`` statements from:: 292 | 293 | raise E, V 294 | raise (((E, E1), E2), E3), V 295 | 296 | to:: 297 | 298 | raise E(V) 299 | raise E(V) 300 | 301 | 302 | Opt-in 303 | ------ 304 | 305 | To specify an opt-in fixer while also running all the default fixers, make sure 306 | to specify the ``-f default`` or ``--fix=default`` option, e.g.:: 307 | 308 | python -m modernize -f default -f modernize.fixes.fix_open 309 | 310 | .. attribute:: classic_division 311 | 312 | When a use of the division operator -- ``/`` -- is found, add 313 | ``from __future__ import division`` and change the operator to ``//``. 314 | If ``from __future__ import division`` is already present, this fixer is 315 | skipped. 316 | 317 | This is intended for use in programs where ``/`` is conventionally only used 318 | for integer division, or where it is intended to do a manual pass after running 319 | ``python -m modernize`` to look for cases that should not have been changed to ``//``. 320 | The results of division on non-integers may differ after running this fixer: 321 | for example, ``3.5 / 2 == 1.75``, but ``3.5 // 2 == 1.0``. 322 | 323 | Some objects may override the ``__div__`` method for a use other than division, 324 | and thus would break when changed to use a ``__floordiv__`` method instead. 325 | 326 | This fixer is opt-in because it may change the meaning of code as described 327 | above. 328 | 329 | .. versionadded:: 1.0 330 | 331 | .. attribute:: open 332 | 333 | When a call to :func:`open ` is discovered, add ``from io import open`` at the top 334 | of the module so as to use :func:`io.open` instead. This fixer is opt-in because it 335 | changes what object is returned by a call to ``open()``. 336 | 337 | .. versionadded:: 0.4 338 | 339 | .. _six project: https://six.readthedocs.io/ 340 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 3 2 | 3 | Modernize 4 | ///////// 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | fixers 10 | 11 | 12 | Purpose of the project 13 | ====================== 14 | 15 | Modernize is a Python program that reads Python 2 source code 16 | and applies a series of fixers to transform it into source code 17 | that is valid on both Python 3 and Python 2.7. 18 | 19 | This allows you to run your test suite on Python 2.7 and Python 3 20 | so you can gradually port your code to being fully Python 3 21 | compatible without slowing down development of your Python 2 22 | project. 23 | 24 | The ``python -m modernize`` command works like 25 | ``python -m fissix``, see `fissix `_. 26 | Here's how you'd rewrite a 27 | single file:: 28 | 29 | python -m modernize -w example.py 30 | 31 | It does not guarantee, but it attempts to spit out a codebase compatible 32 | with Python 2.6+ or Python 3. The code that it generates has a runtime 33 | dependency on `six `_, unless the 34 | ``--no-six`` option is used. Version 1.9.0 or later of ``six`` is 35 | recommended. Some of the fixers output code that is not compatible with 36 | Python 2.5 or lower. 37 | 38 | Once your project is ready to run in production on Python 3 it's 39 | recommended to drop Python 2.7 support using 40 | `pyupgrade `_ 41 | 42 | See the ``LICENSE`` file for the license of ``modernize``. 43 | Using this tool does not affect licensing of the modernized code. 44 | 45 | This library is a very thin wrapper around `fissix 46 | `_, a fork of lib2to3. 47 | 48 | The `project website`_ can be found on GitHub and the PyPI project name is 49 | modernize_ 50 | 51 | A note about handling text literals 52 | =================================== 53 | 54 | .. TODO Explain what a "native string" is if it is going to be referenced 55 | 56 | - By default modernize does not change Unicode literals at all, which means that 57 | you can take advantage of 58 | `PEP 414 `_. 59 | This is the simplest option if you only want to support Python 3.3 and above 60 | along with Python 2. 61 | - Alternatively, there is the ``--six-unicode`` flag which will wrap Unicode 62 | literals with the six helper function ``six.u()`` using the 63 | ``modernize.fixes.fix_unicode`` fixer. This is useful if you want 64 | to support Python 3.1 and Python 3.2 without bigger changes. 65 | - The last alternative is the ``--future-unicode`` flag which 66 | imports the ``unicode_literals`` from the ``__future__`` module using the 67 | ``modernize.fixes.fix_unicode_future`` fixer. 68 | This requires Python 2.6 and later, and will require that you 69 | mark bytestrings with ``b''`` and native strings in ``str('')`` 70 | or something similar that survives the transformation. 71 | 72 | Indices and tables 73 | ////////////////// 74 | 75 | * :ref:`genindex` 76 | * :ref:`search` 77 | 78 | 79 | .. _modernize: https://pypi.python.org/pypi/modernize 80 | .. _project website: https://github.com/pycqa/modernize 81 | -------------------------------------------------------------------------------- /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. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.https://pypi.org/project/sphinx/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\modernize.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\modernize.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /modernize/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A hack on top of fissix (lib2to3 fork) for modernizing code for hybrid codebases. 3 | """ 4 | 5 | from __future__ import generator_stop 6 | 7 | __version__ = "0.9rc1.dev0" 8 | -------------------------------------------------------------------------------- /modernize/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | import sys 4 | 5 | from .main import main 6 | 7 | if __name__ == "__main__": 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /modernize/fixes/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | fissix_fix_names = { 4 | "fissix.fixes.fix_apply", 5 | "fissix.fixes.fix_except", 6 | "fissix.fixes.fix_exec", 7 | "fissix.fixes.fix_execfile", 8 | "fissix.fixes.fix_exitfunc", 9 | "fissix.fixes.fix_funcattrs", 10 | "fissix.fixes.fix_has_key", 11 | "fissix.fixes.fix_idioms", 12 | "fissix.fixes.fix_long", 13 | "fissix.fixes.fix_methodattrs", 14 | "fissix.fixes.fix_ne", 15 | "fissix.fixes.fix_numliterals", 16 | "fissix.fixes.fix_operator", 17 | "fissix.fixes.fix_paren", 18 | "fissix.fixes.fix_reduce", 19 | "fissix.fixes.fix_renames", 20 | "fissix.fixes.fix_repr", 21 | "fissix.fixes.fix_set_literal", 22 | "fissix.fixes.fix_standarderror", 23 | "fissix.fixes.fix_sys_exc", 24 | "fissix.fixes.fix_throw", 25 | "fissix.fixes.fix_tuple_params", 26 | "fissix.fixes.fix_types", 27 | "fissix.fixes.fix_ws_comma", 28 | "fissix.fixes.fix_xreadlines", 29 | } 30 | 31 | # fixes that involve using six 32 | six_fix_names = { 33 | "modernize.fixes.fix_basestring", 34 | "modernize.fixes.fix_dict_six", 35 | "modernize.fixes.fix_filter", 36 | "modernize.fixes.fix_imports_six", 37 | "modernize.fixes.fix_itertools_six", 38 | "modernize.fixes.fix_itertools_imports_six", 39 | "modernize.fixes.fix_input_six", 40 | "modernize.fixes.fix_int_long_tuple", 41 | "modernize.fixes.fix_map", 42 | "modernize.fixes.fix_metaclass", 43 | "modernize.fixes.fix_raise_six", 44 | "modernize.fixes.fix_unicode", 45 | "modernize.fixes.fix_unicode_type", 46 | "modernize.fixes.fix_urllib_six", 47 | "modernize.fixes.fix_unichr", 48 | "modernize.fixes.fix_xrange_six", 49 | "modernize.fixes.fix_zip", 50 | } 51 | 52 | # Fixes that are opt-in only. 53 | opt_in_fix_names = { 54 | "modernize.fixes.fix_classic_division", 55 | "modernize.fixes.fix_open", 56 | } 57 | -------------------------------------------------------------------------------- /modernize/fixes/fix_basestring.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import fixer_base, fixer_util 4 | 5 | 6 | class FixBasestring(fixer_base.BaseFix): 7 | BM_compatible = True 8 | PATTERN = """'basestring'""" 9 | 10 | def transform(self, node, results): 11 | fixer_util.touch_import(None, "six", node) 12 | return fixer_util.Name("six.string_types", prefix=node.prefix) 13 | -------------------------------------------------------------------------------- /modernize/fixes/fix_classic_division.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import fixer_base, pytree 4 | from fissix.pgen2 import token 5 | 6 | from .. import utils 7 | 8 | 9 | class FixClassicDivision(fixer_base.BaseFix): 10 | PATTERN = """ 11 | '/=' | '/' 12 | """ 13 | 14 | def start_tree(self, tree, name): 15 | super().start_tree(tree, name) 16 | self.skip = "division" in tree.future_features 17 | 18 | def match(self, node): 19 | return node.value in ("/", "/=") 20 | 21 | def transform(self, node, results): 22 | if self.skip: 23 | return 24 | utils.add_future(node, "division") 25 | 26 | if node.value == "/": 27 | return pytree.Leaf(token.DOUBLESLASH, "//", prefix=node.prefix) 28 | else: 29 | return pytree.Leaf(token.DOUBLESLASHEQUAL, "//=", prefix=node.prefix) 30 | -------------------------------------------------------------------------------- /modernize/fixes/fix_dict_six.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fixer for iterkeys() -> six.iterkeys(), and similarly for iteritems and itervalues. 3 | """ 4 | 5 | from __future__ import generator_stop 6 | 7 | # Local imports 8 | from fissix import fixer_util, pytree 9 | from fissix.fixes import fix_dict 10 | 11 | 12 | class FixDictSix(fix_dict.FixDict): 13 | def transform_iter(self, node, results): 14 | """Call six.(iter|view)items() and friends.""" 15 | # Make sure six is imported. 16 | fixer_util.touch_import(None, "six", node) 17 | 18 | # Copy of self.transform() from fissix.fix_dict with some changes to 19 | # use the six.* methods. 20 | 21 | head = results["head"] 22 | method = results["method"][0] # Extract node for method name 23 | tail = results["tail"] 24 | syms = self.syms 25 | method_name = method.value 26 | name = fixer_util.Name("six." + method_name, prefix=node.prefix) 27 | assert method_name.startswith(("iter", "view")), repr(method) 28 | assert method_name[4:] in ("keys", "items", "values"), repr(method) 29 | head = [n.clone() for n in head] 30 | tail = [n.clone() for n in tail] 31 | new = pytree.Node(syms.power, head) 32 | new.prefix = "" 33 | new = fixer_util.Call(name, [new]) 34 | if tail: 35 | new = pytree.Node(syms.power, [new] + tail) 36 | new.prefix = node.prefix 37 | return new 38 | 39 | def transform(self, node, results): 40 | method = results["method"][0] 41 | method_name = method.value 42 | if method_name in ("keys", "items", "values"): 43 | return super().transform(node, results) 44 | else: 45 | return self.transform_iter(node, results) 46 | 47 | def in_special_context(self, node, isiter): 48 | # Redefined from parent class to make "for x in d.items()" count as 49 | # in special context; fissix only counts for loops as special context 50 | # for the iter* methods. 51 | return super().in_special_context(node, True) 52 | -------------------------------------------------------------------------------- /modernize/fixes/fix_file.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import fixer_base 4 | from fissix.fixer_util import Name 5 | 6 | 7 | class FixFile(fixer_base.BaseFix): 8 | 9 | BM_compatible = True 10 | order = "pre" 11 | 12 | PATTERN = """ 13 | power< name='file' 14 | trailer< 15 | '(' any ')' 16 | > any* 17 | > 18 | """ 19 | 20 | def transform(self, node, results): 21 | name = results["name"] 22 | name.replace(Name("open", prefix=name.prefix)) 23 | -------------------------------------------------------------------------------- /modernize/fixes/fix_filter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2008 Armin Ronacher. 2 | # Licensed to PSF under a Contributor Agreement. 3 | 4 | from __future__ import generator_stop 5 | 6 | from fissix import fixer_util 7 | from fissix.fixes import fix_filter 8 | 9 | from .. import utils 10 | 11 | 12 | class FixFilter(fix_filter.FixFilter): 13 | 14 | skip_on = "six.moves.filter" 15 | 16 | def transform(self, node, results): 17 | result = super().transform(node, results) 18 | if not utils.is_listcomp(result): 19 | # Keep performance improvement from six.moves.filter in iterator 20 | # contexts on Python 2.7. 21 | fixer_util.touch_import("six.moves", "filter", node) 22 | return result 23 | -------------------------------------------------------------------------------- /modernize/fixes/fix_import.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix.fixer_util import syms 4 | from fissix.fixes import fix_import 5 | 6 | from .. import utils 7 | 8 | 9 | class FixImport(fix_import.FixImport): 10 | 11 | # Make sure this runs before any other fixer to guarantee that any other 12 | # added absolute_import doesn't block this fixer's execution. 13 | run_order = 1 14 | 15 | def transform(self, node, results): 16 | if self.skip: 17 | return 18 | # We're not interested in __future__ imports here 19 | if ( 20 | node.type == syms.import_from 21 | and getattr(results["imp"], "value", None) == "__future__" 22 | ): 23 | return 24 | 25 | # If there are any non-future imports, add absolute_import 26 | utils.add_future(node, "absolute_import") 27 | return super().transform(node, results) 28 | -------------------------------------------------------------------------------- /modernize/fixes/fix_imports_six.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix.fixes import fix_imports 4 | 5 | 6 | class FixImportsSix(fix_imports.FixImports): 7 | 8 | mapping = { 9 | "__builtin__": "six.moves.builtins", 10 | "_winreg": "six.moves.winreg", 11 | "BaseHTTPServer": "six.moves.BaseHTTPServer", 12 | "CGIHTTPServer": "six.moves.CGIHTTPServer", 13 | "ConfigParser": "six.moves.configparser", 14 | "copy_reg": "six.moves.copyreg", 15 | "Cookie": "six.moves.http_cookies", 16 | "cookielib": "six.moves.http_cookiejar", 17 | "cPickle": "six.moves.cPickle", 18 | "Dialog": "six.moves.tkinter_dialog", 19 | "dummy_thread": "six.moves._dummy_thread", 20 | # cStringIO.StringIO() 21 | # email.MIMEBase 22 | # email.MIMEMultipart 23 | # email.MIMENonMultipart 24 | # email.MIMEText 25 | "FileDialog": "six.moves.tkinter_filedialog", 26 | "gdbm": "six.moves.dbm_gnu", 27 | "htmlentitydefs": "six.moves.html_entities", 28 | "HTMLParser": "six.moves.html_parser", 29 | "httplib": "six.moves.http_client", 30 | # intern() 31 | # itertools.ifilter() 32 | # itertools.ifilterfalse() 33 | # itertools.imap() 34 | # itertools.izip() 35 | # itertools.zip_longest() 36 | # pipes.quote 37 | "Queue": "six.moves.queue", 38 | # reduce() 39 | # reload() 40 | "repr": "six.moves.reprlib", 41 | "robotparser": "six.moves.urllib_robotparser", 42 | "ScrolledText": "six.moves.tkinter_scrolledtext", 43 | "SimpleDialog": "six.moves.tkinter_simpledialog", 44 | "SimpleHTTPServer": "six.moves.SimpleHTTPServer", 45 | "SimpleXMLRPCServer": "six.moves.xmlrpc_server", 46 | "SocketServer": "six.moves.socketserver", 47 | "thread": "six.moves._thread", 48 | "Tix": "six.moves.tkinter_tix", 49 | "tkColorChooser": "six.moves.tkinter_colorchooser", 50 | "tkCommonDialog": "six.moves.tkinter_commondialog", 51 | "Tkconstants": "six.moves.tkinter_constants", 52 | "Tkdnd": "six.moves.tkinter_dnd", 53 | "tkFileDialog": "six.moves.tkinter_filedialog", 54 | "tkFont": "six.moves.tkinter_font", 55 | "Tkinter": "six.moves.tkinter", 56 | "tkMessageBox": "six.moves.tkinter_messagebox", 57 | "tkSimpleDialog": "six.moves.tkinter_tksimpledialog", 58 | "ttk": "six.moves.tkinter_ttk", 59 | # urllib 60 | "urlparse": "six.moves.urllib.parse", 61 | # UserDict.UserDict 62 | # UserList.UserList 63 | # UserString.UserString 64 | "xmlrpclib": "six.moves.xmlrpc_client", 65 | } 66 | -------------------------------------------------------------------------------- /modernize/fixes/fix_input_six.py: -------------------------------------------------------------------------------- 1 | # This is a derived work of Lib/lib2to3/fixes/fix_input.py and 2 | # Lib/lib2to3/fixes/fix_raw_input.py. Those files are under the 3 | # copyright of the Python Software Foundation and licensed under the 4 | # Python Software Foundation License 2. 5 | # 6 | # Copyright notice: 7 | # 8 | # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 9 | # 2011, 2012, 2013, 2014 Python Software Foundation. All rights reserved. 10 | 11 | from __future__ import generator_stop 12 | 13 | from fissix import fixer_base, fixer_util 14 | from fissix.fixer_util import Call, Name 15 | 16 | 17 | class FixInputSix(fixer_base.ConditionalFix): 18 | 19 | BM_compatible = True 20 | order = "pre" 21 | skip_on = "six.moves.input" 22 | 23 | PATTERN = """ 24 | power< (name='input' | name='raw_input') 25 | trailer< '(' [any] ')' > any* > 26 | """ 27 | 28 | def transform(self, node, results): 29 | if self.should_skip(node): 30 | return 31 | 32 | fixer_util.touch_import("six.moves", "input", node) 33 | name = results["name"] 34 | if name.value == "raw_input": 35 | name.replace(Name("input", prefix=name.prefix)) 36 | else: 37 | new_node = node.clone() 38 | new_node.prefix = "" 39 | return Call(Name("eval"), [new_node], prefix=node.prefix) 40 | -------------------------------------------------------------------------------- /modernize/fixes/fix_int_long_tuple.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import fixer_base, fixer_util 4 | 5 | 6 | class FixIntLongTuple(fixer_base.BaseFix): 7 | 8 | run_order = 4 # Must run before fix_long. 9 | 10 | PATTERN = """ 11 | pair=atom < '(' testlist_gexp < ( 12 | ('int' ',' 'long') | 13 | ('long' ',' 'int') 14 | ) > ')' > 15 | """ 16 | 17 | def transform(self, node, results): 18 | fixer_util.touch_import(None, "six", node) 19 | pair = results["pair"] 20 | pair.replace(fixer_util.Name("six.integer_types", prefix=pair.prefix)) 21 | -------------------------------------------------------------------------------- /modernize/fixes/fix_itertools_imports_six.py: -------------------------------------------------------------------------------- 1 | """ Fixer for imports of itertools.(imap|ifilter|izip|ifilterfalse) """ 2 | 3 | from __future__ import generator_stop 4 | 5 | # Local imports 6 | from fissix import fixer_base, fixer_util 7 | from fissix.fixer_util import BlankLine, syms, token 8 | 9 | # This is a derived work of Lib/lib2to3/fixes/fix_itertools_imports.py. That file 10 | # is under the copyright of the Python Software Foundation and licensed 11 | # under the Python Software Foundation License 2. 12 | # 13 | # Copyright notice: 14 | # 15 | # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 16 | # 2011, 2012, 2013 Python Software Foundation. All rights reserved. 17 | 18 | 19 | class FixItertoolsImportsSix(fixer_base.BaseFix): 20 | BM_compatible = True 21 | PATTERN = """ 22 | import_from< 'from' 'itertools' 'import' imports=any > 23 | """ % ( 24 | locals() 25 | ) 26 | 27 | def transform(self, node, results): 28 | imports = results["imports"] 29 | if imports.type == syms.import_as_name or not imports.children: 30 | children = [imports] 31 | else: 32 | children = imports.children 33 | for child in children[::2]: 34 | if child.type == token.NAME: 35 | name_node = child 36 | elif child.type == token.STAR: 37 | # Just leave the import as is. 38 | return 39 | else: 40 | assert child.type == syms.import_as_name 41 | name_node = child.children[0] 42 | member_name = name_node.value 43 | if member_name in ( 44 | "imap", 45 | "izip", 46 | "ifilter", 47 | "ifilterfalse", 48 | "izip_longest", 49 | ): 50 | child.value = None 51 | fixer_util.touch_import("six.moves", member_name[1:], node) 52 | child.remove() 53 | 54 | # Make sure the import statement is still sane 55 | children = imports.children[:] or [imports] 56 | remove_comma = True 57 | for child in children: 58 | if remove_comma and child.type == token.COMMA: 59 | child.remove() 60 | else: 61 | remove_comma ^= True 62 | 63 | while children and children[-1].type == token.COMMA: 64 | children.pop().remove() 65 | 66 | # If there are no imports left, just get rid of the entire statement 67 | if ( 68 | not (imports.children or getattr(imports, "value", None)) 69 | or imports.parent is None 70 | ): 71 | p = node.prefix 72 | node = BlankLine() 73 | node.prefix = p 74 | return node 75 | -------------------------------------------------------------------------------- /modernize/fixes/fix_itertools_six.py: -------------------------------------------------------------------------------- 1 | """ Fixer for itertools.(imap|ifilter|izip) --> 2 | (six.moves.map|six.moves.filter|six.moves.zip) and 3 | itertools.ifilterfalse --> six.moves.filterfalse (bugs 2360-2363) 4 | imports from itertools are fixed in fix_itertools_imports_six.py 5 | If itertools is imported as something else (ie: import itertools as it; 6 | it.izip(spam, eggs)) method calls will not get fixed. 7 | """ 8 | # This is a derived work of Lib/lib2to3/fixes/fix_itertools_import.py. That file 9 | # is under the copyright of the Python Software Foundation and licensed 10 | # under the Python Software Foundation License 2. 11 | # 12 | # Copyright notice: 13 | # 14 | # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 15 | # 2011, 2012, 2013 Python Software Foundation. All rights reserved. 16 | from __future__ import generator_stop 17 | 18 | # Local imports 19 | from fissix import fixer_base, fixer_util 20 | from fissix.fixer_util import Name 21 | 22 | 23 | class FixItertoolsSix(fixer_base.BaseFix): 24 | BM_compatible = True 25 | it_funcs = "('imap'|'ifilter'|'izip'|'izip_longest'|'ifilterfalse')" 26 | PATTERN = """ 27 | power< it='itertools' 28 | trailer< 29 | dot='.' func=%(it_funcs)s > trailer< '(' [any] ')' > > 30 | | 31 | power< func=%(it_funcs)s trailer< '(' [any] ')' > > 32 | """ % ( 33 | locals() 34 | ) 35 | 36 | # Needs to be run after fix_(map|zip|filter) 37 | run_order = 6 38 | 39 | def transform(self, node, results): 40 | prefix = None 41 | func = results["func"][0] 42 | if "it" in results and func.value not in ("ifilterfalse", "izip_longest"): 43 | dot, it = (results["dot"], results["it"]) 44 | # Remove the 'itertools' 45 | prefix = it.prefix 46 | it.remove() 47 | # Replace the node which contains ('.', 'function') with the 48 | # function (to be consistant with the second part of the pattern) 49 | dot.remove() 50 | func.parent.replace(func) 51 | fixer_util.touch_import("six.moves", func.value[1:], node) 52 | 53 | prefix = prefix or func.prefix 54 | func.replace(Name(func.value[1:], prefix=prefix)) 55 | -------------------------------------------------------------------------------- /modernize/fixes/fix_map.py: -------------------------------------------------------------------------------- 1 | # Copyright 2008 Armin Ronacher. 2 | # Licensed to PSF under a Contributor Agreement. 3 | 4 | from __future__ import generator_stop 5 | 6 | from fissix import fixer_util 7 | from fissix.fixes import fix_map 8 | 9 | from .. import utils 10 | 11 | 12 | class FixMap(fix_map.FixMap): 13 | 14 | skip_on = "six.moves.map" 15 | 16 | def transform(self, node, results): 17 | result = super().transform(node, results) 18 | if not utils.is_listcomp(result): 19 | # Always use the import even if no change is required so as to have 20 | # improved performance in iterator contexts even on Python 2.7. 21 | fixer_util.touch_import("six.moves", "map", node) 22 | return result 23 | -------------------------------------------------------------------------------- /modernize/fixes/fix_metaclass.py: -------------------------------------------------------------------------------- 1 | """Fixer for __metaclass__ = X -> (six.with_metaclass(X)) methods. 2 | 3 | The various forms of classdef (inherits nothing, inherits once, inherits 4 | many) don't parse the same in the CST, so we look at ALL classes for 5 | a __metaclass__ and if we find one normalize the inherits to all be 6 | an arglist. 7 | 8 | For one-liner classes ('class X: pass') there is no indent/dedent so 9 | we normalize those into having a suite. 10 | 11 | Moving the __metaclass__ into the classdef can also cause the class 12 | body to be empty so there is some special casing for that as well. 13 | 14 | This fixer also tries very hard to keep original indenting and spacing 15 | in all those corner cases. 16 | """ 17 | # This is a derived work of Lib/lib2to3/fixes/fix_metaclass.py. That file 18 | # is under the copyright of the Python Software Foundation and licensed 19 | # under the Python Software Foundation License 2. 20 | # 21 | # Copyright notice: 22 | # 23 | # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 24 | # 2011, 2012, 2013 Python Software Foundation. All rights reserved. 25 | from __future__ import generator_stop 26 | 27 | # Local imports 28 | from fissix import fixer_base, fixer_util 29 | from fissix.fixer_util import Call, Comma, Leaf, Name, Node, syms 30 | from fissix.pygram import token 31 | 32 | # Author: Jack Diederich, Daniel Neuhäuser 33 | 34 | 35 | def has_metaclass(parent): 36 | """we have to check the cls_node without changing it. 37 | There are two possibilities: 38 | 1) clsdef => suite => simple_stmt => expr_stmt => Leaf('__meta') 39 | 2) clsdef => simple_stmt => expr_stmt => Leaf('__meta') 40 | """ 41 | for node in parent.children: 42 | if node.type == syms.suite: 43 | return has_metaclass(node) 44 | elif node.type == syms.simple_stmt and node.children: 45 | expr_node = node.children[0] 46 | if expr_node.type == syms.expr_stmt and expr_node.children: 47 | left_side = expr_node.children[0] 48 | if isinstance(left_side, Leaf) and left_side.value == "__metaclass__": 49 | return True 50 | return False 51 | 52 | 53 | def fixup_parse_tree(cls_node): 54 | """one-line classes don't get a suite in the parse tree so we add 55 | one to normalize the tree 56 | """ 57 | for node in cls_node.children: 58 | if node.type == syms.suite: 59 | # already in the preferred format, do nothing 60 | return 61 | 62 | # !%@#! oneliners have no suite node, we have to fake one up 63 | for i, node in enumerate(cls_node.children): 64 | if node.type == token.COLON: 65 | break 66 | else: 67 | raise ValueError("No class suite and no ':'!") # pragma: no cover 68 | 69 | # move everything into a suite node 70 | suite = Node(syms.suite, []) 71 | while cls_node.children[i + 1 :]: 72 | move_node = cls_node.children[i + 1] 73 | suite.append_child(move_node.clone()) 74 | move_node.remove() 75 | cls_node.append_child(suite) 76 | node = suite 77 | 78 | 79 | def fixup_simple_stmt(parent, i, stmt_node): 80 | """if there is a semi-colon all the parts count as part of the same 81 | simple_stmt. We just want the __metaclass__ part so we move 82 | everything efter the semi-colon into its own simple_stmt node 83 | """ 84 | for semi_ind, node in enumerate(stmt_node.children): 85 | if node.type == token.SEMI: # *sigh* 86 | break 87 | else: 88 | return 89 | 90 | node.remove() # kill the semicolon 91 | new_expr = Node(syms.expr_stmt, []) 92 | new_stmt = Node(syms.simple_stmt, [new_expr]) 93 | while stmt_node.children[semi_ind:]: 94 | move_node = stmt_node.children[semi_ind] 95 | new_expr.append_child(move_node.clone()) 96 | move_node.remove() 97 | parent.insert_child(i, new_stmt) 98 | new_leaf1 = new_stmt.children[0].children[0] 99 | old_leaf1 = stmt_node.children[0].children[0] 100 | new_leaf1.prefix = old_leaf1.prefix 101 | 102 | 103 | def remove_trailing_newline(node): 104 | if node.children and node.children[-1].type == token.NEWLINE: 105 | node.children[-1].remove() 106 | 107 | 108 | def find_metas(cls_node): 109 | # find the suite node (Mmm, sweet nodes) 110 | for node in cls_node.children: 111 | if node.type == syms.suite: 112 | break 113 | else: 114 | raise ValueError("No class suite!") # pragma: no cover 115 | 116 | # look for simple_stmt[ expr_stmt[ Leaf('__metaclass__') ] ] 117 | for i, simple_node in list(enumerate(node.children)): 118 | if simple_node.type == syms.simple_stmt and simple_node.children: 119 | expr_node = simple_node.children[0] 120 | if expr_node.type == syms.expr_stmt and expr_node.children: 121 | # Check if the expr_node is a simple assignment. 122 | left_node = expr_node.children[0] 123 | if isinstance(left_node, Leaf) and left_node.value == "__metaclass__": 124 | # We found an assignment to __metaclass__. 125 | fixup_simple_stmt(node, i, simple_node) 126 | remove_trailing_newline(simple_node) 127 | yield (node, i, simple_node) 128 | 129 | 130 | def fixup_indent(suite): 131 | """If an INDENT is followed by a thing with a prefix then nuke the prefix 132 | Otherwise we get in trouble when removing __metaclass__ at suite start 133 | """ 134 | kids = suite.children[::-1] 135 | # find the first indent 136 | while kids: 137 | node = kids.pop() 138 | if node.type == token.INDENT: 139 | break 140 | 141 | # find the first Leaf 142 | while kids: 143 | node = kids.pop() 144 | if isinstance(node, Leaf) and node.type != token.DEDENT: 145 | if node.prefix: 146 | node.prefix = "" 147 | return 148 | else: 149 | kids.extend(node.children[::-1]) 150 | 151 | 152 | class FixMetaclass(fixer_base.BaseFix): 153 | BM_compatible = True 154 | 155 | PATTERN = """ 156 | classdef 157 | """ 158 | 159 | def transform(self, node, results): 160 | if not has_metaclass(node): 161 | return # pragma: no cover 162 | 163 | fixup_parse_tree(node) 164 | 165 | # find metaclasses, keep the last one 166 | last_metaclass = None 167 | for suite, i, stmt in find_metas(node): 168 | last_metaclass = stmt 169 | stmt.remove() 170 | 171 | text_type = node.children[0].type # always Leaf(nnn, 'class') 172 | 173 | # figure out what kind of classdef we have 174 | if len(node.children) == 7: 175 | # Node(classdef, ['class', 'name', '(', arglist, ')', ':', suite]) 176 | # 0 1 2 3 4 5 6 177 | if node.children[3].type == syms.arglist: 178 | arglist = node.children[3] 179 | # Node(classdef, ['class', 'name', '(', 'Parent', ')', ':', suite]) 180 | else: 181 | parent = node.children[3].clone() 182 | arglist = Node(syms.arglist, [parent]) 183 | node.set_child(3, arglist) 184 | elif len(node.children) == 6: 185 | # Node(classdef, ['class', 'name', '(', ')', ':', suite]) 186 | # 0 1 2 3 4 5 187 | arglist = Node(syms.arglist, []) 188 | node.insert_child(3, arglist) 189 | elif len(node.children) == 4: 190 | # Node(classdef, ['class', 'name', ':', suite]) 191 | # 0 1 2 3 192 | arglist = Node(syms.arglist, []) 193 | node.insert_child(2, Leaf(token.RPAR, ")")) 194 | node.insert_child(2, arglist) 195 | node.insert_child(2, Leaf(token.LPAR, "(")) 196 | else: 197 | raise ValueError("Unexpected class definition") # pragma: no cover 198 | 199 | fixer_util.touch_import(None, "six", node) 200 | 201 | metaclass = last_metaclass.children[0].children[2].clone() 202 | metaclass.prefix = "" 203 | 204 | arguments = [metaclass] 205 | 206 | if arglist.children: 207 | bases = arglist.clone() 208 | bases.prefix = " " 209 | arguments.extend([Comma(), bases]) 210 | 211 | arglist.replace( 212 | Call(Name("six.with_metaclass", prefix=arglist.prefix), arguments) 213 | ) 214 | 215 | fixup_indent(suite) 216 | 217 | # check for empty suite 218 | if not suite.children: 219 | # one-liner that was just __metaclass__ 220 | suite.remove() 221 | pass_leaf = Leaf(text_type, "pass") 222 | pass_leaf.prefix = last_metaclass.prefix 223 | node.append_child(pass_leaf) 224 | node.append_child(Leaf(token.NEWLINE, "\n")) 225 | 226 | elif len(suite.children) > 1 and ( 227 | suite.children[-2].type == token.INDENT 228 | and suite.children[-1].type == token.DEDENT 229 | ): 230 | # there was only one line in the class body and it was __metaclass__ 231 | pass_leaf = Leaf(text_type, "pass") 232 | suite.insert_child(-1, pass_leaf) 233 | suite.insert_child(-1, Leaf(token.NEWLINE, "\n")) 234 | -------------------------------------------------------------------------------- /modernize/fixes/fix_next.py: -------------------------------------------------------------------------------- 1 | """Fixer for it.next() -> next(it)""" 2 | 3 | from __future__ import generator_stop 4 | 5 | # Local imports 6 | from fissix import fixer_base 7 | from fissix.fixer_util import Call, Name 8 | 9 | bind_warning = "Calls to builtin next() possibly shadowed by global binding" 10 | 11 | 12 | class FixNext(fixer_base.BaseFix): 13 | BM_compatible = True 14 | PATTERN = """ 15 | power< base=any+ trailer< '.' attr='next' > trailer< '(' ')' > > 16 | """ 17 | 18 | order = "pre" # Pre-order tree traversal 19 | 20 | def transform(self, node, results): 21 | base = results["base"] 22 | base = [n.clone() for n in base] 23 | base[0].prefix = "" 24 | node.replace(Call(Name("next", prefix=node.prefix), base)) 25 | -------------------------------------------------------------------------------- /modernize/fixes/fix_open.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import fixer_base, fixer_util 4 | 5 | 6 | class FixOpen(fixer_base.BaseFix): 7 | 8 | BM_compatible = True 9 | # Fixers don't directly stack, so make sure the 'file' case is covered. 10 | PATTERN = """ 11 | power< ('open' | 'file') trailer< '(' any+ ')' > > 12 | """ 13 | 14 | def transform(self, node, results): 15 | fixer_util.touch_import("io", "open", node) 16 | -------------------------------------------------------------------------------- /modernize/fixes/fix_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix.fixes import fix_print 4 | 5 | from .. import utils 6 | 7 | 8 | class FixPrint(fix_print.FixPrint): 9 | def transform(self, node, results): 10 | result = super().transform(node, results) 11 | utils.add_future(node, "print_function") 12 | return result 13 | -------------------------------------------------------------------------------- /modernize/fixes/fix_raise.py: -------------------------------------------------------------------------------- 1 | """Fixer for 'raise E, V, T' 2 | 3 | raise -> raise 4 | raise E -> raise E 5 | raise E, V -> raise E(V) 6 | 7 | raise (((E, E'), E''), E'''), V -> raise E(V) 8 | 9 | 10 | CAVEATS: 11 | 1) "raise E, V" will be incorrectly translated if V is an exception 12 | instance. The correct Python 3 idiom is 13 | 14 | raise E from V 15 | 16 | but since we can't detect instance-hood by syntax alone and since 17 | any client code would have to be changed as well, we don't automate 18 | this. 19 | """ 20 | # Author: Collin Winter, Armin Ronacher 21 | from __future__ import generator_stop 22 | 23 | from fissix.fixes import fix_raise 24 | 25 | 26 | class FixRaise(fix_raise.FixRaise): 27 | # We don't want to match 3-argument raise, with a traceback; 28 | # that is handled separately by fix_raise_six 29 | PATTERN = """ 30 | raise_stmt< 'raise' exc=any [',' val=any] > 31 | """ 32 | -------------------------------------------------------------------------------- /modernize/fixes/fix_raise_six.py: -------------------------------------------------------------------------------- 1 | """Fixer for 'raise E, V, T' 2 | 3 | raise E, V, T -> six.reraise(E, V, T) 4 | 5 | """ 6 | # Author : Markus Unterwaditzer 7 | from __future__ import generator_stop 8 | 9 | # Local imports 10 | from fissix import fixer_base, fixer_util 11 | from fissix.fixer_util import Call, Comma, Name 12 | 13 | 14 | class FixRaiseSix(fixer_base.BaseFix): 15 | 16 | BM_compatible = True 17 | PATTERN = """ 18 | raise_stmt< 'raise' exc=any ',' val=any ',' tb=any > 19 | """ 20 | 21 | def transform(self, node, results): 22 | exc = results["exc"].clone() 23 | val = results["val"].clone() 24 | tb = results["tb"].clone() 25 | 26 | exc.prefix = "" 27 | val.prefix = tb.prefix = " " 28 | 29 | fixer_util.touch_import(None, "six", node) 30 | return Call( 31 | Name("six.reraise"), [exc, Comma(), val, Comma(), tb], prefix=node.prefix 32 | ) 33 | -------------------------------------------------------------------------------- /modernize/fixes/fix_unichr.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import fixer_base, fixer_util 4 | from fissix.fixer_util import is_probably_builtin 5 | 6 | 7 | class FixUnichr(fixer_base.ConditionalFix): 8 | BM_compatible = True 9 | 10 | skip_on = "six.moves.unichr" 11 | PATTERN = """'unichr'""" 12 | 13 | def transform(self, node, results): 14 | if self.should_skip(node): 15 | return 16 | if is_probably_builtin(node): 17 | fixer_util.touch_import("six", "unichr", node) 18 | -------------------------------------------------------------------------------- /modernize/fixes/fix_unicode.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | import re 4 | 5 | from fissix import fixer_base, fixer_util 6 | from fissix.fixer_util import Call, Name 7 | 8 | _mapping = {"unichr": "chr", "unicode": "str"} 9 | _literal_re = re.compile("[uU][rR]?[\\'\\\"]") 10 | 11 | 12 | class FixUnicode(fixer_base.BaseFix): 13 | BM_compatible = True 14 | PATTERN = """STRING""" 15 | 16 | def transform(self, node, results): 17 | if _literal_re.match(node.value): 18 | fixer_util.touch_import(None, "six", node) 19 | new = node.clone() 20 | new.value = new.value[1:] 21 | new.prefix = "" 22 | node.replace(Call(Name("six.u", prefix=node.prefix), [new])) 23 | -------------------------------------------------------------------------------- /modernize/fixes/fix_unicode_future.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix.fixes import fix_unicode 4 | 5 | from .. import utils 6 | 7 | 8 | class FixUnicodeFuture(fix_unicode.FixUnicode): 9 | def transform(self, node, results): 10 | res = super().transform(node, results) 11 | if res: 12 | utils.add_future(node, "unicode_literals") 13 | return res 14 | -------------------------------------------------------------------------------- /modernize/fixes/fix_unicode_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import fixer_base, fixer_util 4 | 5 | 6 | class FixUnicodeType(fixer_base.BaseFix): 7 | BM_compatible = True 8 | PATTERN = """'unicode'""" 9 | 10 | def transform(self, node, results): 11 | fixer_util.touch_import(None, "six", node) 12 | return fixer_util.Name("six.text_type", prefix=node.prefix) 13 | -------------------------------------------------------------------------------- /modernize/fixes/fix_urllib_six.py: -------------------------------------------------------------------------------- 1 | """Fix changes imports of urllib which are now incompatible. 2 | This is a copy of Lib/lib2to3/fixes/fix_urllib.py, but modified to point to the 3 | six.moves locations for new libraries instead of the Python 3 locations. 4 | """ 5 | # This is a derived work of Lib/lib2to3/fixes/fix_urllib.py. That file 6 | # is under the copyright of the Python Software Foundation and licensed 7 | # under the Python Software Foundation License 2. 8 | # 9 | # Copyright notice: 10 | # 11 | # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 12 | # 2011, 2012, 2013 Python Software Foundation. All rights reserved. 13 | from __future__ import generator_stop 14 | 15 | from fissix.fixer_util import ( 16 | Comma, 17 | FromImport, 18 | Name, 19 | Newline, 20 | Node, 21 | find_indentation, 22 | syms, 23 | ) 24 | 25 | # Local imports 26 | from fissix.fixes.fix_imports import FixImports, alternates 27 | 28 | # Author: Nick Edds 29 | 30 | 31 | MAPPING = { 32 | "urllib": [ 33 | ( 34 | "six.moves.urllib.request", 35 | [ 36 | "URLopener", 37 | "FancyURLopener", 38 | "urlretrieve", 39 | "_urlopener", 40 | "urlopen", 41 | "urlcleanup", 42 | "pathname2url", 43 | "url2pathname", 44 | ], 45 | ), 46 | ( 47 | "six.moves.urllib.parse", 48 | [ 49 | "quote", 50 | "quote_plus", 51 | "unquote", 52 | "unquote_plus", 53 | "urlencode", 54 | "splitattr", 55 | "splithost", 56 | "splitnport", 57 | "splitpasswd", 58 | "splitport", 59 | "splitquery", 60 | "splittag", 61 | "splittype", 62 | "splituser", 63 | "splitvalue", 64 | ], 65 | ), 66 | ("six.moves.urllib.error", ["ContentTooShortError"]), 67 | ], 68 | "urllib2": [ 69 | ( 70 | "six.moves.urllib.request", 71 | [ 72 | "urlopen", 73 | "install_opener", 74 | "build_opener", 75 | "Request", 76 | "OpenerDirector", 77 | "BaseHandler", 78 | "HTTPDefaultErrorHandler", 79 | "HTTPRedirectHandler", 80 | "HTTPCookieProcessor", 81 | "ProxyHandler", 82 | "HTTPPasswordMgr", 83 | "HTTPPasswordMgrWithDefaultRealm", 84 | "AbstractBasicAuthHandler", 85 | "HTTPBasicAuthHandler", 86 | "ProxyBasicAuthHandler", 87 | "AbstractDigestAuthHandler", 88 | "HTTPDigestAuthHandler", 89 | "ProxyDigestAuthHandler", 90 | "HTTPHandler", 91 | "HTTPSHandler", 92 | "FileHandler", 93 | "FTPHandler", 94 | "CacheFTPHandler", 95 | "UnknownHandler", 96 | ], 97 | ), 98 | ("six.moves.urllib.error", ["URLError", "HTTPError"]), 99 | ], 100 | } 101 | 102 | # Duplicate the url parsing functions for urllib2. 103 | MAPPING["urllib2"].append(MAPPING["urllib"][1]) 104 | 105 | 106 | def build_pattern(): 107 | for old_module, changes in MAPPING.items(): 108 | for change in changes: 109 | new_module, members = change 110 | members = alternates(members) 111 | yield """import_name< 'import' (module={!r} 112 | | dotted_as_names< any* module={!r} any* >) > 113 | """.format( 114 | old_module, old_module 115 | ) 116 | yield """import_from< 'from' mod_member={!r} 'import' 117 | ( member={} | import_as_name< member={} 'as' any > | 118 | import_as_names< members=any* >) > 119 | """.format( 120 | old_module, members, members 121 | ) 122 | yield """import_from< 'from' module_star=%r 'import' star='*' > 123 | """ % old_module 124 | yield """import_name< 'import' 125 | dotted_as_name< module_as=%r 'as' any > > 126 | """ % old_module 127 | # bare_with_attr has a special significance for FixImports.match(). 128 | yield """power< bare_with_attr={!r} trailer< '.' member={} > any* > 129 | """.format( 130 | old_module, members 131 | ) 132 | 133 | 134 | class FixUrllibSix(FixImports): 135 | def build_pattern(self): 136 | return "|".join(build_pattern()) 137 | 138 | def transform_import(self, node, results): 139 | """Transform for the basic import case. Replaces the old 140 | import name with a comma separated list of its 141 | replacements. 142 | """ 143 | import_mod = results.get("module") 144 | pref = import_mod.prefix 145 | 146 | names = [] 147 | 148 | # create a Node list of the replacement modules 149 | for name in MAPPING[import_mod.value][:-1]: 150 | names.extend([Name(name[0], prefix=pref), Comma()]) 151 | names.append(Name(MAPPING[import_mod.value][-1][0], prefix=pref)) 152 | import_mod.replace(names) 153 | 154 | def transform_member(self, node, results): 155 | """Transform for imports of specific module elements. Replaces 156 | the module to be imported from with the appropriate new 157 | module. 158 | """ 159 | mod_member = results.get("mod_member") 160 | pref = mod_member.prefix 161 | member = results.get("member") 162 | 163 | # Simple case with only a single member being imported 164 | if member: 165 | # this may be a list of length one, or just a node 166 | if isinstance(member, list): 167 | member = member[0] 168 | new_name = None 169 | for change in MAPPING[mod_member.value]: 170 | if member.value in change[1]: 171 | new_name = change[0] 172 | break 173 | if new_name: 174 | mod_member.replace(Name(new_name, prefix=pref)) 175 | else: 176 | self.cannot_convert(node, "This is an invalid module element") 177 | 178 | # Multiple members being imported 179 | else: 180 | # a dictionary for replacements, order matters 181 | modules = [] 182 | mod_dict = {} 183 | members = results["members"] 184 | for member in members: 185 | # we only care about the actual members 186 | if member.type == syms.import_as_name: 187 | member_name = member.children[0].value 188 | else: 189 | member_name = member.value 190 | if member_name != ",": 191 | for change in MAPPING[mod_member.value]: 192 | if member_name in change[1]: 193 | if change[0] not in mod_dict: 194 | modules.append(change[0]) 195 | mod_dict.setdefault(change[0], []).append(member) 196 | 197 | new_nodes = [] 198 | indentation = find_indentation(node) 199 | first = True 200 | 201 | def handle_name(name, prefix): 202 | if name.type == syms.import_as_name: 203 | kids = [ 204 | Name(name.children[0].value, prefix=prefix), 205 | name.children[1].clone(), 206 | name.children[2].clone(), 207 | ] 208 | return [Node(syms.import_as_name, kids)] 209 | return [Name(name.value, prefix=prefix)] 210 | 211 | for module in modules: 212 | elts = mod_dict[module] 213 | names = [] 214 | for elt in elts[:-1]: 215 | names.extend(handle_name(elt, pref)) 216 | names.append(Comma()) 217 | names.extend(handle_name(elts[-1], pref)) 218 | new = FromImport(module, names) 219 | if not first or node.parent.prefix.endswith(indentation): 220 | new.prefix = indentation 221 | new_nodes.append(new) 222 | first = False 223 | if new_nodes: 224 | nodes = [] 225 | for new_node in new_nodes[:-1]: 226 | nodes.extend([new_node, Newline()]) 227 | nodes.append(new_nodes[-1]) 228 | node.replace(nodes) 229 | else: 230 | self.cannot_convert(node, "All module elements are invalid") 231 | 232 | def transform_dot(self, node, results): 233 | """Transform for calls to module members in code.""" 234 | module_dot = results.get("bare_with_attr") 235 | member = results.get("member") 236 | new_name = None 237 | if isinstance(member, list): 238 | member = member[0] 239 | for change in MAPPING[module_dot.value]: 240 | if member.value in change[1]: 241 | new_name = change[0] 242 | break 243 | if new_name: 244 | module_dot.replace(Name(new_name, prefix=module_dot.prefix)) 245 | else: 246 | self.cannot_convert(node, "This is an invalid module element") 247 | 248 | def transform(self, node, results): 249 | if results.get("module"): 250 | self.transform_import(node, results) 251 | elif results.get("mod_member"): 252 | self.transform_member(node, results) 253 | elif results.get("bare_with_attr"): 254 | self.transform_dot(node, results) 255 | # Renaming and star imports are not supported for these modules. 256 | elif results.get("module_star"): 257 | self.cannot_convert(node, "Cannot handle star imports.") 258 | elif results.get("module_as"): 259 | self.cannot_convert(node, "This module is now multiple modules") 260 | -------------------------------------------------------------------------------- /modernize/fixes/fix_xrange_six.py: -------------------------------------------------------------------------------- 1 | # Copyright 2008 Armin Ronacher. 2 | # Licensed to PSF under a Contributor Agreement. 3 | 4 | from __future__ import generator_stop 5 | 6 | from fissix import fixer_base, fixer_util 7 | from fissix.fixes import fix_xrange 8 | 9 | 10 | class FixXrangeSix(fixer_base.ConditionalFix, fix_xrange.FixXrange): 11 | 12 | skip_on = "six.moves.range" 13 | 14 | def transform(self, node, results): 15 | if self.should_skip(node): 16 | return 17 | fixer_util.touch_import("six.moves", "range", node) 18 | return super().transform(node, results) 19 | -------------------------------------------------------------------------------- /modernize/fixes/fix_zip.py: -------------------------------------------------------------------------------- 1 | # Copyright 2008 Armin Ronacher. 2 | # Licensed to PSF under a Contributor Agreement. 3 | 4 | from __future__ import generator_stop 5 | 6 | from fissix import fixer_util 7 | from fissix.fixes import fix_zip 8 | 9 | 10 | class FixZip(fix_zip.FixZip): 11 | 12 | skip_on = "six.moves.zip" 13 | 14 | def transform(self, node, results): 15 | result = super().transform(node, results) 16 | # Always use six.moves.zip so that even Python 2.7 gets performance 17 | # boost from using itertools in iterator contexts. 18 | fixer_util.touch_import("six.moves", "zip", node) 19 | return result 20 | -------------------------------------------------------------------------------- /modernize/main.py: -------------------------------------------------------------------------------- 1 | """\ 2 | Python _ _ 3 | _ __ ___ __| |___ _ _ _ _ (_)______ 4 | | ' \\/ _ \\/ _` / -_) '_| ' \\| |_ / -_) 5 | |_|_|_\\___/\\__,_\\___|_| |_||_|_/__\\___|\ 6 | """ 7 | 8 | from __future__ import generator_stop 9 | 10 | import logging 11 | import optparse 12 | import os 13 | import sys 14 | 15 | from fissix import refactor 16 | from fissix.main import StdoutRefactoringTool, warn 17 | 18 | from . import __version__ 19 | from .fixes import fissix_fix_names, opt_in_fix_names, six_fix_names 20 | 21 | usage = ( 22 | __doc__ 23 | + """\ 24 | %s 25 | 26 | Usage: modernize [options] file|dir ... 27 | """ 28 | % __version__ 29 | ) 30 | 31 | 32 | def format_usage(usage): 33 | """Method that doesn't output "Usage:" prefix""" 34 | return usage 35 | 36 | 37 | def main(args=None): 38 | """Main program. 39 | 40 | Returns a suggested exit status (0, 1, 2). 41 | """ 42 | # Set up option parser 43 | parser = optparse.OptionParser(usage=usage, version="modernize %s" % __version__) 44 | parser.formatter.format_usage = format_usage 45 | parser.add_option( 46 | "-v", "--verbose", action="store_true", help="Show more verbose logging." 47 | ) 48 | parser.add_option( 49 | "--no-diffs", action="store_true", help="Don't show diffs of the refactoring." 50 | ) 51 | parser.add_option( 52 | "-l", "--list-fixes", action="store_true", help="List standard transformations." 53 | ) 54 | parser.add_option( 55 | "-d", "--doctests_only", action="store_true", help="Fix up doctests only." 56 | ) 57 | parser.add_option( 58 | "-f", 59 | "--fix", 60 | action="append", 61 | default=[], 62 | help="Each FIX specifies a transformation; " 63 | "'-f default' includes default fixers.", 64 | ) 65 | parser.add_option( 66 | "--fixers-here", 67 | action="store_true", 68 | help="Add current working directory to python path (so fixers can be found)", 69 | ) 70 | parser.add_option( 71 | "-j", 72 | "--processes", 73 | action="store", 74 | default=1, 75 | type="int", 76 | help="Run fissix concurrently.", 77 | ) 78 | parser.add_option( 79 | "-x", 80 | "--nofix", 81 | action="append", 82 | default=[], 83 | help="Prevent a fixer from being run.", 84 | ) 85 | parser.add_option( 86 | "-p", 87 | "--print-function", 88 | action="store_true", 89 | help="Modify the grammar so that print() is a function.", 90 | ) 91 | parser.add_option( 92 | "-w", "--write", action="store_true", help="Write back modified files." 93 | ) 94 | parser.add_option( 95 | "-n", 96 | "--nobackups", 97 | action="store_true", 98 | default=False, 99 | help="Don't write backups for modified files.", 100 | ) 101 | parser.add_option( 102 | "--six-unicode", 103 | action="store_true", 104 | default=False, 105 | help="Wrap unicode literals in six.u().", 106 | ) 107 | parser.add_option( 108 | "--future-unicode", 109 | action="store_true", 110 | default=False, 111 | help="Use 'from __future__ import unicode_literals'" 112 | "(only useful for Python 2.6+).", 113 | ) 114 | parser.add_option( 115 | "--no-six", 116 | action="store_true", 117 | default=False, 118 | help="Exclude fixes that depend on the six package.", 119 | ) 120 | parser.add_option( 121 | "--enforce", 122 | action="store_true", 123 | default=False, 124 | help="Returns non-zero exit code if any fixers had to be applied. " 125 | "Useful for enforcing Python 3 compatibility.", 126 | ) 127 | 128 | fixer_pkg = "modernize.fixes" 129 | avail_fixes = set(refactor.get_fixers_from_package(fixer_pkg)) 130 | avail_fixes.update(fissix_fix_names) 131 | 132 | # Parse command line arguments 133 | refactor_stdin = False 134 | flags = {} 135 | options, args = parser.parse_args(args) 136 | if not options.write and options.no_diffs: 137 | warn("Not writing files and not printing diffs; that's not very useful.") 138 | if not options.write and options.nobackups: 139 | parser.error("Can't use '-n' without '-w'.") 140 | if options.list_fixes: 141 | print( 142 | "Standard transformations available for the " 143 | "-f/--fix and -x/--nofix options:" 144 | ) 145 | for fixname in sorted(avail_fixes): 146 | print(" {} ({})".format(fixname, fixname.split(".fix_", 1)[1])) 147 | print() 148 | if not args: 149 | return 0 150 | if not args: 151 | print("At least one file or directory argument required.", file=sys.stderr) 152 | print("Use --help to show usage.", file=sys.stderr) 153 | return 2 154 | if "-" in args: 155 | refactor_stdin = True 156 | if options.write: 157 | print("Can't write to stdin.", file=sys.stderr) 158 | return 2 159 | if options.print_function: 160 | flags["print_function"] = True 161 | if options.fixers_here: 162 | sys.path.append(os.getcwd()) 163 | 164 | # Set up logging handler 165 | level = logging.DEBUG if options.verbose else logging.INFO 166 | logging.basicConfig(format="%(name)s: %(message)s", level=level) 167 | 168 | # Initialize the refactoring tool 169 | unwanted_fixes = set() 170 | splitfixes = [] 171 | for fix in options.nofix: 172 | splitfixes.extend(fix.split(",")) 173 | for fix in splitfixes: 174 | matched = None 175 | for tgt in avail_fixes: 176 | if tgt == fix or tgt.endswith(f".fix_{fix}"): 177 | matched = tgt 178 | unwanted_fixes.add(matched) 179 | break 180 | else: 181 | print(f"Error: fix '{fix}' was not found", file=sys.stderr) 182 | return 2 183 | 184 | default_fixes = avail_fixes.difference(opt_in_fix_names) 185 | 186 | # Remove unicode fixers depending on command line options 187 | if options.six_unicode: 188 | unwanted_fixes.add("modernize.fixes.fix_unicode_future") 189 | elif options.future_unicode: 190 | unwanted_fixes.add("modernize.fixes.fix_unicode") 191 | else: 192 | unwanted_fixes.add("modernize.fixes.fix_unicode") 193 | unwanted_fixes.add("modernize.fixes.fix_unicode_future") 194 | 195 | if options.no_six: 196 | unwanted_fixes.update(six_fix_names) 197 | 198 | explicit = set() 199 | if options.fix: 200 | default_present = False 201 | splitfixes = [] 202 | for fix in options.fix: 203 | splitfixes.extend(fix.split(",")) 204 | for fix in splitfixes: 205 | if fix == "default": 206 | default_present = True 207 | else: 208 | matched = None 209 | for tgt in avail_fixes: 210 | if tgt == fix or tgt.endswith(f".fix_{fix}"): 211 | matched = tgt 212 | explicit.add(matched) 213 | break 214 | else: 215 | # A non-standard fix -- trust user to have supplied path 216 | explicit.add(fix) 217 | requested = default_fixes.union(explicit) if default_present else explicit 218 | else: 219 | requested = default_fixes 220 | fixer_names = requested.difference(unwanted_fixes) # Filter out unwanted fixers 221 | explicit = explicit.intersection( 222 | fixer_names 223 | ) # Filter `explicit` fixers vs remaining fixers 224 | 225 | print(" Loading the following fixers:", file=sys.stderr) 226 | if fixer_names: 227 | for fixname in sorted(fixer_names): 228 | print( 229 | " {} ({})".format(fixname, fixname.split(".fix_", 1)[1]), 230 | file=sys.stderr, 231 | ) 232 | else: 233 | print(" (None)", file=sys.stderr) 234 | print(" Applying the following explicit transformations:", file=sys.stderr) 235 | if explicit: 236 | for fixname in sorted(explicit): 237 | print( 238 | " {} ({})".format(fixname, fixname.split(".fix_", 1)[1]), 239 | file=sys.stderr, 240 | ) 241 | else: 242 | print(" (None)", file=sys.stderr) 243 | print(file=sys.stderr) 244 | 245 | # Refactor all files and directories passed as arguments 246 | rt = StdoutRefactoringTool( 247 | sorted(fixer_names), 248 | flags, 249 | sorted(explicit), 250 | options.nobackups, 251 | not options.no_diffs, 252 | ) 253 | if not rt.errors: 254 | if refactor_stdin: 255 | rt.refactor_stdin() 256 | else: 257 | try: 258 | rt.refactor( 259 | args, options.write, options.doctests_only, options.processes 260 | ) 261 | except refactor.MultiprocessingUnsupported: # pragma: no cover 262 | assert options.processes > 1 263 | print("Sorry, -j isn't supported on this platform.", file=sys.stderr) 264 | return 1 265 | rt.summarize() 266 | 267 | # Return error status (0 if rt.errors is zero) 268 | return_code = int(bool(rt.errors)) 269 | # If we are enforcing python 3 compatibility, return a non-zero exit code 270 | # if we had to modify any files. 271 | if options.enforce and rt.files: 272 | return_code |= 2 273 | return return_code 274 | -------------------------------------------------------------------------------- /modernize/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import fixer_util 4 | from fissix.pgen2 import token 5 | from fissix.pygram import python_symbols as syms 6 | from fissix.pytree import Leaf, Node 7 | 8 | 9 | def _check_future_import(node): 10 | """If this is a future import, return set of symbols that are imported, 11 | else return None.""" 12 | # node should be the import statement here 13 | if not (node.type == syms.simple_stmt and node.children): 14 | return set() 15 | node = node.children[0] 16 | # now node is the import_from node 17 | if not ( 18 | node.type == syms.import_from 19 | and node.children[1].type == token.NAME 20 | and node.children[1].value == "__future__" 21 | ): 22 | return set() 23 | 24 | if node.children[3].type == token.LPAR: 25 | # from __future__ import (.. 26 | node = node.children[4] 27 | else: 28 | # from __future__ import ... 29 | node = node.children[3] 30 | # now node is the import_as_name[s] 31 | 32 | # print(python_grammar.number2symbol[node.type]) 33 | if node.type == syms.import_as_names: 34 | result = set() 35 | for n in node.children: 36 | if n.type == token.NAME: 37 | result.add(n.value) 38 | elif n.type == syms.import_as_name: 39 | n = n.children[0] 40 | assert n.type == token.NAME 41 | result.add(n.value) 42 | return result 43 | elif node.type == syms.import_as_name: 44 | node = node.children[0] 45 | assert node.type == token.NAME 46 | return {node.value} 47 | elif node.type == token.NAME: 48 | return {node.value} 49 | else: # pragma: no cover 50 | assert 0, "strange import" 51 | 52 | 53 | def add_future(node, symbol): 54 | 55 | root = fixer_util.find_root(node) 56 | 57 | for idx, node in enumerate(root.children): 58 | if ( 59 | node.type == syms.simple_stmt 60 | and len(node.children) > 0 61 | and node.children[0].type == token.STRING 62 | ): 63 | # skip over docstring 64 | continue 65 | names = _check_future_import(node) 66 | if not names: 67 | # not a future statement; need to insert before this 68 | break 69 | if symbol in names: 70 | # already imported 71 | return 72 | 73 | import_ = fixer_util.FromImport( 74 | "__future__", [Leaf(token.NAME, symbol, prefix=" ")] 75 | ) 76 | 77 | # Place after any comments or whitespace. (copyright, shebang etc.) 78 | import_.prefix = node.prefix 79 | node.prefix = "" 80 | 81 | children = [import_, fixer_util.Newline()] 82 | root.insert_child(idx, Node(syms.simple_stmt, children)) 83 | 84 | 85 | def is_listcomp(node): 86 | def _is_listcomp(node): 87 | return ( 88 | isinstance(node, Node) 89 | and node.type == syms.atom 90 | and len(node.children) >= 2 91 | and isinstance(node.children[0], Leaf) 92 | and node.children[0].value == "[" 93 | and isinstance(node.children[-1], Leaf) 94 | and node.children[-1].value == "]" 95 | ) 96 | 97 | def _is_noop_power_node(node): 98 | """https://github.com/python/cpython/pull/2235 changed the node 99 | structure for fix_map / fix_filter to contain a top-level `power` node 100 | """ 101 | return ( 102 | isinstance(node, Node) 103 | and node.type == syms.power 104 | and len(node.children) == 1 105 | ) 106 | 107 | return ( 108 | _is_listcomp(node) 109 | or _is_noop_power_node(node) 110 | and _is_listcomp(node.children[0]) 111 | ) 112 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "modernize" 7 | author = "Armin Ronacher" 8 | author-email = "armin.ronacher@active-4.com" 9 | maintainer = "PyCQA" 10 | maintainer-email = "code-quality@python.org" 11 | home-page = "https://github.com/PyCQA/modernize" 12 | classifiers = [ 13 | "License :: OSI Approved :: BSD License", 14 | "Programming Language :: Python", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.6", 17 | "Programming Language :: Python :: 3.7", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | ] 21 | description-file = "README.rst" 22 | requires = ["fissix"] 23 | requires-python = "~=3.6" 24 | 25 | [tool.flit.metadata.requires-extra] 26 | docs = [ 27 | "alabaster~=0.7.12", 28 | "commonmark~=0.9.1", 29 | "docutils~=0.16.0", 30 | "mock~=4.0", 31 | "pillow~=7.2", 32 | "readthedocs-sphinx-ext~=2.1", 33 | "recommonmark~=0.6.0", 34 | "sphinx~=3.2", 35 | "sphinx-rtd-theme~=0.5.0", 36 | ] 37 | test = ["pytest", "pytest-cov", "coverage>=5.3"] 38 | 39 | [tool.flit.scripts] 40 | modernize = "modernize.main:main" 41 | python-modernize = "modernize.main:main" 42 | 43 | [tool.isort] 44 | profile = "black" 45 | add_imports=["from __future__ import generator_stop"] 46 | 47 | [tool.pytest.ini_options] 48 | addopts = [ 49 | "--strict-config", 50 | "--strict-markers", 51 | "--cov", 52 | "--cov-fail-under=92.11", 53 | "--cov-report=term-missing:skip-covered", 54 | ] 55 | xfail_strict = true 56 | junit_family = "xunit2" 57 | filterwarnings = ["error"] 58 | 59 | [tool.coverage.run] 60 | branch = true 61 | source_pkgs = ["modernize"] 62 | source = ["tests"] 63 | 64 | [tool.coverage.paths] 65 | source = [ 66 | ".", 67 | ".tox/*/lib/*/site-packages/", 68 | '.tox\\*\\Lib\\site-packages\\', 69 | ] 70 | 71 | 72 | [tool.tox] 73 | legacy_tox_ini = """ 74 | ; tox configuration file for running tests on local dev env and Travis CI. 75 | ; 76 | ; The local dev environment will be executed against latest released Twisted. 77 | ; The coverage is reported only and local dev and not on Travis-CI as there 78 | ; we have separate reported (ex codecov.io) 79 | 80 | [tox] 81 | envlist = 82 | py{36,37,38,39},lint 83 | minversion=3.20.1 84 | isolated_build=true 85 | requires= 86 | virtualenv >= 20.1.0 87 | tox-wheel >= 0.6.0 88 | tox-gh-actions >= 2.1.0 89 | 90 | [testenv] 91 | extras = test 92 | commands = pytest {posargs} 93 | wheel = True 94 | wheel_build_env = build 95 | 96 | [testenv:build] 97 | # empty environment to build universal wheel once per tox invocation 98 | # https://github.com/ionelmc/tox-wheel#build-configuration 99 | 100 | [testenv:coveralls] 101 | passenv = GITHUB_* 102 | deps = 103 | coveralls 104 | coverage>=5.3 105 | commands = coveralls 106 | 107 | [testenv:lint] 108 | deps = pre-commit 109 | commands = pre-commit run --all-files --show-diff-on-failure {posargs} 110 | skip_install = true 111 | 112 | 113 | [testenv:release] 114 | deps = pep517 115 | whitelist_externals = 116 | cp 117 | rm 118 | commands = 119 | rm -rf {toxinidir}/dist 120 | cp -r {distdir} {toxinidir}/dist # copy the wheel built by tox-wheel 121 | {envpython} -m pep517.build --source --out-dir={toxinidir}/dist {toxinidir} 122 | """ 123 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [zest.releaser] 2 | python-file-with-version = modernize/__init__.py 3 | tag-format = v{version} 4 | 5 | [flake8] 6 | disable-noqa = True 7 | max-line-length = 88 8 | extend-ignore = 9 | E203, # whitespace before : is not PEP8 compliant (& conflicts with black) 10 | -------------------------------------------------------------------------------- /tests/test_fix_basestring.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | BASESTRING = ( 6 | """\ 7 | isinstance(x, basestring) 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | import six 12 | isinstance(x, six.string_types) 13 | """, 14 | ) 15 | 16 | 17 | def test_basestring(): 18 | check_on_input(*BASESTRING) 19 | -------------------------------------------------------------------------------- /tests/test_fix_classic_division.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | CLASSIC_DIVISION = ( 6 | """\ 7 | a /= 3 8 | 1 / 2 9 | """, 10 | """\ 11 | from __future__ import division 12 | a //= 3 13 | 1 // 2 14 | """, 15 | ) 16 | 17 | NEW_DIVISION = ( 18 | """\ 19 | from __future__ import division 20 | 1 / 2 21 | a /= 3 22 | """, 23 | """\ 24 | from __future__ import division 25 | 1 / 2 26 | a /= 3 27 | """, 28 | ) 29 | 30 | 31 | def test_optional(): 32 | check_on_input(CLASSIC_DIVISION[0], CLASSIC_DIVISION[0]) 33 | 34 | 35 | def test_fix_classic_division(): 36 | check_on_input( 37 | *CLASSIC_DIVISION, extra_flags=["-f", "modernize.fixes.fix_classic_division"] 38 | ) 39 | 40 | 41 | def test_new_division(): 42 | check_on_input( 43 | *NEW_DIVISION, extra_flags=["-f", "modernize.fixes.fix_classic_division"] 44 | ) 45 | -------------------------------------------------------------------------------- /tests/test_fix_dict_six.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | TYPES = "keys", "items", "values" 6 | 7 | DICT_ITER = ( 8 | """\ 9 | x.iter{type}() 10 | """, 11 | """\ 12 | from __future__ import absolute_import 13 | import six 14 | six.iter{type}(x) 15 | """, 16 | ) 17 | 18 | DICT_VIEW = ( 19 | """\ 20 | x.view{type}() 21 | """, 22 | """\ 23 | from __future__ import absolute_import 24 | import six 25 | six.view{type}(x) 26 | """, 27 | ) 28 | 29 | DICT_PLAIN = ( 30 | """\ 31 | x.{type}() 32 | """, 33 | """\ 34 | list(x.{type}()) 35 | """, 36 | ) 37 | 38 | DICT_IN_LOOP = ( 39 | """\ 40 | for k in x.items(): 41 | pass 42 | """, 43 | """\ 44 | for k in x.items(): 45 | pass 46 | """, 47 | ) 48 | 49 | DICT_ITER_IN_LOOP = ( 50 | """\ 51 | for k in x.iter{type}(): 52 | pass 53 | """, 54 | """\ 55 | from __future__ import absolute_import 56 | import six 57 | for k in six.iter{type}(x): 58 | pass 59 | """, 60 | ) 61 | 62 | DICT_ITER_IN_LIST = ( 63 | """\ 64 | for k in list(x.iter{type}()): 65 | pass 66 | """, 67 | """\ 68 | from __future__ import absolute_import 69 | import six 70 | for k in list(six.iter{type}(x)): 71 | pass 72 | """, 73 | ) 74 | 75 | CHAINED_CALLS = ( 76 | """\ 77 | (x + y).foo().iter{type}().bar() 78 | """, 79 | """\ 80 | from __future__ import absolute_import 81 | import six 82 | six.iter{type}((x + y).foo()).bar() 83 | """, 84 | ) 85 | 86 | 87 | def check_all_types(input, output): 88 | for type_ in TYPES: 89 | check_on_input(input.format(type=type_), output.format(type=type_)) 90 | 91 | 92 | def test_dict_iter(): 93 | check_all_types(*DICT_ITER) 94 | 95 | 96 | def test_dict_view(): 97 | check_all_types(*DICT_VIEW) 98 | 99 | 100 | def test_dict_plain(): 101 | check_all_types(*DICT_PLAIN) 102 | 103 | 104 | def test_dict_in_loop(): 105 | check_on_input(*DICT_IN_LOOP) 106 | 107 | 108 | def test_dict_iter_in_loop(): 109 | check_all_types(*DICT_ITER_IN_LOOP) 110 | 111 | 112 | def test_dict_iter_in_list(): 113 | check_all_types(*DICT_ITER_IN_LIST) 114 | 115 | 116 | def test_chained_calls(): 117 | check_all_types(*CHAINED_CALLS) 118 | -------------------------------------------------------------------------------- /tests/test_fix_file.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | FILE_CALL = ( 6 | """\ 7 | file('some/file/path', 'r') 8 | """, 9 | """\ 10 | open('some/file/path', 'r') 11 | """, 12 | ) 13 | 14 | FILE_CONTEXT_MANAGER = ( 15 | """\ 16 | with file('some/file/path', 'r') as file_: 17 | pass 18 | """, 19 | """\ 20 | with open('some/file/path', 'r') as file_: 21 | pass 22 | """, 23 | ) 24 | 25 | FILE_ATTR = ( 26 | """\ 27 | file('path').readlines 28 | """, 29 | """\ 30 | open('path').readlines 31 | """, 32 | ) 33 | 34 | FILE_REF = ( 35 | """\ 36 | file 37 | """, 38 | """\ 39 | file 40 | """, 41 | ) 42 | 43 | 44 | def test_file_call(): 45 | check_on_input(*FILE_CALL) 46 | 47 | 48 | def test_file_context_manager(): 49 | check_on_input(*FILE_CONTEXT_MANAGER) 50 | 51 | 52 | def test_file_attr(): 53 | check_on_input(*FILE_ATTR) 54 | 55 | 56 | def test_file_ref(): 57 | check_on_input(*FILE_REF) 58 | -------------------------------------------------------------------------------- /tests/test_fix_filter.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | FILTER_CALL = ( 6 | """\ 7 | filter(func, [1]) 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | from six.moves import filter 12 | list(filter(func, [1])) 13 | """, 14 | ) 15 | 16 | 17 | FILTER_ITERATOR_CONTEXT = ( 18 | """\ 19 | for a in filter(func, [1]): 20 | pass 21 | """, 22 | """\ 23 | from __future__ import absolute_import 24 | from six.moves import filter 25 | for a in filter(func, [1]): 26 | pass 27 | """, 28 | ) 29 | 30 | FILTER_NONE = ( 31 | """\ 32 | filter(None, x) 33 | """, 34 | """\ 35 | [_f for _f in x if _f] 36 | """, 37 | ) 38 | 39 | 40 | def test_filter_call(): 41 | check_on_input(*FILTER_CALL) 42 | 43 | 44 | def test_filter_iterator_context(): 45 | check_on_input(*FILTER_ITERATOR_CONTEXT) 46 | 47 | 48 | def test_filter_None(): 49 | check_on_input(*FILTER_NONE) 50 | -------------------------------------------------------------------------------- /tests/test_fix_import.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | NO_IMPORTS = ( 6 | """\ 7 | """, 8 | """\ 9 | """, 10 | ) 11 | 12 | ONLY_FUTURE_IMPORTS = ( 13 | """\ 14 | from __future__ import print_function 15 | """, 16 | """\ 17 | from __future__ import print_function 18 | """, 19 | ) 20 | 21 | ONLY_NORMAL_IMPORTS = ( 22 | """\ 23 | import foo 24 | """, 25 | """\ 26 | from __future__ import absolute_import 27 | import foo 28 | """, 29 | ) 30 | 31 | NORMAL_AND_FUTURE_IMPORTS = ( 32 | """\ 33 | from __future__ import print_function 34 | import foo 35 | """, 36 | """\ 37 | from __future__ import print_function 38 | from __future__ import absolute_import 39 | import foo 40 | """, 41 | ) 42 | 43 | DOCSTRING = ( 44 | """\ 45 | \""" 46 | Docstring 47 | \""" 48 | import foo 49 | """, 50 | """\ 51 | \""" 52 | Docstring 53 | \""" 54 | from __future__ import absolute_import 55 | import foo 56 | """, 57 | ) 58 | 59 | SHEBANG = ( 60 | """\ 61 | #!/usr/bin/env python 62 | import foo 63 | """, 64 | """\ 65 | #!/usr/bin/env python 66 | from __future__ import absolute_import 67 | import foo 68 | """, 69 | ) 70 | 71 | DOCSTING_AND_SHEBANG = ( 72 | """\ 73 | #!/usr/bin/env python 74 | \""" 75 | Docstring 76 | \""" 77 | import foo 78 | """, 79 | """\ 80 | #!/usr/bin/env python 81 | \""" 82 | Docstring 83 | \""" 84 | from __future__ import absolute_import 85 | import foo 86 | """, 87 | ) 88 | 89 | COPYRIGHT_AND_SHEBANG = ( 90 | """\ 91 | #!/usr/bin/env python 92 | 93 | # 94 | # Copyright notice 95 | # 96 | 97 | import foo 98 | """, 99 | """\ 100 | #!/usr/bin/env python 101 | 102 | # 103 | # Copyright notice 104 | # 105 | 106 | from __future__ import absolute_import 107 | import foo 108 | """, 109 | ) 110 | 111 | 112 | COPYRIGHT_AND_DOCSTRING = ( 113 | """\ 114 | # 115 | # Copyright notice 116 | # 117 | 118 | \"""Docstring\""" 119 | 120 | import foo 121 | """, 122 | """\ 123 | # 124 | # Copyright notice 125 | # 126 | 127 | \"""Docstring\""" 128 | 129 | from __future__ import absolute_import 130 | import foo 131 | """, 132 | ) 133 | 134 | 135 | def test_no_imports(): 136 | check_on_input(*NO_IMPORTS) 137 | 138 | 139 | def test_only_future_imports(): 140 | check_on_input(*ONLY_FUTURE_IMPORTS) 141 | 142 | 143 | def test_only_normal_imports(): 144 | check_on_input(*ONLY_NORMAL_IMPORTS) 145 | 146 | 147 | def test_normal_and_future_imports(): 148 | check_on_input(*NORMAL_AND_FUTURE_IMPORTS) 149 | 150 | 151 | def test_import_with_docstring(): 152 | check_on_input(*DOCSTRING) 153 | 154 | 155 | def test_import_with_shebang(): 156 | check_on_input(*SHEBANG) 157 | 158 | 159 | def test_import_with_docstring_and_shebang(): 160 | check_on_input(*DOCSTING_AND_SHEBANG) 161 | 162 | 163 | def test_import_with_copyright_and_shebang(): 164 | check_on_input(*COPYRIGHT_AND_SHEBANG) 165 | 166 | 167 | def test_import_with_copyright_and_docstring(): 168 | check_on_input(*COPYRIGHT_AND_DOCSTRING) 169 | -------------------------------------------------------------------------------- /tests/test_fix_imports_six.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | import sys 4 | import unittest 5 | 6 | try: 7 | from six.moves import tkinter 8 | except ImportError: 9 | tkinter = None 10 | 11 | from utils import check_on_input 12 | 13 | from modernize.fixes import fix_imports_six 14 | 15 | MOVED_MODULE = ( 16 | """\ 17 | import ConfigParser 18 | ConfigParser.ConfigParser() 19 | """, 20 | """\ 21 | from __future__ import absolute_import 22 | import six.moves.configparser 23 | six.moves.configparser.ConfigParser() 24 | """, 25 | ) 26 | 27 | MOVED_MODULE_FROMLIST = ( 28 | """\ 29 | from ConfigParser import ConfigParser 30 | ConfigParser() 31 | """, 32 | """\ 33 | from __future__ import absolute_import 34 | from six.moves.configparser import ConfigParser 35 | ConfigParser() 36 | """, 37 | ) 38 | 39 | 40 | def test_moved_module(): 41 | check_on_input(*MOVED_MODULE) 42 | 43 | 44 | def test_moved_module_fromlist(): 45 | check_on_input(*MOVED_MODULE_FROMLIST) 46 | 47 | 48 | @unittest.skipIf(sys.version_info[0] >= 3, "Test only runs on Python 2") 49 | def test_validate_mapping(): 50 | for py2_name, six_name in fix_imports_six.FixImportsSix.mapping.items(): 51 | try: 52 | __import__(py2_name) 53 | __import__(six_name) 54 | except ImportError: 55 | if "tkinter" in six_name: 56 | # Ignore error if tkinter not installed 57 | if tkinter is not None: 58 | raise 59 | elif "winreg" in six_name: 60 | # Ignore error if we're not on Windows 61 | if sys.platform.startswith("win"): 62 | raise 63 | else: 64 | raise 65 | -------------------------------------------------------------------------------- /tests/test_fix_input_six.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | INPUT = ( 6 | """\ 7 | input() 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | from six.moves import input 12 | eval(input()) 13 | """, 14 | ) 15 | 16 | INPUT_ARGS = ( 17 | """\ 18 | input('hello') 19 | """, 20 | """\ 21 | from __future__ import absolute_import 22 | from six.moves import input 23 | eval(input('hello')) 24 | """, 25 | ) 26 | 27 | RAW_INPUT = ( 28 | """\ 29 | raw_input() 30 | """, 31 | """\ 32 | from __future__ import absolute_import 33 | from six.moves import input 34 | input() 35 | """, 36 | ) 37 | 38 | RAW_INPUT_TRAILER = ( 39 | """\ 40 | raw_input()[0] 41 | """, 42 | """\ 43 | from __future__ import absolute_import 44 | from six.moves import input 45 | input()[0] 46 | """, 47 | ) 48 | 49 | RAW_INPUT_INPUT = ( 50 | """\ 51 | raw_input() 52 | input() 53 | """, 54 | """\ 55 | from __future__ import absolute_import 56 | from six.moves import input 57 | input() 58 | eval(input()) 59 | """, 60 | ) 61 | 62 | 63 | def test_input(): 64 | check_on_input(*INPUT) 65 | 66 | 67 | def test_input_args(): 68 | check_on_input(*INPUT_ARGS) 69 | 70 | 71 | def test_raw_input(): 72 | check_on_input(*RAW_INPUT) 73 | 74 | 75 | def test_raw_input_trailer(): 76 | check_on_input(*RAW_INPUT_TRAILER) 77 | 78 | 79 | def test_raw_input_input(): 80 | check_on_input(*RAW_INPUT_INPUT) 81 | -------------------------------------------------------------------------------- /tests/test_fix_int_long_tuple.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | INT_LONG_ISINSTANCE = ( 6 | """\ 7 | isinstance(1, (int, long)) 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | import six 12 | isinstance(1, six.integer_types) 13 | """, 14 | ) 15 | 16 | LONG_INT_ISINSTANCE = ( 17 | """\ 18 | isinstance(1, (long, int)) 19 | """, 20 | """\ 21 | from __future__ import absolute_import 22 | import six 23 | isinstance(1, six.integer_types) 24 | """, 25 | ) 26 | 27 | 28 | def test_int_long_isinstance(): 29 | check_on_input(*INT_LONG_ISINSTANCE) 30 | 31 | 32 | def test_long_int_isinstance(): 33 | check_on_input(*LONG_INT_ISINSTANCE) 34 | -------------------------------------------------------------------------------- /tests/test_fix_itertools_imports_six.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | IZIP_AND_CHAIN_REFERENCE = ( 6 | """\ 7 | from itertools import izip_longest, chain 8 | izip_longest([1, 2], [1]) 9 | """, 10 | """\ 11 | from __future__ import absolute_import 12 | from itertools import chain 13 | from six.moves import zip_longest 14 | zip_longest([1, 2], [1]) 15 | """, 16 | ) 17 | 18 | REMOVE_ITERTOOLS_REFERENCE = ( 19 | """\ 20 | from itertools import izip 21 | izip([1, 2], [1]) 22 | """, 23 | """\ 24 | from __future__ import absolute_import 25 | 26 | from six.moves import zip 27 | zip([1, 2], [1]) 28 | """, 29 | ) 30 | 31 | IMAP_TO_MAP_MODULE_IMPORT = ( 32 | """\ 33 | import itertools 34 | itertools.imap(lambda x: x * 2, [1, 2]) 35 | """, 36 | """\ 37 | from __future__ import absolute_import 38 | import itertools 39 | from six.moves import map 40 | map(lambda x: x * 2, [1, 2]) 41 | """, 42 | ) 43 | 44 | 45 | def test_izip_longest_and_chain(): 46 | check_on_input(*IZIP_AND_CHAIN_REFERENCE) 47 | 48 | 49 | def test_removes_import_line(): 50 | check_on_input(*REMOVE_ITERTOOLS_REFERENCE) 51 | 52 | 53 | def test_imap_to_map_module_import(): 54 | check_on_input(*IMAP_TO_MAP_MODULE_IMPORT) 55 | -------------------------------------------------------------------------------- /tests/test_fix_map.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | MAP_1_ARG = ( 6 | """\ 7 | map(*args) 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | from six.moves import map 12 | list(map(*args)) 13 | """, 14 | ) 15 | 16 | MAP_2_ARGS = ( 17 | """\ 18 | map(x, [1]) 19 | """, 20 | """\ 21 | from __future__ import absolute_import 22 | from six.moves import map 23 | list(map(x, [1])) 24 | """, 25 | ) 26 | 27 | MAP_3_ARGS = ( 28 | """\ 29 | map(x, [1], [2]) 30 | """, 31 | """\ 32 | from __future__ import absolute_import 33 | from six.moves import map 34 | list(map(x, [1], [2])) 35 | """, 36 | ) 37 | 38 | MAP_4_ARGS = ( 39 | """\ 40 | map(x, [1], [2], [3]) 41 | """, 42 | """\ 43 | from __future__ import absolute_import 44 | from six.moves import map 45 | list(map(x, [1], [2], [3])) 46 | """, 47 | ) 48 | 49 | MAP_REF = ( 50 | """\ 51 | x = map 52 | """, 53 | """\ 54 | x = map 55 | """, 56 | ) 57 | 58 | MAP_ITERATOR_CONTEXT = ( 59 | """\ 60 | for a in map(x, [1]): 61 | pass 62 | """, 63 | """\ 64 | from __future__ import absolute_import 65 | from six.moves import map 66 | for a in map(x, [1]): 67 | pass 68 | """, 69 | ) 70 | 71 | MAP_LAMBDA = ( 72 | """\ 73 | x = map(lambda x: x+1, stuff) 74 | """, 75 | """\ 76 | x = [x+1 for x in stuff] 77 | """, 78 | ) 79 | 80 | 81 | def test_map_1_arg(): 82 | check_on_input(*MAP_1_ARG) 83 | 84 | 85 | def test_map_2_args(): 86 | check_on_input(*MAP_2_ARGS) 87 | 88 | 89 | def test_map_3_args(): 90 | check_on_input(*MAP_3_ARGS) 91 | 92 | 93 | def test_map_4_args(): 94 | check_on_input(*MAP_4_ARGS) 95 | 96 | 97 | def test_map_ref(): 98 | check_on_input(*MAP_REF) 99 | 100 | 101 | def test_map_iterator_context(): 102 | check_on_input(*MAP_ITERATOR_CONTEXT) 103 | 104 | 105 | def test_map_lambda(): 106 | check_on_input(*MAP_LAMBDA) 107 | -------------------------------------------------------------------------------- /tests/test_fix_metaclass.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | METACLASS_NO_BASE = ( 6 | """\ 7 | class Foo: 8 | __metaclass__ = Meta 9 | """, 10 | """\ 11 | from __future__ import absolute_import 12 | import six 13 | class Foo(six.with_metaclass(Meta)): 14 | pass 15 | """, 16 | ) 17 | 18 | METACLASS_NO_BASE_PARENS = ( 19 | """\ 20 | class Foo(): 21 | __metaclass__ = Meta 22 | """, 23 | """\ 24 | from __future__ import absolute_import 25 | import six 26 | class Foo(six.with_metaclass(Meta)): 27 | pass 28 | """, 29 | ) 30 | 31 | METACLASS_SINGLE_BASE = ( 32 | """\ 33 | class Foo(Bar): 34 | __metaclass__ = Meta 35 | """, 36 | """\ 37 | from __future__ import absolute_import 38 | import six 39 | class Foo(six.with_metaclass(Meta, Bar)): 40 | pass 41 | """, 42 | ) 43 | 44 | METACLASS_MANY_BASES = ( 45 | """\ 46 | class Foo(Bar, Spam): 47 | __metaclass__ = Meta 48 | """, 49 | """\ 50 | from __future__ import absolute_import 51 | import six 52 | class Foo(six.with_metaclass(Meta, Bar, Spam)): 53 | pass 54 | """, 55 | ) 56 | 57 | METACLASS_ONE_LINER = ( 58 | """\ 59 | class Foo: __metaclass__ = Meta 60 | """, 61 | """\ 62 | from __future__ import absolute_import 63 | import six 64 | class Foo(six.with_metaclass(Meta)): pass 65 | """, 66 | ) 67 | 68 | METACLASS_SEMICOLON_STMT = ( 69 | """\ 70 | class Foo(Bar): 71 | __metaclass__ = Meta; a = 12 72 | b = 64 73 | """, 74 | """\ 75 | from __future__ import absolute_import 76 | import six 77 | class Foo(six.with_metaclass(Meta, Bar)): 78 | a = 12 79 | b = 64 80 | """, 81 | ) 82 | 83 | 84 | def test_metaclass_no_base(): 85 | check_on_input(*METACLASS_NO_BASE) 86 | 87 | 88 | def test_metaclass_no_base_parens(): 89 | check_on_input(*METACLASS_NO_BASE_PARENS) 90 | 91 | 92 | def test_metaclass_single_base(): 93 | check_on_input(*METACLASS_SINGLE_BASE) 94 | 95 | 96 | def test_metaclass_many_bases(): 97 | check_on_input(*METACLASS_MANY_BASES) 98 | 99 | 100 | def test_metaclass_one_liner(): 101 | check_on_input(*METACLASS_ONE_LINER) 102 | 103 | 104 | def test_metaclass_semicolon_stmt(): 105 | check_on_input(*METACLASS_SEMICOLON_STMT) 106 | -------------------------------------------------------------------------------- /tests/test_fix_next.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | NEXT_METHOD = ( 6 | """ 7 | spam.next() 8 | """, 9 | """ 10 | next(spam) 11 | """, 12 | ) 13 | 14 | NEXT_NESTED = ( 15 | """ 16 | eggs.spam.next() 17 | """, 18 | """ 19 | next(eggs.spam) 20 | """, 21 | ) 22 | 23 | 24 | def test_next_method(): 25 | check_on_input(*NEXT_METHOD) 26 | 27 | 28 | def test_next_nested(): 29 | check_on_input(*NEXT_NESTED) 30 | -------------------------------------------------------------------------------- /tests/test_fix_open.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | OPEN = ( 6 | """\ 7 | {0}('some/path') 8 | """, 9 | """\ 10 | from io import open 11 | open('some/path') 12 | """, 13 | ) 14 | 15 | 16 | def test_open(): 17 | check_on_input( 18 | OPEN[0].format("open"), 19 | OPEN[1], 20 | extra_flags=["-f", "modernize.fixes.fix_open"], 21 | ) 22 | 23 | 24 | def test_open_optional(): 25 | check_on_input(OPEN[0].format("open"), OPEN[0].format("open")) 26 | 27 | 28 | def test_file(): 29 | flags = ["-f", "modernize.fixes.fix_open", "-f", "modernize.fixes.fix_file"] 30 | check_on_input(OPEN[0].format("file"), OPEN[1], extra_flags=flags) 31 | -------------------------------------------------------------------------------- /tests/test_fix_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | PRINT_BARE = ( 6 | """\ 7 | print 8 | """, 9 | """\ 10 | from __future__ import print_function 11 | print() 12 | """, 13 | ) 14 | 15 | PRINT_SIMPLE = ( 16 | """\ 17 | print 'Hello' 18 | """, 19 | """\ 20 | from __future__ import print_function 21 | print('Hello') 22 | """, 23 | ) 24 | 25 | PRINT_MULTIPLE = ( 26 | """\ 27 | print 'Hello', 'world' 28 | """, 29 | """\ 30 | from __future__ import print_function 31 | print('Hello', 'world') 32 | """, 33 | ) 34 | 35 | PRINT_WITH_PARENS = ( 36 | """\ 37 | print('Hello') 38 | """, 39 | """\ 40 | from __future__ import print_function 41 | print('Hello') 42 | """, 43 | ) 44 | 45 | PRINT_WITH_COMMA = ( 46 | """\ 47 | print 'Hello', 48 | """, 49 | """\ 50 | from __future__ import print_function 51 | print('Hello', end=' ') 52 | """, 53 | ) 54 | 55 | PRINT_TO_STREAM = ( 56 | """\ 57 | print >>x, 'Hello' 58 | """, 59 | """\ 60 | from __future__ import print_function 61 | print('Hello', file=x) 62 | """, 63 | ) 64 | 65 | PRINT_TO_STREAM_WITH_COMMA = ( 66 | """\ 67 | print >>x, 'Hello', 68 | """, 69 | """\ 70 | from __future__ import print_function 71 | print('Hello', end=' ', file=x) 72 | """, 73 | ) 74 | 75 | 76 | def test_print_bare(): 77 | check_on_input(*PRINT_BARE) 78 | 79 | 80 | def test_print_simple(): 81 | check_on_input(*PRINT_SIMPLE) 82 | 83 | 84 | def test_print_multiple(): 85 | check_on_input(*PRINT_MULTIPLE) 86 | 87 | 88 | def test_print_with_parens(): 89 | check_on_input(*PRINT_WITH_PARENS) 90 | 91 | 92 | def test_print_with_comma(): 93 | check_on_input(*PRINT_WITH_COMMA) 94 | 95 | 96 | def test_print_to_stream(): 97 | check_on_input(*PRINT_TO_STREAM) 98 | 99 | 100 | def test_print_to_stream_with_comma(): 101 | check_on_input(*PRINT_TO_STREAM_WITH_COMMA) 102 | -------------------------------------------------------------------------------- /tests/test_fix_raise.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | RAISE = ( 6 | """\ 7 | raise 8 | """, 9 | """\ 10 | raise 11 | """, 12 | ) 13 | 14 | RAISE_EXC = ( 15 | """\ 16 | raise e 17 | """, 18 | """\ 19 | raise e 20 | """, 21 | ) 22 | 23 | RAISE_VALUE = ( 24 | """\ 25 | raise e, v 26 | """, 27 | """\ 28 | raise e(v) 29 | """, 30 | ) 31 | 32 | RAISE_TUPLE = ( 33 | """\ 34 | raise ((((E1, E2), E3), E4), E5), V 35 | """, 36 | """\ 37 | raise E1(V) 38 | """, 39 | ) 40 | 41 | # Can't be converted; translation would emit a warning. 42 | RAISE_STRING = ( 43 | """\ 44 | raise 'exception' 45 | """, 46 | """\ 47 | raise 'exception' 48 | """, 49 | ) 50 | 51 | RAISE_TUPLE_ARGS = ( 52 | """\ 53 | raise e, (1, 2) 54 | """, 55 | """\ 56 | raise e(1, 2) 57 | """, 58 | ) 59 | 60 | 61 | def test_raise(): 62 | check_on_input(*RAISE) 63 | 64 | 65 | def test_raise_exc(): 66 | check_on_input(*RAISE_EXC) 67 | 68 | 69 | def test_raise_value(): 70 | check_on_input(*RAISE_VALUE) 71 | 72 | 73 | def test_raise_tuple(): 74 | check_on_input(*RAISE_TUPLE) 75 | 76 | 77 | def test_raise_string(): 78 | check_on_input(*RAISE_STRING) 79 | 80 | 81 | def test_raise_tuple_args(): 82 | check_on_input(*RAISE_TUPLE_ARGS) 83 | -------------------------------------------------------------------------------- /tests/test_fix_unichr_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | UNICHR_METHOD_REF = ( 6 | """\ 7 | converter = unichr 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | from six import unichr 12 | converter = unichr 13 | """, 14 | ) 15 | 16 | UNICHR_METHOD_CALL = ( 17 | """\ 18 | unichr(42) 19 | """, 20 | """\ 21 | from __future__ import absolute_import 22 | from six import unichr 23 | unichr(42) 24 | """, 25 | ) 26 | 27 | UNICHR_USER_CALL = ( 28 | """\ 29 | foobar.unichr(42) 30 | """, 31 | """\ 32 | foobar.unichr(42) 33 | """, 34 | ) 35 | 36 | 37 | def test_unichr_method_ref(): 38 | check_on_input(*UNICHR_METHOD_REF) 39 | 40 | 41 | def test_unichr_method_call(): 42 | check_on_input(*UNICHR_METHOD_CALL) 43 | 44 | 45 | def test_unichr_user_call(): 46 | check_on_input(*UNICHR_USER_CALL) 47 | -------------------------------------------------------------------------------- /tests/test_fix_unicode.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | UNICODE_LITERALS = """\ 6 | a = u'' 7 | b = U"\\u2041" 8 | c = ur'''blah 9 | foo''' 10 | import sys 11 | """ 12 | 13 | UNICODE_LITERALS_six = """\ 14 | from __future__ import absolute_import 15 | import six 16 | a = six.u('') 17 | b = six.u("\\u2041") 18 | c = six.u(r'''blah 19 | foo''') 20 | import sys 21 | """ 22 | 23 | UNICODE_LITERALS_compat = """\ 24 | from __future__ import absolute_import 25 | a = u'' 26 | b = U"\\u2041" 27 | c = ur'''blah 28 | foo''' 29 | import sys 30 | """ 31 | 32 | UNICODE_LITERALS_future = """\ 33 | from __future__ import absolute_import 34 | from __future__ import unicode_literals 35 | a = '' 36 | b = "\\u2041" 37 | c = r'''blah 38 | foo''' 39 | import sys 40 | """ 41 | 42 | 43 | def test_unicode_six(): 44 | check_on_input( 45 | UNICODE_LITERALS, UNICODE_LITERALS_six, extra_flags=["--six-unicode"] 46 | ) 47 | 48 | 49 | def test_unicode_compat(): 50 | check_on_input(UNICODE_LITERALS, UNICODE_LITERALS_compat) 51 | 52 | 53 | def test_unicode_future(): 54 | check_on_input( 55 | UNICODE_LITERALS, UNICODE_LITERALS_future, extra_flags=["--future-unicode"] 56 | ) 57 | -------------------------------------------------------------------------------- /tests/test_fix_unicode_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | UNICODE_TYPE_REF = ( 6 | """\ 7 | isinstance(u'str', unicode) 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | import six 12 | isinstance(u'str', six.text_type) 13 | """, 14 | ) 15 | 16 | UNICODE_TYPE_CALL = ( 17 | """\ 18 | unicode(x) 19 | """, 20 | """\ 21 | from __future__ import absolute_import 22 | import six 23 | six.text_type(x) 24 | """, 25 | ) 26 | 27 | 28 | def test_unicode_type_ref(): 29 | check_on_input(*UNICODE_TYPE_REF) 30 | 31 | 32 | def test_unicode_type_call(): 33 | check_on_input(*UNICODE_TYPE_CALL) 34 | -------------------------------------------------------------------------------- /tests/test_fix_urllib_six.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | URLLIB_MODULE_REFERENCE = ( 6 | """\ 7 | import urllib 8 | urllib.quote_plus('hello world') 9 | """, 10 | """\ 11 | from __future__ import absolute_import 12 | import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error 13 | six.moves.urllib.parse.quote_plus('hello world') 14 | """, 15 | ) 16 | 17 | 18 | URLLIB_FUNCTION_REFERENCE = ( 19 | """\ 20 | from urllib2 import urlopen 21 | urlopen('https://www.python.org') 22 | """, 23 | """\ 24 | from __future__ import absolute_import 25 | from six.moves.urllib.request import urlopen 26 | urlopen('https://www.python.org') 27 | """, 28 | ) 29 | 30 | 31 | URLLIB_MULTI_IMPORT_REFERENCE = ( 32 | """\ 33 | from urllib2 import HTTPError, urlopen 34 | """, 35 | """\ 36 | from __future__ import absolute_import 37 | from six.moves.urllib.error import HTTPError 38 | from six.moves.urllib.request import urlopen 39 | """, 40 | ) 41 | 42 | 43 | URLLIB_IMPORT_AS = ( 44 | """\ 45 | from urllib2 import urlopen as urlo 46 | from urllib2 import HTTPError, URLError as urle 47 | """, 48 | """\ 49 | from __future__ import absolute_import 50 | from six.moves.urllib.request import urlopen as urlo 51 | from six.moves.urllib.error import HTTPError, URLError as urle 52 | """, 53 | ) 54 | 55 | 56 | # Can't be converted; translation would emit a warning. 57 | URLIB_INVALID_CODE = ( 58 | """\ 59 | from urllib2 import * 60 | from urllib2 import foobarraz 61 | from urllib2 import foo, bar as raz 62 | import urllib as urllib_py2 63 | import urllib 64 | urllib.foobarraz('hello world') 65 | """, 66 | """\ 67 | from __future__ import absolute_import 68 | from urllib2 import * 69 | from urllib2 import foobarraz 70 | from urllib2 import foo, bar as raz 71 | import urllib as urllib_py2 72 | import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error 73 | urllib.foobarraz('hello world') 74 | """, 75 | ) 76 | 77 | 78 | def test_urllib_module_reference(): 79 | check_on_input(*URLLIB_MODULE_REFERENCE) 80 | 81 | 82 | def test_urllib_function_reference(): 83 | check_on_input(*URLLIB_FUNCTION_REFERENCE) 84 | 85 | 86 | def test_urllib_multi_import(): 87 | check_on_input(*URLLIB_MULTI_IMPORT_REFERENCE) 88 | 89 | 90 | def test_urllib_import_as(): 91 | check_on_input(*URLLIB_IMPORT_AS) 92 | 93 | 94 | def test_urllib_invalid_imports(): 95 | check_on_input(*URLIB_INVALID_CODE) 96 | -------------------------------------------------------------------------------- /tests/test_fix_xrange_six.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | RANGE = ( 6 | """\ 7 | x = range(1) 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | from six.moves import range 12 | x = list(range(1)) 13 | """, 14 | ) 15 | 16 | XRANGE = ( 17 | """\ 18 | xrange(1) 19 | """, 20 | """\ 21 | from __future__ import absolute_import 22 | from six.moves import range 23 | range(1) 24 | """, 25 | ) 26 | 27 | XRANGE_RANGE = ( 28 | """\ 29 | x = xrange(1) 30 | y = range(1) 31 | """, 32 | """\ 33 | from __future__ import absolute_import 34 | from six.moves import range 35 | x = range(1) 36 | y = list(range(1)) 37 | """, 38 | ) 39 | 40 | 41 | def test_range(): 42 | check_on_input(*RANGE) 43 | 44 | 45 | def test_xrange(): 46 | check_on_input(*XRANGE) 47 | 48 | 49 | def test_xrange_range(): 50 | check_on_input(*XRANGE_RANGE) 51 | -------------------------------------------------------------------------------- /tests/test_fix_zip.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | ZIP_CALL_NO_ARGS = ( 6 | """\ 7 | zip() 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | from six.moves import zip 12 | list(zip()) 13 | """, 14 | ) 15 | 16 | ZIP_CALL_1_ARG = ( 17 | """\ 18 | zip(x) 19 | """, 20 | """\ 21 | from __future__ import absolute_import 22 | from six.moves import zip 23 | list(zip(x)) 24 | """, 25 | ) 26 | 27 | ZIP_CALL_2_ARGS = ( 28 | """\ 29 | zip(x, y) 30 | zip(w, z) 31 | """, 32 | """\ 33 | from __future__ import absolute_import 34 | from six.moves import zip 35 | list(zip(x, y)) 36 | list(zip(w, z)) 37 | """, 38 | ) 39 | 40 | ZIP_CALL_STAR_ARGS = ( 41 | """\ 42 | zip(*args) 43 | """, 44 | """\ 45 | from __future__ import absolute_import 46 | from six.moves import zip 47 | list(zip(*args)) 48 | """, 49 | ) 50 | 51 | ZIP_ITERATOR_CONTEXT = ( 52 | """\ 53 | for a in zip(x): 54 | pass 55 | """, 56 | """\ 57 | from __future__ import absolute_import 58 | from six.moves import zip 59 | for a in zip(x): 60 | pass 61 | """, 62 | ) 63 | 64 | 65 | def test_zip_call_no_args(): 66 | check_on_input(*ZIP_CALL_NO_ARGS) 67 | 68 | 69 | def test_zip_call_1_arg(): 70 | check_on_input(*ZIP_CALL_1_ARG) 71 | 72 | 73 | def test_zip_call_2_args(): 74 | check_on_input(*ZIP_CALL_2_ARGS) 75 | 76 | 77 | def test_zip_call_star_args(): 78 | check_on_input(*ZIP_CALL_STAR_ARGS) 79 | 80 | 81 | def test_zip_iterator_context(): 82 | check_on_input(*ZIP_ITERATOR_CONTEXT) 83 | -------------------------------------------------------------------------------- /tests/test_fixes.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from fissix import refactor 4 | 5 | from modernize import fixes 6 | 7 | FISSIX_FIXES_PKG = "fissix.fixes" 8 | MODERNIZE_FIXES_PKG = "modernize.fixes" 9 | 10 | 11 | def check_existence(prefix, module_names): 12 | """Check that module_names have the expected prefix and exist.""" 13 | dotted_prefix = prefix + "." 14 | for module_name in module_names: 15 | if not module_name.startswith(dotted_prefix): 16 | msg = f"{module_name!r} does not start with {prefix!r}" 17 | raise AssertionError(msg) 18 | try: 19 | __import__(module_name) 20 | except ImportError: 21 | raise AssertionError(f"{module_name!r} cannot be imported") 22 | 23 | 24 | def test_fissix_fix_names(): 25 | check_existence(FISSIX_FIXES_PKG, fixes.fissix_fix_names) 26 | 27 | 28 | def test_six_fix_names(): 29 | check_existence(MODERNIZE_FIXES_PKG, fixes.six_fix_names) 30 | 31 | 32 | def test_fixers_importable(): 33 | fixers = refactor.get_fixers_from_package(MODERNIZE_FIXES_PKG) 34 | for module_name in fixers: 35 | try: 36 | __import__(module_name) 37 | except ImportError: 38 | raise AssertionError(f"{module_name!r} cannot be imported") 39 | -------------------------------------------------------------------------------- /tests/test_future_behaviour.py: -------------------------------------------------------------------------------- 1 | # Tests for problem with multiple futures added to single file 2 | 3 | from __future__ import generator_stop 4 | 5 | import os 6 | import shutil 7 | import tempfile 8 | 9 | from utils import check_on_input 10 | 11 | from modernize.__main__ import main as modernize_main 12 | 13 | SINGLE_PRINT_CONTENT = """ 14 | print 'world' 15 | """ 16 | 17 | TWO_PRINTS_CONTENT = """ 18 | print 'Hello' 19 | print 'world' 20 | """ 21 | 22 | COMPLICATED_CONTENT = """ 23 | print 'Hello' 24 | print u'world' 25 | def sub(x): 26 | print x, u"here" 27 | """ 28 | 29 | PROBLEMATIC_CONTENT = ''' 30 | 31 | """ 32 | Hello 33 | """ 34 | 35 | from a.b.c import d 36 | 37 | def test_existing(): 38 | print d 39 | ''' 40 | 41 | 42 | def _check_for_multiple_futures(file_name, source_content): 43 | """ 44 | Checks for multiple identical futures in given file, 45 | raises if found. 46 | Returns dictionary of found futures (name => 1) 47 | """ 48 | counts = {} 49 | result_content = "" 50 | with open(file_name) as input: 51 | for line in input: 52 | if line.startswith("from __future__"): 53 | counts[line] = 1 + counts.get(line, 0) 54 | result_content += line 55 | for future, how_many in counts.items(): 56 | if how_many > 1: 57 | raise Exception( 58 | f"The same future repeated more than once ({how_many} times):\n" 59 | f"{future}\n\n* Input file:\n{source_content}\n\n" 60 | f"* Output file:\n{result_content}\n" 61 | ) 62 | return counts 63 | 64 | 65 | def _check_on_input(file_content, extra_flags=[]): 66 | try: 67 | tmpdirname = tempfile.mkdtemp() 68 | test_input_name = os.path.join(tmpdirname, "input.py") 69 | with open(test_input_name, "wt") as input: 70 | input.write(file_content) 71 | modernize_main(extra_flags + ["-w", test_input_name]) 72 | _check_for_multiple_futures(test_input_name, file_content) 73 | finally: 74 | shutil.rmtree(tmpdirname) 75 | 76 | 77 | def test_single_print(): 78 | _check_on_input(SINGLE_PRINT_CONTENT) 79 | 80 | 81 | def test_two_prints(): 82 | _check_on_input(TWO_PRINTS_CONTENT) 83 | 84 | 85 | def test_many_prints_and_unicode(): 86 | _check_on_input(COMPLICATED_CONTENT, ["--future-unicode"]) 87 | 88 | 89 | def test_two_files_on_single_run(): 90 | # Mostly to test whether second file gets its "from future ..." 91 | try: 92 | tmpdirname = tempfile.mkdtemp() 93 | input_names = [ 94 | os.path.join(tmpdirname, f"input_{idx}.py") for idx in range(0, 3) 95 | ] 96 | for input_name in input_names: 97 | with open(input_name, "wt") as input: 98 | input.write(TWO_PRINTS_CONTENT) 99 | modernize_main(["-w"] + input_names) 100 | for input_name in input_names: 101 | futs = _check_for_multiple_futures(input_name, TWO_PRINTS_CONTENT) 102 | if not futs: 103 | raise Exception("File {0} got no from __future__ (but it should)") 104 | finally: 105 | shutil.rmtree(tmpdirname) 106 | 107 | 108 | def test_problematic_file(): 109 | # ON this one I get crash 110 | _check_on_input(PROBLEMATIC_CONTENT) 111 | 112 | 113 | FUTURE_IMPORT_AS = ( 114 | """\ 115 | from __future__ import print_function as pf 116 | print("abc") 117 | """, 118 | """\ 119 | from __future__ import print_function as pf 120 | print("abc") 121 | """, 122 | ) 123 | 124 | FUTURE_IMPORT_AS_MULTIPLE = ( 125 | """\ 126 | from __future__ import print_function as pf, division as dv 127 | print("abc") 128 | """, 129 | """\ 130 | from __future__ import print_function as pf, division as dv 131 | print("abc") 132 | """, 133 | ) 134 | 135 | FUTURE_IMPORT_PAREN = ( 136 | """\ 137 | from __future__ import (absolute_import, division, print_function) 138 | unicode("abc") 139 | """, 140 | """\ 141 | from __future__ import (absolute_import, division, print_function) 142 | import six 143 | six.text_type("abc") 144 | """, 145 | ) 146 | 147 | 148 | def test_future_import_as(): 149 | check_on_input(*FUTURE_IMPORT_AS) 150 | 151 | 152 | def test_future_import_as_multiple(): 153 | check_on_input(*FUTURE_IMPORT_AS_MULTIPLE) 154 | 155 | 156 | def test_future_import_paren(): 157 | check_on_input(*FUTURE_IMPORT_PAREN) 158 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | import contextlib 4 | import io 5 | 6 | from utils import check_on_input 7 | 8 | from modernize.__main__ import main as modernize_main 9 | 10 | 11 | def test_list_fixers(): 12 | stdout = io.StringIO() 13 | with contextlib.redirect_stdout(stdout): 14 | returncode = modernize_main(["-l"]) 15 | assert returncode == 0 16 | assert "xrange_six" in stdout.getvalue() 17 | 18 | 19 | def test_nofix_fixers(tmp_path): 20 | stdout = io.StringIO() 21 | stderr = io.StringIO() 22 | with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): 23 | returncode = modernize_main(["--nofix=ham", str(tmp_path)]) 24 | 25 | assert returncode == 2 26 | assert stderr.getvalue() == "Error: fix 'ham' was not found\n" 27 | assert stdout.getvalue() == "" 28 | 29 | 30 | NO_SIX_SAMPLE = """\ 31 | a = range(10) 32 | 33 | class B(object): 34 | __metaclass__ = Meta 35 | """ 36 | 37 | EXPECTED_SIX_RESULT = """\ 38 | from __future__ import absolute_import 39 | import six 40 | from six.moves import range 41 | a = list(range(10)) 42 | 43 | class B(six.with_metaclass(Meta, object)): 44 | pass 45 | """ 46 | 47 | 48 | def test_no_six(): 49 | check_on_input( 50 | NO_SIX_SAMPLE, NO_SIX_SAMPLE, extra_flags=["--no-six"], expected_return_code=0 51 | ) 52 | 53 | 54 | def test_enforce(): 55 | check_on_input( 56 | NO_SIX_SAMPLE, 57 | EXPECTED_SIX_RESULT, 58 | extra_flags=["--enforce"], 59 | expected_return_code=2, 60 | ) 61 | -------------------------------------------------------------------------------- /tests/test_raise_six.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | from utils import check_on_input 4 | 5 | RAISE_TRACEBACK = ( 6 | """\ 7 | raise Exception, value, traceback 8 | """, 9 | """\ 10 | from __future__ import absolute_import 11 | import six 12 | six.reraise(Exception, value, traceback) 13 | """, 14 | ) 15 | 16 | 17 | def test_raise_traceback(): 18 | check_on_input(*RAISE_TRACEBACK) 19 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import generator_stop 2 | 3 | import os.path 4 | import shutil 5 | import tempfile 6 | 7 | from modernize.__main__ import main as modernize_main 8 | 9 | 10 | def check_on_input( 11 | input_content, expected_content, extra_flags=[], expected_return_code=None 12 | ): 13 | """ 14 | Check that input_content is fixed to expected_content, idempotently: 15 | Writes input_content to a temporary file 16 | Runs modernize on it with any extra arguments as given in extra_flags 17 | Runs modernize again with the same arguments, to flush out cumulative effects 18 | (e.g., 'import' fixer isn't triggered until an import exists) 19 | Asserts that the resulting file matches expected_content 20 | Runs modernize again with any extra arguments 21 | Asserts that the final run makes no changes 22 | """ 23 | tmpdirname = tempfile.mkdtemp() 24 | try: 25 | test_input_name = os.path.join(tmpdirname, "input.py") 26 | with open(test_input_name, "wt") as input_file: 27 | input_file.write(input_content) 28 | 29 | def _check(this_input_content, which_check, check_return_code=True): 30 | return_code = modernize_main(extra_flags + ["-w", test_input_name]) 31 | 32 | if check_return_code and expected_return_code is not None: 33 | if expected_return_code != return_code: 34 | raise AssertionError( 35 | "Actual return code: %s\nExpected return code: %s" 36 | % (return_code, expected_return_code) 37 | ) 38 | 39 | # Second pass to deal with cumulative effects that affect 'import' 40 | return_code = modernize_main(extra_flags + ["-w", test_input_name]) 41 | 42 | if check_return_code and expected_return_code is not None: 43 | if expected_return_code != return_code: 44 | raise AssertionError( 45 | "Actual return code: %s\nExpected return code: %s" 46 | % (return_code, expected_return_code) 47 | ) 48 | 49 | output_content = "" 50 | with open(test_input_name) as output_file: 51 | for line in output_file: 52 | if line: 53 | output_content += line 54 | 55 | if output_content != expected_content: 56 | raise AssertionError( 57 | "%s\nInput:\n%sOutput:\n%s\nExpecting:\n%s" 58 | % ( 59 | which_check, 60 | this_input_content, 61 | output_content, 62 | expected_content, 63 | ) 64 | ) 65 | 66 | _check(input_content, "output check failed") 67 | if input_content != expected_content: 68 | _check( 69 | expected_content, "idempotence check failed", check_return_code=False 70 | ) 71 | finally: 72 | shutil.rmtree(tmpdirname) 73 | --------------------------------------------------------------------------------