Skip to content

Commit

Permalink
Merge pull request #55 from browniebroke/cli
Browse files Browse the repository at this point in the history
  • Loading branch information
browniebroke authored May 23, 2020
2 parents f093dda + fb06ad9 commit 6897543
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 68 deletions.
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,34 @@
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

Collections of libCST codemodder to help upgrades to newer versions of Django.
A tool to help upgrade Django projects to newer version of the framework by automatically fixing deprecations.

## Features
## Installation

This is based on [libCST](https://libcst.readthedocs.io/en/latest/index.html) and implements codemods for it. This is currently very limited but the aim is to add more for helping with upcoming deprecations.

Currently implemented codemodders are listed below and grouped by the version of Django where deprecations are removed.
Install this via pip (or your favourite installer):

Not finding what you need? I'm open to contributions, please send me a pull request.
`pip install django-codemod`

### Example of use
## Usage

For example, to fix deprecations removed in Django 4.0:
To fix deprecations removed in Django 4.0:

```bash
python3 -m libcst.tool codemod django_codemod.Django40Command .
djcodemod --removed-in 4.0 .
```

Will go through all the files under your local directory `.` and apply the code modifications to update imports and function calls.
This will go through all the files under your local directory, under `.` and apply code modifications to help upgrading to Django 4.0.

Check out the [documentation](https://django-codemod.readthedocs.io) for more detail on usage and the full list of codemodders.

## How it works

This is based on [libCST](https://libcst.readthedocs.io/en/latest/index.html) and implements codemods for it. This is currently very limited but the aim is to add more for helping with upcoming deprecations.

Codemodders are grouped by the version of Django where a function or feature is removed.

Not finding what you need? I'm open to contributions, please send me a pull request.

## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
Expand Down
150 changes: 150 additions & 0 deletions django_codemod/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import inspect
from abc import ABC
from typing import List

import click
from libcst.codemod import (
CodemodContext,
ContextAwareTransformer,
gather_files,
parallel_exec_transform_with_prettyprint,
)

from django_codemod.commands.base import BaseCodemodCommand
from django_codemod.visitors import django_30, django_40


class VersionParamType(click.ParamType):
"""A type of parameter to parse Versions as arguments."""

name = "version"
example = (
"Should include the major & minor digits of the Django version"
" e.g. '2.2' or '2.2.10'"
)

def convert(self, value, param, ctx):
"""Parse version to keep only major an minor digits."""
try:
return self._parse_unsafe(value, param, ctx)
except TypeError:
self.fail(
f"{value!r} unable to parse version. {self.example}", param, ctx,
)
except ValueError:
self.fail(f"{value!r} is not a valid version. {self.example}", param, ctx)

def _parse_unsafe(self, value, param, ctx):
"""Parse version and validate it's a supported one."""
parsed_version = self._split_digits(value, param, ctx)
if parsed_version not in VERSIONS_MODIFIERS.keys():
supported_versions = ", ".join(
".".join(str(version_part) for version_part in version_tuple)
for version_tuple in VERSIONS_MODIFIERS.keys()
)
self.fail(
f"{value!r} is not supported. "
f"Versions supported: {supported_versions}",
param,
ctx,
)
return parsed_version

def _split_digits(self, value, param, ctx):
"""Split version into 2-tuple of digits, ignoring patch digit."""
values_parts = tuple(int(v) for v in value.split("."))
if len(values_parts) < 2:
self.fail(
f"{value!r} missing version parts. {self.example}", param, ctx,
)
major, minor, *patches = values_parts
return (major, minor)


DJANGO_VERSION = VersionParamType()

VERSIONS_MODIFIERS = {
(3, 0): django_30,
# (3, 1): django_31,
# (3, 2): django_32,
(4, 0): django_40,
}


@click.command()
@click.argument("path")
@click.option(
"--removed-in",
"removed_in",
help="The version of Django to fix deprecations for.",
type=DJANGO_VERSION,
required=True,
)
def djcodemod(removed_in, path):
"""
Automatically fixes deprecations removed Django deprecations.
This command takes the path to target as argument and a version of
Django where a previously deprecated feature is removed.
"""
codemod_modules_list = [VERSIONS_MODIFIERS[removed_in]]
command_instance = build_command(codemod_modules_list)
call_command(command_instance, path)


def build_command(codemod_modules_list: List) -> BaseCodemodCommand:
"""Build a custom command with the list of visitors."""
codemodders_list = []
for codemod_module in codemod_modules_list:
for objname in dir(codemod_module):
try:
obj = getattr(codemod_module, objname)
if (
obj is ContextAwareTransformer
or not issubclass(obj, ContextAwareTransformer)
or inspect.isabstract(obj)
):
continue
# isabstract is broken for direct subclasses of ABC which
# don't themselves define any abstract methods, so lets
# check for that here.
if any(cls[0] is ABC for cls in inspect.getclasstree([obj])):
continue
# Looks like this one is good to go
codemodders_list.append(obj)
except TypeError:
continue

class CustomCommand(BaseCodemodCommand):
transformers = codemodders_list

return CustomCommand(CodemodContext())


def call_command(command_instance: BaseCodemodCommand, path: str):
"""Call libCST with our customized command."""
files = gather_files(path)
try:
# Super simplified call
result = parallel_exec_transform_with_prettyprint(
command_instance,
files,
# Number of jobs to use when processing files. Defaults to number of cores
jobs=None,
)
except KeyboardInterrupt:
raise click.Abort("Interrupted!")

# fancy summary a-la libCST
total = result.successes + result.skips + result.failures
click.echo(f"Finished codemodding {total} files!")
click.echo(f" - Transformed {result.successes} files successfully.")
click.echo(f" - Skipped {result.skips} files.")
click.echo(f" - Failed to codemod {result.failures} files.")
click.echo(f" - {result.warnings} warnings were generated.")
if result.failures > 0:
raise click.exceptions.Exit(1)


if __name__ == "__main__":
djcodemod()
38 changes: 13 additions & 25 deletions docs/codemods.rst
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
List of codemodders
===================

If everything is setup properly, the list of Django Codemods should appear when running libCST's ``list`` command:
Here are the automatic fixes which are supported by django-codemod at this stage:

bash::
Removed in Django 3.0
---------------------

> python3 -m libcst.tool list
django_codemod.Django30Command - Resolve deprecations for removals in Django 3.0.
django_codemod.Django40Command - Resolve deprecations for removals in Django 4.0.
Applied by passing the ``--removed-in 3.0`` option:

Codemodders are organised following the Django `deprecation timeline page`_, listing all its deprecations by version.
- Replaces ``render_to_response()`` by ``render()`` and add ``request=None``
as the first argument of ``render()``.
- Add the ``obj`` argument to ``InlineModelAdmin.has_add_permission()``.

.. _deprecation timeline page: https://docs.djangoproject.com/en/3.0/internals/deprecation/
Removed in Django 4.0
---------------------

Django 3.0
----------
Applied by passing the ``--removed-in 4.0`` option:

This command should fix things `removed in Django 3.0`_.

.. _removed in Django 3.0: https://docs.djangoproject.com/en/dev/internals/deprecation/#deprecation-removed-in-3-0

.. autoclass:: django_codemod.commands.django_codemod.Django30Command
:members:

Django 4.0
----------

This command should fix things `removed in Django 4.0`_.

.. _removed in Django 4.0: https://docs.djangoproject.com/en/dev/internals/deprecation/#deprecation-removed-in-4-0

.. autoclass:: django_codemod.commands.django_codemod.Django40Command
:members:
- Replaces ``force_text`` and ``smart_text`` from the ``django.utils.encoding`` module by ``force_str`` and ``smart_str``
- Replaces ``ugettext``, ``ugettext_lazy``, ``ugettext_noop``, ``ungettext``, and ``ungettext_lazy`` from the ``django.utils.translation`` module by their replacements, respectively ``gettext``, ``gettext_lazy``, ``gettext_noop``, ``ngettext``, and ``ngettext_lazy``.
- Replaces ``django.conf.urls.url`` by ``django.urls.re_path``
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Welcome to Django Codemod's documentation!
==========================================

Collections of libCST codemodder to help upgrades to newer versions of Django.
A tool to help upgrade Django projects to newer version of the framework by automatically fixing deprecations.

.. toctree::
:caption: User Guide
Expand Down
32 changes: 0 additions & 32 deletions docs/usage.md

This file was deleted.

77 changes: 77 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
Usage
=====

Via the ``djcodemod`` command
-----------------------------

This is the preferred way to use this tool, which should fit most of the
use case, via the ``djcodemod`` command line. This command will be
available after installation and is used as follows:

.. code:: shell
$ djcodemod --removed-in <Django version> <path to modify>
- The Django version is the major + minor version of Django, for
example ``3.0``. You may specify the patch version (e.g. ``3.0.5``),
but only the first 2 digits are considered.
- The path may be the root of your project or a specific file. If the
path is a directory, the tool works recursively and will look at all
the files under it.

Using libCST
------------

Unless you already use libCST for something else, you probably don’t
need this. It is less user friendly and requires more configuration than
the CLI.

The codemodders are implemented using libCST and the library provides
commands working nicely with `libCST
codemods <https://libcst.readthedocs.io/en/latest/codemods_tutorial.html#working-with-codemods>`__.

1. If you starting from scratch and never used ``libcst`` in your
project, generate the ``.libcst.codemod.yaml`` config file `as per
the libCST
docs <https://libcst.readthedocs.io/en/latest/codemods_tutorial.html?highlight=modules#setting-up-and-running-codemods>`__:

.. code:: bash
> python3 -m libcst.tool initialize .
You may skip this step if the ``.libcst.codemod.yaml`` file is
already present.

2. Edit the config to add the commands from ``django-codemod`` to your
modules:

.. code:: yaml
# .libcst.codemod.yaml
modules:
- 'django_codemod.commands'
This makes the codemodders from Djnago codecod discoverable by libCST

3. If everything is setup properly, the list of Django Codemods should
appear when running libCST’s ``list`` command:

.. code:: shell
> python3 -m libcst.tool list
django_codemod.Django30Command - Resolve deprecations for removals in Django 3.0.
django_codemod.Django40Command - Resolve deprecations for removals in Django 4.0.
Codemodders are organised following the Django `deprecation timeline
page <https://docs.djangoproject.com/en/3.0/internals/deprecation/>`__,
listing all its deprecations by version.
4. Run libCST with the command from ``django-codemod`` that you want to
apply:
.. code:: bash
> python3 -m libcst.tool codemod django_codemod.Django40Command .
This will apply to code modifications for all the code under ``.``
**in place**. Make sure it’s backed up in source control!
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ flake8-isort==3.0.0
parameterized==0.7.4
pre-commit==2.4.0
pytest-runner==5.2
pytest-mock==3.1.0
pytest==5.4.2
pyupgrade==2.4.3
tox==3.15.1
Expand Down
5 changes: 5 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ include_package_data = true
zip_safe = false
install_requires =
libcst
click
test_require =
pytest>=3

Expand All @@ -44,6 +45,10 @@ include =
django_codemod
django_codemod.*

[options.entry_points]
console_scripts =
djcodemod = django_codemod.cli:djcodemod

[bumpversion:file:django_codemod/__init__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
Expand Down
Loading

0 comments on commit 6897543

Please sign in to comment.