Code Monkey home page Code Monkey logo

django-upgrade's Introduction

django-upgrade

image

image

image

image

pre-commit

Automatically upgrade your Django project code.


Improve your code quality with my book Boost Your Django DX which covers using pre-commit, django-upgrade, and many other tools. I wrote django-upgrade whilst working on the book!


Installation

Use pip:

python -m pip install django-upgrade

Python 3.8 to 3.12 supported.

(Python 3.12+ is required to correctly apply fixes within f-strings.)

pre-commit hook

You can also install django-upgrade as a pre-commit hook. Add the following to the repos section of your .pre-commit-config.yaml file (docs), above any code formatters (such as Black):

-   repo: https://github.com/adamchainz/django-upgrade
    rev: ""  # replace with latest tag on GitHub
    hooks:
    -   id: django-upgrade
        args: [--target-version, "5.0"]   # Replace with Django version

Then, upgrade your entire project:

pre-commit run django-upgrade --all-files

Commit any changes. In the process, your other hooks will run, potentially reformatting django-upgrade’s changes to match your project’s code style.

Keep the hook installed in order to upgrade all code added to your project. pre-commit’s autoupdate command will also let you take advantage of future django-upgrade features.

Usage

django-upgrade is a commandline tool that rewrites files in place. Pass your Django version as <major>.<minor> to the --target-version flag and a list of files. django-upgrade’s fixers will rewrite your code to avoid DeprecationWarnings and use some new features.

For example:

django-upgrade --target-version 5.0 example/core/models.py example/settings.py

django-upgrade focuses on upgrading your code and not on making it look nice. Run django-upgrade before formatters like Black.

django-upgrade does not have any ability to recurse through directories. Use the pre-commit integration, globbing, or another technique for applying to many files. Some fixers depend on the names of containing directories to activate, so ensure you run django-upgrade with paths relative to the root of your project. For example, with git ls-files | xargs_:

git ls-files -z -- '*.py' | xargs -0 django-upgrade --target-version 5.0

…or PowerShell’s ForEach-Object__:

git ls-files -- '*.py' | %{django-upgrade --target-version 5.0 $_}

The full list of fixers is documented below.

Options

--target-version

The version of Django to target, in the format <major>.<minor>. django-upgrade enables all of its fixers for versions up to and including the target version.

This option defaults to 2.2, the oldest supported version when this project was created. See the list of available versions with django-upgrade --help.

--exit-zero-even-if-changed

Exit with a zero return code even if files have changed. By default, django-upgrade uses the failure return code 1 if it changes any files, which may stop scripts or CI pipelines.

--only <fixer_name>

Run only the named fixer (names are documented below). The fixer must still be enabled by --target-version. Select multiple fixers with multiple --only options.

For example:

django-upgrade --target-version 5.0 --only admin_allow_tags --only admin_decorators example/core/admin.py

--skip <fixer_name>

Skip the named fixer. Skip multiple fixers with multiple --skip options.

For example:

django-upgrade --target-version 5.0 --skip admin_register example/core/admin.py

--list-fixers

List all available fixers’ names and then exit. All other options are ignored when listing fixers.

For example:

django-upgrade --list-fixers

History

django-codemod is a pre-existing, more complete Django auto-upgrade tool, written by Bruno Alla. Unfortunately its underlying library LibCST is particularly slow, making it annoying to run django-codemod on every commit and in CI.

django-upgrade is an experiment in reimplementing such a tool using the same techniques as the fantastic pyupgrade. The tool leans on the standard library’s ast and tokenize modules, the latter via the tokenize-rt wrapper. This means it will always be fast and support the latest versions of Python.

For a quick benchmark: running django-codemod against a medium Django repository with 153k lines of Python takes 133 seconds. pyupgrade and django-upgrade both take less than 0.5 seconds.

Fixers

All Versions

The below fixers run regardless of the target version.

Versioned blocks

Name: versioned_branches

Removes outdated comparisons and blocks from if statements comparing to django.VERSION. Supports comparisons of the form:

if django.VERSION <comparator> (<X>, <Y>):
    ...

Where <comparator> is one of <, <= , >, or >=, and <X> and <Y> are integer literals. A single else block may be present, but elif is not supported.

-if django.VERSION < (4, 1):
-    class RenameIndex:
-        ...

-if django.VERSION >= (4, 1):
-    constraint.validate()
-else:
-    custom_validation(constraint)
+constraint.validate()

See also pyupgrade’s similar feature that removes outdated code from checks on the Python version.

Django 1.7

Release Notes

Admin model registration

Name: admin_register

Rewrites admin.site.register() calls to the new @admin.register()_ decorator syntax when eligible. This only applies in files that use from django.contrib import admin or from django.contrib.gis import admin.

from django.contrib import admin
[email protected](MyModel1, MyModel2)
class MyCustomAdmin(admin.ModelAdmin):

...

-admin.site.register(MyModel1, MyCustomAdmin) -admin.site.register(MyModel2, MyCustomAdmin)

This also works with custom admin sites. Such calls are detected heuristically based on three criteria:

  1. The object whose register() method is called has a name ending with site.
  2. The registered class has a name ending with Admin.
  3. The filename has the word admin somewhere in its path.
from myapp.admin import custom_site
from django.contrib import admin

+@admin.register(MyModel)
+@admin.register(MyModel, site=custom_site)
class MyModelAdmin(admin.ModelAdmin):
    pass

