Migrating from adrf

Compared with adrf 0.1.14. The procedure for a DRF project is in the DRF guide; this one covers what differs for adrf. The architectural comparison explains the execution and maintenance trade-offs without assuming a migration is needed.

Prefer explicit aiodrf imports and reviewable codemod changes (sections 1 and 2). The optional compatibility layer (section 0) is an alternative for staged migration; it installs process-wide import aliases and needs separate review.

0. Running adrf code unchanged

# settings.py; uninstall adrf first.
INSTALLED_APPS = [
    "aiodrf",  # before any app whose modules import adrf
    ...,
]
AIODRF = {"ADRF_COMPAT": True}

aiodrf.contrib.adrf_compat then serves adrf, adrf.views, adrf.viewsets, adrf.serializers and adrf's other modules from aiodrf, through sys.modules aliases: adrf's own code never runs. The layer is off by default. It is installed when Django loads aiodrf's app, before any app's models. Code that imports adrf before django.setup() calls aiodrf.contrib.adrf_compat.install() first (in settings.py). It refuses to install while adrf is installed, or when adrf was imported already.

Preserved interfaces:

adrf Under the layer
actions alist, acreate, aretrieve, aupdate, partial_aupdate, adestroy and their overrides, super().alist(...) included an override also becomes aiodrf's list, create, ...; the adrf name on the base class runs aiodrf's action
perform_acreate, perform_aupdate, perform_adestroy, get_apaginated_response the same, as aperform_create, aperform_update, aperform_destroy, aget_paginated_response
adrf.routers.SimpleRouter, DefaultRouter route to adrf's action names, so self.action is "alist" as before; an action the viewset defines under DRF's name (its own list) is routed to that name
check_async_permissions(request, permissions) and the object and throttle variants, overridden adrf's split: async policies to check_async_*, the others to check_sync_* in a thread hop; a throttle's wait() after an async denial also runs in a thread hop unless declared pure
adrf.permissions.AsyncBasePermission, AAND, AOR, ANOT aiodrf's BasePermission with async defaults; DRF's operators, which aiodrf evaluates with async operands
adrf.fields.*, a custom field with async def ato_representation(self, value) DRF's fields; aiodrf awaits that method for fields derived from adrf.fields
await serializer.adata, acreate, aupdate, asave, async def get_<field> aiodrf's serializers
adrf.requests.AsyncRequest, adrf.shortcuts, adrf.decorators.api_view, adrf.test aiodrf's request, Django's shortcut, aiodrf's api_view and test client

The shim runs aiodrf implementations: permissions are checked in DRF's order (section 4), a page is represented in one thread hop (section 6), and perform_create written with DRF's name is called, where adrf would call only perform_acreate. A sample adrf project gives the same responses, with the same number of thread hops, under the compatibility layer and after conversion with the codemod.

Each adrf module imported and each adrf method name defined warns once with aiodrf.contrib.adrf_compat.AdrfCompatWarning, a DeprecationWarning, pointing at the import or the class. The layer stays supported; the warning names aiodrf's equivalent and the codemod. To silence it:

import warnings
from aiodrf.contrib.adrf_compat import AdrfCompatWarning

warnings.filterwarnings("ignore", category=AdrfCompatWarning)

1. Names

