Chapter 17 · The utility layer

17.13 Deprecation: warnings named by year, warn_about_external_use and deprecate_posargs

django/utils/deprecation.py holds the warning classes with which Django marks what it will remove, RemovedInDjango2028Warning and RemovedInDjango2029Warning, and the helpers that raise them: warn_about_external_use, which warns only when the code using a deprecated feature is outside Django, warn_about_implementation, which names a definition, and deprecate_posargs, which makes parameters keyword-only while still taking them by position for a time.

A deprecated feature has to go on working and warn the code that uses it, naming that code’s line, which is seldom the line that raises the warning. Most of the module is about finding it.

Two steps, and a class for each release

Django’s deprecation policy is written in feature releases: a feature deprecated in A.x keeps working in every A.x release but raises warnings, and is removed in B.0, or in B.1 when it was deprecated in the last A.x, so that a deprecation spans at least two feature releases. Following DEP 20, the releases planned as 7.0 and 7.1 are Django 2028 and 2029, and each warning class is named for the release that removes what it marks. The timeline lists under 2028 what 6.0 and 6.1 deprecated, with the new shape of django.VERSION that 6.2 announces, and under 2029 the rest of what 6.2 deprecated; What still warns: the deprecations and the releases that end them gives each with the code that raises it.

RemovedInDjango2028Warning derives from DeprecationWarning, and RemovedInDjango2029Warning, the farther off, from PendingDeprecationWarning. RemovedInNextVersionWarning and RemovedAfterNextVersionWarning are the same two classes, named by distance rather than year; nothing in the django package uses them, and Django’s test suite turns both into errors. Code that must change when a deprecation ends but raises no warning carries a comment above it naming the class.

What Python shows

Python’s default filters show a DeprecationWarning only when it is attributed to the module __main__, once for each line, and ignore every PendingDeprecationWarning. A filter is matched against the module the warning is attributed to, not the one that raised it, so the attribution decides both the line a warning names and whether it is seen at all:

# a run of a script, with Python's default filters
django.VERSION[2], in the script                  shown: RemovedInDjango2028Warning
django.VERSION[2], in harbour/office/board.py     nothing shown
MiddlewareMixin from django.utils.deprecation    nothing shown
RemovedInDjango70Warning from the same            shown: UserWarning

Python’s -Wd option shows them all, the third a RemovedInDjango2029Warning.

The module’s __getattr__

A name the module does not define is looked up in its __getattr__. MiddlewareMixin, moved to django/middleware/__init__.py in 6.2, is answered with a RemovedInDjango2029Warning naming the importing line, and imported inside the function, its comment says, so that importing the module does not need django.middleware’s dependencies (Sync and async in one chain). The old name RemovedInDjango70Warning answers with RemovedInDjango2028Warning and a warning of no category, a UserWarning, shown anywhere. Importing RemovedInDjango71Warning fails with ImportError.

django_file_prefixes: the first frame outside Django

Most of Django’s deprecations warn whoever calls, and django_file_prefixes lets such a warning name the first caller outside Django. The standard library’s warnings.warn takes skip_file_prefixes, a tuple of path prefixes: counting up the stack to the frame it names, it passes over every frame whose file begins with one of them. django_file_prefixes gives the django package’s directory with a separator at the end, computed once. A warning raised with skip_file_prefixes=django_file_prefixes() names the first frame above its raiser whose file is not Django’s, as JsonResponse does when given its deprecated safe argument.

warn_about_external_use: silent for Django’s own calls

When one deprecated feature is built on another, Django’s own call would warn a project about code it did not write; the docstring of warn_about_external_use gives avoiding that cascade as its reason. It finds the effective caller, the code that used the feature, and warns only if that is outside Django. From its own frame it goes up:

  1. It passes over two frames: its own, and its caller’s.
  2. Given skip_name_prefixes, it passes over frames while each one’s qualified name, its module’s __name__, a dot and the code’s qualified name, begins with one of them.
  3. It passes over skip_frames more.
  4. The frame reached is the effective caller. If its qualified name begins with one of internal_modules and a dot, ("django",) unless given, nothing happens; otherwise warnings.warn is called with the stacklevel that counts to that frame.

Inside is decided by the module’s name, not its file, so a script, __main__, is outside.

Figure 17.3 warn_about_external_use walks up from its own frame: two frames always, then those matching skip_name_prefixes, then skip_frames more. From the example, a deprecated read of django.VERSION warns and names the example’s line; through get_version, the walk stops at Django’s own code and nothing is warned.

VersionTuple._warn passes its class’s prefix, so that the code that read the tuple is the effective caller (Django’s version: VersionTuple and get_version). The deprecated settings pass as prefixes the settings classes and the test utilities that override settings, and get_connection, under deprecate_posargs, passes skip_frames=1, for its wrapper.

warn_about_implementation: the line of a definition

warn_about_implementation is for deprecations found in code a project wrote rather than in a call, such as an override with an old signature, where the line to name is its definition, which need not be on the stack. It takes a function, a class, a bound method, or a classmethod, staticmethod or property object, finds the unwrapped function’s file and line with inspect.getsourcefile and inspect.getsourcelines, and calls warnings.warn_explicit with them. ModelAdmin uses it for an override of ModelAdmin.get_actions without the action_location parameter (ModelAdmin._get_actions_with_action_location; Actions: response_action and delete_selected).

deprecate_posargs

Making a parameter keyword-only breaks every call that passes it by position; deprecate_posargs lets the function be written in its final form, after a *, and still take the old calls for the period. It is given the warning class and the names that became keyword-only, in their old order:

# harbour/office/board.py
class RemovedInHarbour3Warning(DeprecationWarning):
    pass


@deprecate_posargs(RemovedInHarbour3Warning, ["berth"])
def moor(ship, *, berth="North Quay"):
    return f"{ship} moored at {berth}"

As it decorates, it refuses with TypeError a class (it goes on __init__), a classmethod or staticmethod object (it goes under them), a function with *args or no keyword-only parameter, and a listed name that is not keyword-only. A call with no more positional arguments than the function has positional parameters passes straight through. Extra ones are taken, in order, as the first of the listed names and moved into the keyword arguments, and the warning is raised with skip_file_prefixes=django_file_prefixes(). A coroutine function gets a coroutine wrapper, and the message for an __init__ names its class, read from __qualname__ because the class does not yet exist.

# a run in a script, every warning recorded
moor("Beagle", berth="Pier 2")            'Beagle moored at Pier 2'
moor("Beagle", "Pier 2")                  'Beagle moored at Pier 2'   RemovedInHarbour3Warning:
    Passing positional argument(s) 'berth' to moor() is deprecated. Use keyword arguments instead.
moor("Beagle", "Pier 2", "Pier 3")
    TypeError: moor() takes at most 2 positional argument(s) (including 1 deprecated) but 3 were given.
moor("Beagle", "Pier 2", berth="Pier 3")
    TypeError: moor() got both deprecated positional and keyword argument values for 'berth'.

The docstring’s caution follows: during the period no positional parameter may be added, since an old call’s value would land in it. Django decorates the functions of django.core.mail and the constructors of EmailMessage and EmailMultiAlternatives (Email messages: EmailMessage and EmailMultiAlternatives).

Renamed methods: RenameMethodsBase

RenameMethodsBase is a metaclass for renaming a method, its docstring citing ticket 15363; a subclass lists triples of old name, new name and warning class in RenameMethodsBase.renamed_methods. A class defining only the old name is warned as it is made and gets the new name too, and one defining only the new name gets the old; in both cases the old name becomes a warn_about_renamed_method wrapper that warns at each call. Nothing in Django uses either.