-custom_site.register(MyModel, MyModelAdmin)
-admin.site.register(MyModel, MyModelAdmin)

If a register() call is preceded by an unregister() call that includes the same model, it is ignored.

from django.contrib import admin


class MyCustomAdmin(admin.ModelAdmin):
    ...


admin.site.unregister(MyModel1)
admin.site.register(MyModel1, MyCustomAdmin)

Compatibility imports

Rewrites some compatibility imports:

  • django.contrib.admin.helpers.ACTION_CHECKBOX_NAME in django.contrib.admin
  • django.template.context.BaseContext, django.template.context.Context, django.template.context.ContextPopException and django.template.context.RequestContext in django.template.base
-from django.contrib.admin import ACTION_CHECKBOX_NAME
+from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME

-from django.template.base import Context
+from django.template.context import Context

Django 1.9

Release Notes

on_delete argument

Name: on_delete

Add on_delete=models.CASCADE to ForeignKey and OneToOneField:

from django.db import models

-models.ForeignKey("auth.User") +models.ForeignKey("auth.User", on_delete=models.CASCADE)

-models.OneToOneField("auth.User") +models.OneToOneField("auth.User", on_delete=models.CASCADE)

This fixer also support from-imports:

-from django.db.models import ForeignKey
+from django.db.models import CASCADE, ForeignKey

-ForeignKey("auth.User")
+ForeignKey("auth.User", on_delete=CASCADE)

DATABASES

Name: settings_database_postgresql

Update the DATABASES setting backend path django.db.backends.postgresql_psycopg2 to use the renamed version django.db.backends.postgresql.

Settings files are heuristically detected as modules with the whole word “settings” somewhere in their path. For example myproject/settings.py or myproject/settings/production.py.

DATABASES = {
    "default": {
  • "ENGINE": "django.db.backends.postgresql_psycopg2",
+ "ENGINE": "django.db.backends.postgresql",

"NAME": "mydatabase", "USER": "mydatabaseuser", "PASSWORD": "mypassword", "HOST": "127.0.0.1", "PORT": "5432",

}

}

Compatibility imports

Name: compatibility_imports

Rewrites some compatibility imports:

  • django.forms.utils.pretty_name in django.forms.forms
  • django.forms.boundfield.BoundField in django.forms.forms
  • django.forms.widgets.SelectDateWidget in django.forms.extras

Whilst mentioned in the Django 3.1 release notes, these have been possible since Django 1.9.

-from django.forms.forms import pretty_name
+from django.forms.utils import pretty_name

Django 1.10

Release Notes

request.user boolean attributes

Name: request_user_attributes

Rewrites calls to request.user.is_authenticated() and request.user.is_anonymous() to remove the parentheses, per the deprecation.

-request.user.is_authenticated()
+request.user.is_authenticated

-self.request.user.is_anonymous()
+self.request.user.is_anonymous

Compatibility imports

Rewrites some compatibility imports:

  • django.templatetags.static.static in django.contrib.staticfiles.templatetags.staticfiles

    (Whilst mentioned in the Django 2.1 release notes, this has been possible since Django 1.10.)

  • django.urls.* in django.core.urlresolvers.*
-from django.contrib.staticfiles.templatetags.staticfiles import static
+from django.templatetags.static import static

-from django.core.urlresolvers import reverse
+from django.urls import reverse

-from django.core.urlresolvers import resolve
+from django.urls import resolve

Django 1.11

Release Notes

Compatibility imports

Name: compatibility_imports

Rewrites some compatibility imports:

  • django.core.exceptions.EmptyResultSet in django.db.models.query, django.db.models.sql, and django.db.models.sql.datastructures
  • django.core.exceptions.FieldDoesNotExist in django.db.models.fields

Whilst mentioned in the Django 3.1 release notes, these have been possible since Django 1.11.

-from django.db.models.query import EmptyResultSet
+from django.core.exceptions import EmptyResultSet

-from django.db.models.fields import FieldDoesNotExist
+from django.core.exceptions import FieldDoesNotExist

Django 2.0

Release Notes

URL’s

Name: django_urls

Rewrites imports of include() and url() from django.conf.urls to django.urls. url() calls using compatible regexes are rewritten to the new path() syntax_, otherwise they are converted to call re_path().

-from django.conf.urls import include, url
+from django.urls import include, path, re_path

 urlpatterns = [
-    url(r'^$', views.index, name='index'),
+    path('', views.index, name='index'),
-    url(r'^about/$', views.about, name='about'),
+    path('about/', views.about, name='about'),
-    url(r'^post/(?P<slug>[-a-zA-Z0-9_]+)/$', views.post, name='post'),
+    path('post/<slug:slug>/', views.post, name='post'),
-    url(r'^weblog', include('blog.urls')),
+    re_path(r'^weblog', include('blog.urls')),
 ]

Existing re_path() calls are also rewritten to the path() syntax when eligible.

-from django.urls import include, re_path
+from django.urls import include, path, re_path

 urlpatterns = [
-    re_path(r'^about/$', views.about, name='about'),
+    path('about/', views.about, name='about'),
     re_path(r'^post/(?P<slug>[\w-]+)/$', views.post, name='post'),
 ]

The compatible regexes that will be converted to use path converters are the following:

  • [^/]+str
  • [0-9]+int
  • [-a-zA-Z0-9_]+slug
  • [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}uuid
  • .+path