adrf aiodrf Codemod
adrf.views, generics, mixins, viewsets, serializers, decorators, permissions, routers, test the aiodrf module of the same name yes
adrf.requests.AsyncRequest aiodrf.request.Request yes, imported under its old name
adrf.shortcuts.aget_object_or_404 django.shortcuts.aget_object_or_404 yes
adrf.permissions.AsyncBasePermission, AAND, AOR, ANOT aiodrf.permissions.BasePermission, AND, OR, NOT (aiodrf evaluates DRF's operators with async operands) yes, imported under their old names
adrf.generics.aget_object_or_404, adrf.mixins.get_data, adrf's operator helpers (try_convert_operator, ...), adrf.fields, adrf.utils no counterpart: await self.aget_object(), await aio.data(serializer); the helpers are not needed kept, with a note
view actions alist, acreate, aretrieve, aupdate, partial_aupdate, adestroy DRF's names: list, create, retrieve, update, partial_update, destroy, as async def yes
perform_acreate, perform_aupdate, perform_adestroy aperform_create, aperform_update, aperform_destroy yes
get_apaginated_response aget_paginated_response yes
check_async_permissions, check_async_object_permissions, check_async_throttles acheck_permissions, acheck_object_permissions, acheck_throttles renamed; the signature is not (below)
serializer acreate, aupdate, asave the same names left alone; super().acreate() / super().aupdate() are noted (aiodrf's serializers do not define them)
await serializer.adata await serializer.adata(); the property spelling still works no change needed
serializer.is_valid() await serializer.ais_valid() no: adrf validates synchronously

adrf's check_async_* methods receive the list of policies to check; aiodrf's acheck_* take (request) or (request, obj) and select the policies themselves. An override of one of them has to be rewritten by hand after the rename.

2. The codemod

The codemod needs libcst, installed with the codemod extra:

pip install "django-aiodrf[codemod]"
python -m aiodrf.codemod --diff myproject/api/

A directory is scanned for .py files, skipping hidden directories (.venv, .nox, .git), venv, site-packages, vendor, node_modules, build, dist, __pycache__ and migrations, and symbolic links; a file named on the command line is always converted. A rewritten file keeps its encoding (PEP 263), line endings and permissions, and is replaced in one step.

Besides the imports it renames the methods of the table inside classes that are views, when they take request first or, as adrf declares them, only *args, **kwargs. self.<name> and super().<name> references in those classes follow in every method: a hook only adrf names (get_apaginated_response, perform_acreate, ...), a method the class renamed, and any adrf name when the class derives from adrf's views; any other reference is left and noted. So are the keyword arguments of extend_schema_view(). A multi-line import keeps its comments and its trailing comma. String literals naming an adrf action in an as_view({...}) method map (the first argument or actions=), such as as_view({"get": "alist"}), become DRF's names; a map built from variables is left alone. A class is a view when one of its bases, followed through the file's imports if it was imported under another name, ends in View or ViewSet. A Mixin base counts only when imported from DRF, adrf or aiodrf, not an application's AuditMixin. Each class body is judged on its own: a serializer nested in a view keeps its acreate and its super().acreate(...) call, and a class the codemod cannot classify is left alone. Base classes imported under another name are recognized.

Both from adrf.viewsets import ModelViewSet and module imports such as from adrf import serializers, viewsets as views are handled. Converted code behaves as before for CRUD, filtering, policies, custom actions and the OpenAPI schema, and running the codemod a second time changes nothing. Calls from helper methods whose signatures cannot be classified need manual review, and so does every diagnostic note the codemod prints.

Not rewritten, because they are strings: self.action == "alist" comparisons and per-action permission or serializer maps keyed by action name. Search for the old action names after running it. A routed view that still defines alist, acreate, ... with an action's signature (request first, or only *args, **kwargs) is reported by manage.py check as aiodrf.W006 unless the compatibility layer serves it: aiodrf routes to DRF's names, so such a method is never called.

3. Routers

Use DRF's routers (aiodrf.routers re-exports them; the codemod moves the import). adrf's router maps list to alist, which an aiodrf viewset does not have. With DRF's action names, basename, reverse URL names and drf-spectacular's list detection are those of a DRF project.

4. Policies

adrf gathers all async permissions concurrently and then checks the synchronous ones. aiodrf asks them in declaration order and stops at the first denial, as DRF does, looking through &, | and ~. When several policies deny or have side effects, the reported error and the order of those effects can differ from adrf's.

adrf-style policies (async def has_permission) work unchanged, from synchronous callers too: the browsable API and schema generation no longer see a coroutine, which is truthy, where they expect a boolean. For new code subclass aiodrf.permissions.BasePermission and implement ahas_permission / ahas_object_permission; likewise aauthenticate and aallow_request.

5. Serializers and writes

Serializer acreate, aupdate and asave keep their names and meaning. Validation is new: adrf runs DRF's synchronous is_valid(), aiodrf has ais_valid() with async def validators, avalidate_<field> and avalidate, awaited in DRF's order. Code that keeps calling is_valid() from a thread keeps working.

ATOMIC_SAVE wraps aiodrf's default synchronous save in a transaction; an acreate/aupdate of the project is not wrapped, because async code cannot run inside a synchronous atomic block. Successive awaited writes do not share a transaction. What must be atomic goes into one synchronous function with transaction.atomic(), awaited once.

6. Execution after migration

aiodrf can represent a synchronous page in the worker operation that fetched it. Async fields and custom hooks can require additional boundaries. Verify query counts, response contracts and loop behavior with the application's own dependencies; changing imports alone does not change performance.