Skip to content

adamchainz/django-upgrade

Repository files navigation

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 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):

Then, upgrade your entire project:

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. django-upgrade will run all its fixers for versions up to and including the target version. These fixers rewrite your code to avoid DeprecationWarnings and use some new features.

For example:

The --target-version flag defaults to 2.2, the oldest supported version when this project was created. For more on usage run django-upgrade --help.

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_:

…or PowerShell’s ForEach-Object__:

The full list of fixers is documented below.

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

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.

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

Django 1.7

Release Notes

Admin model registration

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.

+@admin.register(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.

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

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

Django 1.9

Release Notes

on_delete argument

Add on_delete=models.CASCADE to ForeignKey and OneToOneField:

-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:

DATABASES

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.

  • "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

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.

Django 1.10

Release Notes

request.user boolean attributes

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

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.*

Django 1.11

Release Notes

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.

Django 2.0

Release Notes

URL’s

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().

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

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

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

ContextDecorator

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

<func>.allow_tags = True

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.

-upper_case_name.allow_tags = True

Django 2.2

Release Notes

HttpRequest.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.

QuerySetPaginator

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

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.

FloatRangeField

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

-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

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).

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

    self.assertEqual(2 * 2, 4)

Django 3.0

Release Notes

django.utils.encoding aliases

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

django.utils.http deprecations

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().

django.utils.text deprecation

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

django.utils.translation deprecations

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

Django 3.1

Release Notes

JSONField

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.

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.

Signal

Removes the deprecated documentation-only providing_args argument.

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

get_random_string

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

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

NullBooleanField

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.

ModelMultipleChoiceField

Replace list error message key with list_invalid on forms ModelMultipleChoiceField.

Django 3.2

Release Notes

@admin.action()

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.

+@admin.action( + 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()

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.

+@admin.display( + 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

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.

  • requires_system_checks = True
  • requires_system_checks = "__all__"

class SecondCommand(BaseCommand):

  • requires_system_checks = False
  • requires_system_checks = []

EmailValidator

Rewrites the whitelist keyword argument to its new name allowlist.

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

default_app_config

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

Django 4.0

Release Notes

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.

lookup_needs_distinct

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

Compatibility imports

Rewrites some compatibility imports:

  • django.utils.translation.template.TRANSLATOR_COMMENT_MARK in django.template.base

Django 4.1

Release Notes

django.utils.timezone.utc deprecations

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

-from django.utils.timezone import utc

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

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

assertFormError() and assertFormsetError()

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

Django 4.2

Release Notes

STORAGES setting

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.

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:

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

Test client 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.

assertFormsetError and assertQuerysetEqual

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

Django 5.0

Release Notes

No fixers yet.