These are taken from the path converter classes.

For some cases, this change alters the type of the arguments passed to the view, from str to the converted type (e.g. int). This is not guaranteed backwards compatible: there is a chance that the view expects a string, rather than the converted type. But, pragmatically, it seems 99.9% of views do not require strings, and instead work with either strings or the converted type. Thus, you should test affected paths after this fixer makes any changes.

Note that [\w-] is sometimes used for slugs, but is not converted because it might be incompatible. That pattern matches all Unicode word characters, such as “α”, unlike Django's slug converter, which only matches Latin characters.

lru_cache

Name: compatibility_imports

Rewrites imports of lru_cache from django.utils.functional to use functools.

-from django.utils.functional import lru_cache
+from functools import lru_cache

ContextDecorator

Rewrites imports of ContextDecorator from django.utils.decorators to use contextlib.

-from django.utils.decorators import ContextDecorator
+from contextlib import ContextDecorator

<func>.allow_tags = True

Name: admin_allow_tags

Removes assignments of allow_tags attributes to True. This was an admin feature to allow display functions to return HTML without marking it as unsafe, deprecated in Django 1.9. In practice, most display functions that return HTML already use format_html()_ or similar, so the attribute wasn’t necessary. This only applies in files that use from django.contrib import admin or from django.contrib.gis import admin.

from django.contrib import admin

def upper_case_name(obj):
    ...

-upper_case_name.allow_tags = True

Django 2.2

Release Notes

HttpRequest.headers

Name: request_headers

Rewrites use of request.META to read HTTP headers to instead use request.headers_. Header lookups are done in lowercase per the HTTP/2 specification.

-request.META['HTTP_ACCEPT_ENCODING']
+request.headers['accept-encoding']

-self.request.META.get('HTTP_SERVER', '')
+self.request.headers.get('server', '')

-request.META.get('CONTENT_LENGTH')
+request.headers.get('content-length')

-"HTTP_SERVER" in request.META
+"server" in request.headers

QuerySetPaginator

Name: queryset_paginator

Rewrites deprecated alias django.core.paginator.QuerySetPaginator to Paginator.

-from django.core.paginator import QuerySetPaginator
+from django.core.paginator import Paginator

-QuerySetPaginator(...)
+Paginator(...)

FixedOffset

Name: timezone_fixedoffset

Rewrites deprecated class FixedOffset(x, y)) to timezone(timedelta(minutes=x), y)

Known limitation: this fixer will leave code broken with an ImportError if FixedOffset is called with only *args or **kwargs.

-from django.utils.timezone import FixedOffset
-FixedOffset(120, "Super time")
+from datetime import timedelta, timezone
+timezone(timedelta(minutes=120), "Super time")

FloatRangeField

Name: postgres_float_range_field

Rewrites model and form fields using FloatRangeField to DecimalRangeField, from the relevant django.contrib.postgres modules.

from django.db.models import Model

-from django.contrib.postgres.fields import FloatRangeField +from django.contrib.postgres.fields import DecimalRangeField

class MyModel(Model):

  • my_field = FloatRangeField("My range of numbers")
  • my_field = DecimalRangeField("My range of numbers")

TestCase class database declarations

Name: testcase_databases

Rewrites the allow_database_queries and multi_db attributes of Django’s TestCase classes to the new databases attribute. This only applies in test files, which are heuristically detected as files with either “test” or “tests” somewhere in their path.

Note that this will only rewrite to databases = [] or databases = "__all__". With multiple databases you can save some test time by limiting test cases to the databases they require (which is why Django made the change).

from django.test import SimpleTestCase

class MyTests(SimpleTestCase):
  • allow_database_queries = True
  • databases = "__all"
    def test_something(self):

    self.assertEqual(2 * 2, 4)

Django 3.0

Release Notes

django.utils.encoding aliases

Name: utils_encoding

Rewrites smart_text() to smart_str(), and force_text() to force_str().

-from django.utils.encoding import force_text, smart_text
+from django.utils.encoding import force_str, smart_str


-force_text("yada")
-smart_text("yada")
+force_str("yada")
+smart_str("yada")

django.utils.http deprecations

Name: utils_http:

Rewrites the urlquote(), urlquote_plus(), urlunquote(), and urlunquote_plus() functions to the urllib.parse versions. Also rewrites the internal function is_safe_url() to url_has_allowed_host_and_scheme().

-from django.utils.http import urlquote
+from urllib.parse import quote

-escaped_query_string = urlquote(query_string)
+escaped_query_string = quote(query_string)

django.utils.text deprecation

Name: utils_text

Rewrites unescape_entities() with the standard library html.escape().

-from django.utils.text import unescape_entities
+import html

-unescape_entities("some input string")
+html.escape("some input string")

django.utils.translation deprecations

Name: utils_translation

Rewrites the ugettext(), ugettext_lazy(), ugettext_noop(), ungettext(), and ungettext_lazy() functions to their non-u-prefixed versions.

-from django.utils.translation import ugettext as _, ungettext
+from django.utils.translation import gettext as _, ngettext

-ungettext("octopus", "octopodes", n)
+ngettext("octopus", "octopodes", n)

Django 3.1

Release Notes

JSONField

Name: compatibility_imports

Rewrites imports of JSONField and related transform classes from those in django.contrib.postgres to the new all-database versions. Ignores usage in migration files, since Django kept the old class around to support old migrations. You will need to make migrations after this fix makes changes to models.

