-
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #55 from browniebroke/cli
- Loading branch information
Showing
9 changed files
with
327 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.