-from django.contrib.postgres.fields import JSONField
+from django.db.models import JSONField

PASSWORD_RESET_TIMEOUT_DAYS

Name: password_reset_timeout_days

Rewrites the setting PASSWORD_RESET_TIMEOUT_DAYS to PASSWORD_RESET_TIMEOUT, adding the multiplication by the number of seconds in a day.

Settings files are heuristically detected as modules with the whole word “settings” somewhere in their path. For example myproject/settings.py or myproject/settings/production.py.

-PASSWORD_RESET_TIMEOUT_DAYS = 4
+PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 * 4

Signal

Name: signal_providing_args

Removes the deprecated documentation-only providing_args argument.

from django.dispatch import Signal

-my_cool_signal = Signal(providing_args=["documented", "arg"]) +my_cool_signal = Signal()

get_random_string

Name: crypto_get_random_string

Injects the now-required length argument, with its previous default 12.

from django.utils.crypto import get_random_string

-key = get_random_string(allowed_chars="01234567899abcdef") +key = get_random_string(length=12, allowed_chars="01234567899abcdef")

NullBooleanField

Name: null_boolean_field

Transforms the NullBooleanField() model field to BooleanField(null=True). Applied only in model files, not migration files, since Django kept the old class around to support old migrations. You will need to make migrations after this fix makes changes to models.

-from django.db.models import Model, NullBooleanField
+from django.db.models import Model, BooleanField

 class Book(Model):
-    valuable = NullBooleanField("Valuable")
+    valuable = BooleanField("Valuable", null=True)

ModelMultipleChoiceField

Name: forms_model_multiple_choice_field

Replace list error message key with list_invalid on forms ModelMultipleChoiceField.

-forms.ModelMultipleChoiceField(error_messages={"list": "Enter multiple values."})
+forms.ModelMultipleChoiceField(error_messages={"invalid_list": "Enter multiple values."})

Django 3.2

Release Notes

@admin.action()

Name: admin_decorators

Rewrites functions that have admin action attributes assigned to them to use the new @admin.action() decorator_. This only applies in files that use from django.contrib import admin or from django.contrib.gis import admin.

from django.contrib import admin

# Module-level actions:

[email protected]( + description="Publish articles", +) def make_published(modeladmin, request, queryset): ...

-make_published.short_description = "Publish articles"

# …and within classes:

@admin.register(Book) class BookAdmin(admin.ModelAdmin):

  • @admin.action(
  • description="Unpublish articles",
  • permissions=("unpublish",),
  • ) def make_unpublished(self, request, queryset): ...
  • make_unpublished.allowed_permissions = ("unpublish",)
  • make_unpublished.short_description = "Unpublish articles"

@admin.display()

Name: admin_decorators

Rewrites functions that have admin display attributes assigned to them to use the new @admin.display() decorator_. This only applies in files that use from django.contrib import admin or from django.contrib.gis import admin.

from django.contrib import admin

# Module-level display functions:

[email protected]( + description="NAME", +) def upper_case_name(obj): ...

-upper_case_name.short_description = "NAME"

# …and within classes:

@admin.register(Book) class BookAdmin(admin.ModelAdmin):

  • @admin.display(
  • description='Is Published?',
  • boolean=True,
  • ordering='-publish_date',
  • ) def is_published(self, obj): ...
  • is_published.boolean = True
  • is_published.admin_order_field = '-publish_date'
  • is_published.short_description = 'Is Published?'

BaseCommand.requires_system_checks

Name: management_commands

Rewrites the requires_system_checks attributes of management command classes from bools to "__all__" or [] as appropriate. This only applies in command files, which are heuristically detected as files with management/commands somewhere in their path.

from django.core.management.base import BaseCommand

class Command(BaseCommand):
  • requires_system_checks = True
  • requires_system_checks = "__all__"

class SecondCommand(BaseCommand):

  • requires_system_checks = False
  • requires_system_checks = []

EmailValidator

Name: email_validator

Rewrites the whitelist keyword argument to its new name allowlist.

from django.core.validators import EmailValidator

-EmailValidator(whitelist=["example.com"]) +EmailValidator(allowlist=["example.com"])

default_app_config

Name: default_app_config

Removes module-level default_app_config assignments from __init__.py files:

-default_app_config = 'my_app.apps.AppConfig'

Django 4.0

Release Notes

USE_L10N

Name: use_l10n

Removes the deprecated USE_L10N setting if set to its default value of True.

Settings files are heuristically detected as modules with the whole word “settings” somewhere in their path. For example myproject/settings.py or myproject/settings/production.py.

-USE_L10N = True

lookup_needs_distinct

Name: admin_lookup_needs_distinct

Renames the undocumented django.contrib.admin.utils.lookup_needs_distinct to lookup_spawns_duplicates:

-from django.contrib.admin.utils import lookup_needs_distinct
+from django.contrib.admin.utils import lookup_spawns_duplicates

-if lookup_needs_distinct(self.opts, search_spec):
+if lookup_spawns_duplicates(self.opts, search_spec):
    ...

Compatibility imports

Rewrites some compatibility imports:

  • django.utils.translation.template.TRANSLATOR_COMMENT_MARK in django.template.base
-from django.template.base import TRANSLATOR_COMMENT_MARK
+from django.utils.translation.template import TRANSLATOR_COMMENT_MARK

Django 4.1

Release Notes

django.utils.timezone.utc deprecations

Name: utils_timezone

Rewrites imports of django.utils.timezone.utc to use datetime.timezone.utc. Requires an existing import of the datetime module.

import datetime

-from django.utils.timezone import utc

-calculate_some_datetime(utc) +calculate_some_datetime(datetime.timezone.utc)

import datetime as dt
from django.utils import timezone

-do_a_thing(timezone.utc) +do_a_thing(dt.timezone.utc)

assertFormError() and assertFormsetError()

Name: assert_form_error

Rewrites calls to these test case methods from the old signatures to the new ones.

-self.assertFormError(response, "form", "username", ["Too long"])
+self.assertFormError(response.context["form"], "username", ["Too long"])

-self.assertFormError(response, "form", "username", None)
+self.assertFormError(response.context["form"], "username", [])

-self.assertFormsetError(response, "formset", 0, "username", ["Too long"])
+self.assertFormsetError(response.context["formset"], 0, "username", ["Too long"])

-self.assertFormsetError(response, "formset", 0, "username", None)
+self.assertFormsetError(response.context["formset"], 0, "username", [])

Django 4.2

Release Notes

STORAGES setting

Name: settings_storages

Combines deprecated settings DEFAULT_FILE_STORAGE and STATICFILES_STORAGE into the new STORAGES setting, within settings files. Only applies if all old settings are defined as strings, at module level, and a STORAGES setting hasn’t been defined.

Settings files are heuristically detected as modules with the whole word “settings” somewhere in their path. For example myproject/settings.py or myproject/settings/production.py.

-DEFAULT_FILE_STORAGE = "example.storages.ExtendedFileSystemStorage"
-STATICFILES_STORAGE = "example.storages.ExtendedS3Storage"
+STORAGES = {
+    "default": {
+        "BACKEND": "example.storages.ExtendedFileSystemStorage",
+    },
+    "staticfiles": {
+        "BACKEND": "example.storages.ExtendedS3Storage",
+    },
+}

If the module has a from ... import * with a module path mentioning “settings”, django-upgrade makes an educated guess that a base STORAGES setting is imported from there. It then uses ** to extend that with any values in the current module:

from example.settings.base import *

-DEFAULT_FILE_STORAGE = "example.storages.S3Storage" +STORAGES = { + **STORAGES, + "default": { + "BACKEND": "example.storages.S3Storage", + }, +}

Test client HTTP headers

Name: test_http_headers

Transforms HTTP headers from the old WSGI kwarg format to use the new headers dictionary, for:

  • Client method like self.client.get()
  • Client instantiation
  • RequestFactory instantiation

Requires Python 3.9+ due to changes in ast.keyword.

-response = self.client.get("/", HTTP_ACCEPT="text/plain")
+response = self.client.get("/", headers={"accept": "text/plain"})

 from django.test import Client
-Client(HTTP_ACCEPT_LANGUAGE="fr-fr")
+Client(headers={"accept-language": "fr-fr"})

 from django.test import RequestFactory
-RequestFactory(HTTP_USER_AGENT="curl")
+RequestFactory(headers={"user-agent": "curl"})

assertFormsetError and assertQuerysetEqual

Name: assert_set_methods

Rewrites calls to these test case methods from the old names to the new ones with capitalized “Set”.

-self.assertFormsetError(response.context["form"], "username", ["Too long"])
+self.assertFormSetError(response.context["form"], "username", ["Too long"])

-self.assertQuerysetEqual(authors, ["Brad Dayley"], lambda a: a.name)
+self.assertQuerySetEqual(authors, ["Brad Dayley"], lambda a: a.name)

Django 5.0

Release Notes

No fixers yet.

django-upgrade's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-upgrade's Issues

Add `@admin.action` and `@admin.display`

Python Version

No response

Django Version

No response

Package Version

No response

Description

Find functions with appropriate attributes being set and rewrite them to use @admin.action or @admin.display respectively

https://docs.djangoproject.com/en/3.2/ref/contrib/admin/actions/#django.contrib.admin.action
https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#django.contrib.admin.display

This is going to be a little tricky to do as multiple AST nodes will need inspecting and rewriting at once, if they all match.

We might need to visit class definitions and modules, and check all the function definitions within each + following assignments to figure out everything.

Steps:

  • module-level action functions - #178
  • class-level action functions - #197
  • module-level and class-level display functions - #199

Fix update_imports to not leave a trailing comma

Python Version

No response

Django Version

No response

Package Version

No response

Description

Take:

from a import b, c, d

Call update_imports on it to remove imports c and d.

One will be left with:

from a import b,

which is a syntax error.

Issue with url function replacement

Python Version

3.7

Django Version

2.2

Package Version

1.4.0

Description

Django-upgrade changes this kind of url functions:
url(r"^foo", my_view)

with these path functions:
path("foo", my_view)

However, the automated change made by the library is not backwards compatible. The initial code allowed all paths such as foos, footest etc., whereas the upgraded code only allows foo (the missing $ in the first example being the problem).

For this use-case, a backwards compatible solution would have been this:
re_path(r"^foo", my_view)

I would be glad to offer any help needed if you would take this issue into consideration.

Fixing deprecated code within virtual env

Description

Could you please implement accessing and fixing the deprecated import within the virtual envirement ?
there are some apps still have deprecated import for Django 4.0 I had to update them manually .
Appreciated .

Rewrite re_path() calls into path()

For people who did find/replace of url() to re_path(), we could help by applying the same rewriting logic of url() to convert re_path() to path()

This could get a little awkward with removing re_path() if it's no longer needed, tracking if that's needed.

ModuleNotFoundError: No module named 'datetime.timezone'; 'datetime' is not a package

Python Version

3.10

Django Version

4.1

Package Version

1.8.0

Description

After running the new 1.8.0 my project now throws this error:

>>> from datetime.timezone import utc
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'datetime.timezone'; 'datetime' is not a package

I guess #169 was a bit too eager since timezone.utc is just an attribute on timezone and thus not importable.

How to use django-upgrade in the Pipeline?

Python Version

3.10

Django Version

4.1.1

Package Version

1.10.0

Description

Hi there!

I installed your great package - thx for the update on DjangoCon! - and it works great with pre-commit. Additionally, I want to ensure via the pipeline that nobody smuggles in some deprecated code... but the best way I found was using pre-commit inside the pipeline step which feels a little off. If I run it directly, the tool will rewrite the files but not return error code 1 in case it did something.

Solution (on python-slim image):

    - apt-get update && apt-get install --no-install-recommends -y git
    - pip install pre-commit
    - pre-commit install -t pre-push -t pre-commit --install-hooks
    - pre-commit run --all-files --hook-stage push django-upgrade

Not working solution (on python-slim image):

    - apt-get update && apt-get install --no-install-recommends -y git
    - pip install django-upgrade
    - git ls-files -- '*.py' | xargs django-upgrade --target-version 4.1

Thx!

urlquote / f-string issue

Python Version

3.8

Django Version

3.2.9

Package Version

1.4

Description

django-upgrade changed this import:

-from django.utils.http import urlquote
+from urllib.parse import quote

But then failed to change usages of urlquote to quote. The only usage in my case was inside an f-string, don't know if that is relevant.

I'll put more effort into a reproducible test case if needed

`request.headers.get("Content-Length")` and `request.headers.get("Content-Type")`

Python Version

3.10

Django Version

4.0

Package Version

1.10

Description

Hi! Thanks for your work on this project, I wish I'd found it sooner! I was just applying the fixers, and noticed that two HTTP headers aren't 'fixed' to user request.headers.get():

  • CONTENT_LENGTH
  • CONTENT_TYPE

I was going to write a PR to fix this, but noticed that you have a test specifically ensuring that these are not fixed:

def test_not_header_access():
    check_noop(
        """\
        request.META['CONTENT_LENGTH']
        """,
        settings,
    )

Can you help me understand why these aren't fixed? I may be misunderstanding the docs:

HttpRequest.headers
A case insensitive, dict-like object that provides access to all HTTP-prefixed headers (plus Content-Length and Content-Type) from the request.

Happy to help if you'd like these added to the fixer, but my gut feeling is that these were excluded intentionally and I'm misunderstanding something. Cheers.

IndexError: list index out of range

Python Version

3.9.4

Django Version

3.2.7

Package Version

1.3.0

Description

Traceback (most recent call last):
  File "/Users/mike/.cache/pre-commit/repopgjsi47x/py_env-python3.9/bin/django-upgrade", line 8, in <module>
    sys.exit(main())
  File "/Users/mike/.cache/pre-commit/repopgjsi47x/py_env-python3.9/lib/python3.9/site-packages/django_upgrade/main.py", line 50, in main
    ret |= fix_file(
  File "/Users/mike/.cache/pre-commit/repopgjsi47x/py_env-python3.9/lib/python3.9/site-packages/django_upgrade/main.py", line 76, in fix_file
    contents_text = apply_fixers(contents_text, settings, filename)
  File "/Users/mike/.cache/pre-commit/repopgjsi47x/py_env-python3.9/lib/python3.9/site-packages/django_upgrade/main.py", line 114, in apply_fixers
    callback(tokens, i)
  File "/Users/mike/.cache/pre-commit/repopgjsi47x/py_env-python3.9/lib/python3.9/site-packages/django_upgrade/tokens.py", line 227, in update_import_names
    j = find(tokens, j, name=NAME, src=alias.name)
  File "/Users/mike/.cache/pre-commit/repopgjsi47x/py_env-python3.9/lib/python3.9/site-packages/django_upgrade/tokens.py", line 26, in find
    while tokens[i].name != name or (src is not None and tokens[i].src != src):
IndexError: list index out of range

Rewrite `admin.site.register()` to `@admin.register()`

Description

Description

We could rewrite admin.site.register() to the more elegant @admin.register() syntax :

+@admin.register(MyModel)
class MyCustomAdmin:
    pass

-admin.site.register(MyModel, MyCustomAdmin)

This syntax was actually added a while ago (Django1.7).
I usually find it a bit easier to use since the Model and associated Custom Admin definition stay really close.

Potential Issue

There is a known issue when using a python 2 style super call in the __init__ method. The fixer should not rewrite in this case.

You can’t use this decorator if you have to reference your model admin class in its __init__() method, e.g. super(PersonAdmin, self).__init__(*args, **kwargs). You can use super().__init__(*args, **kwargs).

Let me know if this is something you'll be interested in, I'll be happy to submit a PR.
Cheers

Add --version flag

Description

It would be useful to have a --version flag to know what version is the application running.

I am working on a PR right now to implement this feature.

Rewrite compatible `re_path()` to the new `path()` syntax

Description

Hi,

I came here after discovering pyupgrade, these are fascinating projects, really like it !

I created this issue to suggest a slight improvement of this feature:

url() calls using compatible regexes are rewritten to the new path() syntax, otherwise they are converted to call re_path()

This is nice and could also be useful to convert compatible re_path() to simpler path() syntax.
It would benefit projects that handled the deprecation by simply replacing url() calls with re_path() calls directly. It's also usually nice to use the path() syntax instead of unnecessary regexes when possible.

I'm working on a PR for this, let me know if it's something you would be interested in.
Cheers

Templates - ifequal / ifnotequal

Django 3.1 deprecated these template tags, it would be nice if we could rewrite them.

We could expand to support html files, detecting file type with identify, and assuming Python by default.

For these two deprecated tags, we should be able to rewrite them with regexes. The only thing is to skip over any verbatim blocks. I imagine the structure will be a general splitter that feeds non-verbatim blocks into regex replacer functions. Alternatively we could use a tokenizer like django's own.

Tidy `find_final_token()`

Description

  • Fix off-by-one - it current returns token after last token
  • Rename to first_token / last_token

Supported python versions

Python Version

n/a

Django Version

n/a

Package Version

1.0.0

Description

Hi!

Thanks for creating this!

I just had a quick question about your supported Python versions:

    Programming Language :: Python :: 3.6
    Programming Language :: Python :: 3.7

Since you utilize end_lineno, here, I think you'll have troubles on Python versions earlier than 3.8, since I believe 3.8 was when end_lineno was added (see this).

Just wanted to open this in case you were not aware of this yet - or even better, that you are aware of some backport that enables you to use end_lineno with earlier Python versions, that I don't know about; If that exists please let me know!

Combine contrib.postgres fixers

Python Version

No response

Django Version

No response

Package Version

No response

Description

They currently will fight each other - if both rewrite imports for the same module, the second run of update_import will fail as it won't find the required tokens.

HttpRequest.headers fixer produces unsupported assigments in tests

Python Version

3.8

Django Version

3.1

Package Version

1.3.1

Description

I ran django-upgrade on our codebase and it replaced some places where we were setting request.META to a fake requests created by RequestFactory in our tests, which isn't correct.

I guess django-upgrade shouldn't change these if they are on the left hand side of an assignment.

Here is an example:

  request = RequestFactory().get('/')
- request.META['HTTP_USER_AGENT'] = user_agent
+ request.headers['User-Agent'] = user_agent

When running tests, it causes: TypeError: 'HttpHeaders' object does not support item assignment.

known workarounds

  1. Rename the variable to something else than request, e.g. req

  2. In this specific example, I was able to avoid django-upgrade to touch our code if I rewrite it with:

    - request = RequestFactory().get('/')
    - request.META['HTTP_USER_AGENT'] = user_agent 
    + request = RequestFactory(HTTP_USER_AGENT=user_agent).get('/')

Make `re_path` -> `path` fixer convert `include()`s with unterminated regexes

Python Version

3.10.7

Django Version

4.1.2

Package Version

1.10.0

Description

Not sure if this is a bug strictly speaking or a feature request, but after running this package with --target-version 4.1 the fixer that converts re_path usage to path misses cases which should result in a direct translation.

An example from our codebase is
re_path(r"^accounts/", include("allauth.urls")),

In the current version this line is left unmodified.

Presumably something like this could be automatically converted to
path("accounts/", include("allauth.urls")),

Synergy with django-codemod

Hey Adam! Just noticed that you started this project 😀

django-codemod is a pre-existing, more complete Django auto-upgrade tool, written by Bruno Alla. Unfortunately its underlying library LibCST is particularly slow, making it annoying to run django-codemod on every commit and in CI

I've always been bothered by django-codemod' speed, and despite my efforts to speed it up, it became obvious that libCST is a major limiting factor. I was also considering a rework of the CLI option to accept a min/max version, but reading your approach, the --target-version from django-upgrade makes a lot of sense! Also the benchmark looks fantastic!

Anyway, all of that to say that django-upgrade looks like what I wanted to make of django-codemod, and rather than spending a lot of time in changing django-codemod, it might make more sense to be directing my efforts to your project.

Are you interested in porting some of django-codemod's fixers for older Django versions? Another approach would be to make the 2 projects complementary, leaving older fixes in django-codemod and focusing on newer Django version in django-upgrade.

(This is probably more of a discussion that an issue)

Django 4.1 fixer: `assertFormError` / `assertFormsetError`

Description

New feature: https://docs.djangoproject.com/en/4.1/releases/4.1/#tests

With old form deprecated: https://docs.djangoproject.com/en/4.1/releases/4.1/#id2

Passing a response object and a form/formset name to SimpleTestCase.assertFormError() and assertFormsetError() is deprecated. Use:

assertFormError(response.context['form_name'], …)
assertFormsetError(response.context['formset_name'], …)

or pass the form/formset object directly instead.

So basically we want to do:

-self.assertFormError(response, "form", ["too long"])
+self.assertFormError(response.context["form"], ["too long"])

combine first two arguments into one

Bonus:

The undocumented ability to pass errors=None to SimpleTestCase.assertFormError() and assertFormsetError() is deprecated. Use errors=[] instead.

  • assertFormError with pos args - #210
  • try detect responses from client - #212
  • also try detect responses from async client - #213
  • assertFormsetError with pos args - #214
  • Switch errors=None to [] (pos args) - #216
  • Keyword arg support (usage probably a lot rarer) - #216

Undesired rewriting of import statements (pre-commit loop)

Python Version

3.8

Django Version

2.2

Package Version

main

Description

I need some help triaging this behavior, as I'm not familiar with the patterns that django-upgrade uses to detect and alter import statements.

While running django-upgrade on a project, I have the following import statement generated:

from django.urls import include, path, re_path

However, if my project is using reorder_python_imports, it will split up this line into 3 import statements:

from django.urls import include
from django.urls import path
from django.urls import re_path

Now, if I run the django-upgrade hook again, the former line reappears like this:

from django.urls import include
from django.urls import path
from django.urls import include, path, re_path

You can add the latest version of reorder_python_imports with this configuration:

-   repo: https://github.com/asottile/reorder_python_imports
    rev: v3.8.3
    hooks:
    -   id: reorder-python-imports
        language_version: python3

Are you able to immediately grasp the issue @adamchainz or does it need to be isolated into a test project / test case?

The correct behavior?

I think that what I would expect from django-upgrade is that it generates whatever import statements that it needs, but that subsequent linting from black, flake8 or reorder_python_imports is left in place and doesn't result in any new changes.

Rewrite translated URL's

It's theoretically possible to rewrite old url() calls using translation to the new path format, e.g.

-from django.conf.urls import url
+from django.urls import re_path
 from django.utils.translation import gettext_lazy as _

-url(_(r'^about/$'), views.about)
+path(_(r'about/'), views.about)

State.from_imports would need extending to track aliases as it currently ignores them.

Rewrite get_random_string bug

Python Version

3.9.4

Django Version

3.2.7

Package Version

1.2.0

Description

     def save(self, **kwargs):
         if not self.token:
-            self.token = crypto.get_random_string(12)
+            self.token = crypto.get_random_string(length=1212)
         super().save(**kwargs)

NullBooleanField to BooleanField upgrade issue when null=True parameter is present

Python Version

3.8.19

Django Version

2.2.28

Package Version

21479d3

Description

While working on Sentry running latest django-upgrade one of the change on model assistant failed.

diff --git a/src/sentry/models/assistant.py b/src/sentry/models/assistant.py
index 896dd70b03..3cea21dd09 100644
--- a/src/sentry/models/assistant.py
+++ b/src/sentry/models/assistant.py
@@ -23,7 +23,7 @@ class AssistantActivity(Model):
     # Time the user dismissed the guide. If this is set, viewed_ts will be null.
     dismissed_ts = models.DateTimeField(null=True)
     # Whether the user found the guide useful.
-    useful = models.NullBooleanField(null=True)
+    useful = models.BooleanField(null=True, null=True)
 
     __repr__ = sane_repr("user", "guide_id", "viewed_ts", "dismissed_ts", "useful")

Seems to be an issue when converting NullBooleanField to BooleanField.

Require at least one filename

Description

Currently running without any arguments does nothing, with no output:

$ django-upgrade

Kinda useless.

The filename arg should be updated to require at least one filename.

NullBooleanField will also fix earlier migrations

Python Version

3.9.4

Django Version

3.2.6

Package Version

1.3.2

Description

I wrote my code in earlier times using NullBooleanField, then recently I updated to 3.2 and already made the changes to the fields, also adding a new version. The code is IMHO "fixed".

Now I've added the django-upgrade check and it's trying to rewrite my earlier migrations. I guess it is not intended this way or should at least be documented.

How would you handle such a case? Some might argue that a Django version bump is a good point to reset migrations...

Extensions to `@admin.register` fixer

Description

Following #182 / #189 , there are a few more cases that could be covered with further work.

  1. Support kwarg form ✅ #192
+@admin.register(MyModel)
 class MyModelAdmin(...):
    ...

-admin.site.register(MyModel, admin_class=MyModelAdmin)
  1. Support multiple models ✅ #200
+@admin.register(Model1, Model2)
 class MyModelAdmin(...):
    ...

-admin.site.register(Model1, MyModelAdmin)
-admin.site.register(Model2, MyModelAdmin)
+@admin.register(Model1, Model2)
 class MyModelAdmin(...):
    ...

-admin.site.register((Model1, Model2), MyModelAdmin)
  1. Support when there are other class decorators, insert at top ✅ #193
+@admin.register(MyModel)
 @something
 class MyModelAdmin(...):
    ...

-admin.site.register(MyModel, MyModelAdmin)
  1. Support custom admin sites ✅ #228
from myapp.admin import custom_site

+@admin.register(MyModel, site=custom_site)
 class MyModelAdmin(...):
    ...

-custom_site.register(MyModel, MyModelAdmin)
  • detect custom site objects heuristically, to avoid affecting other code that uses a 'register' pattern, perhaps when the object's name ends in 'site', and the registered class name ends in 'Admin' (and maybe the file looks like an admin file, called "admin.py" or in an "admin" directory?)
  1. Also work for from django.contrib.gis import admin - ✅ #204
    This imports the register decorator for convenience when authoring GIS apps.

HttpRequest.META is not deprecated

Python Version

3.8.10

Django Version

3.2

Package Version

1.0.0

Description

According to the documentation of HttpRequest.META, HttpRequest.META is not deprecated in any version of django, so the rule in django-upgrade which converts these calls to request.headers and removes the HTTP prefix looks like it is too strict.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.