Chapter 17 · The utility layer
17.12 Django’s exceptions: django.core.exceptions
django/ holds the exceptions its docstring calls global, many raised in one part of Django and caught in another: the bases of each model’s Model and Model, SuspiciousOperation and its subclasses, PermissionDenied, BadRequest, ImproperlyConfigured, ValidationError with NON_FIELD_ERRORS, the compiler’s EmptyResultSet and FullResultSet, and SynchronousOnlyOperation, which async_unsafe raises.
The code that raises an exception and the code that catches it are often in different parts of Django: the ORM raises a model’s Model and the template engine catches it; the request raises DisallowedHost and the handler answers it. The module imports nothing of Django but make_hashable, so any part can import it. Most of its classes are a name, a base and a docstring.
Who raises each, and what becomes of it
settings.DEBUG_PROPAGATE_EXCEPTIONS is true (How an exception becomes a response); the result sets are told in The where clause: build_filter, add_q and WhereNode.| exception | base | raised by | what becomes of it |
|---|---|---|---|
Object |
base Exception |
raised by a model’s Model, from QuerySet.get (QuerySet.get django/ |
what becomes of it get_ raises Http404 instead (get_ django/"string_, empty by default |
Multiple |
base Exception |
raised by a model’s Model, from QuerySet.get |
what becomes of it it goes up |
ObjectNotUpdated |
base Exception |
raised by a model’s Model.NotUpdated, from a forced update that changed no row (Model django/ |
what becomes of it it goes up |
Field |
base Exception |
raised by Options (Options django/ |
what becomes of it the code that asked; a query raises FieldError instead |
FieldError |
base Exception |
raised by a query’s keyword that names no field (Query django/ |
what becomes of it it goes up |
Field |
base FieldError |
raised by FETCH_RAISE, when a field not loaded is read (FetchRaise.fetch django/ |
what becomes of it it goes up (Deferred fields and fetch modes in a queryset) |
ValidationError |
base Exception |
raised by validators (validators.py django/core/) |
what becomes of it a form’s or a model’s errors |
Suspicious |
base Exception |
raised by its subclasses; markup strip_tags gives up on (strip_tags django/ |
what becomes of it a 400, logged under "django (response_ django/ |
DisallowedHost |
base Suspicious |
raised by a host ALLOWED_HOSTS refuses (Http django/ |
what becomes of it a 400 |
Disallowed |
base Suspicious |
raised by a redirect too long or to a scheme not allowed (Http django/ |
what becomes of it a 400 |
Request |
base Suspicious |
raised by a body over DATA_ (Http django/ |
what becomes of it a 400 |
Too |
base Suspicious |
raised by fields over DATA_ (Query django/ |
what becomes of it a 400 |
TooManyFilesSent |
base Suspicious |
raised by files over DATA_ (Multi django/ |
what becomes of it a 400 |
Suspicious |
base Suspicious |
raised by a multipart body on which the parser seems to loop (Lazy django/ |
what becomes of it a 400 |
Suspicious |
base Suspicious |
raised by a file name reaching outside its directory (safe_join django/ |
what becomes of it a 400 |
BadRequest |
base Exception |
raised by a urlencoded form whose encoding is not UTF-8 (Http django/ |
what becomes of it a 400 |
PermissionDenied |
base Exception |
raised by a permission check (Access django/ |
what becomes of it a 403; from a backend, authenticate tries no other (authenticate django/ |
RequestAborted |
base Exception |
raised by a client gone (ASGIHandler django/ |
what becomes of it ASGIHandler sends nothing |
Improperly |
base Exception |
raised by a setting read before any are configured (Lazy django/ |
what becomes of it it goes up |
App |
base Exception |
raised by the registry asked before it is ready (Apps django/ |
what becomes of it it goes up |
Middleware |
base Exception |
raised by a middleware’s factory | what becomes of it the layer is left out (Building the middleware chain) |
ViewDoesNotExist |
base Exception |
raised by a dotted path that names no view (get_callable django/ |
what becomes of it the URL checks report it |
EmptyResultSet |
base Exception |
raised by a condition that can match no row (WhereNode.as_sql django/ |
what becomes of it the compiler sends no statement, or writes a false condition |
FullResultSet |
base Exception |
raised by a condition that matches every row | what becomes of it the compiler writes no "WHERE" |
Synchronous |
base Exception |
raised by async_unsafe |
what becomes of it it goes up |
ValidationError: one error, a list, or a dictionary
ValidationError gives the error one of three shapes, chosen by its first argument:
- a single error: a message, a code a program can test, and params for the message, kept as
ValidationError,.message ValidationErrorand.code ValidationError, with a list of the error itself as.params ValidationError;.error_list - a list: each item made a
ValidationErrorif it is not one, and the single errors of all of them kept inerror_list; a dictionary-shaped item gives its errors without their names; - a dictionary of field names: each value made a
ValidationError, and its list kept under the name inValidationError..error_dict
Given a ValidationError, it takes that error’s dictionary, its list, or its message, code and params, which win over a code and params passed beside it.
A single or list-shaped error iterates as its messages, each with its params put in by %; a dictionary-shaped one as pairs of a name and its messages. ValidationError is a flat list of strings for any shape; ValidationError is the dictionary of them, and raises AttributeError for the other shapes. ValidationError adds the errors to a dictionary it is given, by name, or under NON_FIELD_ERRORS, "__all__"; Model.full_clean gathers its steps’ errors with it, and BaseForm files a form’s errors in the same shape (Validation: full_clean, cleaned_data and the errors).
ValidationError compares hashes. ValidationError hashes a single error’s message, code and params; a dictionary-shaped error’s dictionary through make_hashable, which sorts the names and keeps each list’s order; a list-shaped error’s errors sorted by message. So a list’s order does not count, the order within a dictionary’s list does, and one error is not equal to a list of it.
# a run: a ship's errors
small = ValidationError("Tonnage %(tons)s is too small.", code="min_tonnage", params={"tons": 0})
both = ValidationError([small, "A ship needs a home port."])
byname = ValidationError({"tonnage": small, "home": ["A ship needs a home port."]})
home = both.error_list[1]
list(small) ['Tonnage 0 is too small.']
both.messages ['Tonnage 0 is too small.', 'A ship needs a home port.']
both.update_error_dict({}) {'__all__': [ValidationError(['Tonnage 0 is too small.']), ValidationError(['A ship needs a home port.'])]}
dict(byname) {'tonnage': ['Tonnage 0 is too small.'], 'home': ['A ship needs a home port.']}
ValidationError([byname]).messages ['Tonnage 0 is too small.', 'A ship needs a home port.']
both == ValidationError([home, small]) True
small == ValidationError([small]) False
A model’s exceptions and the template engine
ModelBase makes each model that is not abstract its own Model, Model and Model.NotUpdated from ObjectDoesNotExist, MultipleObjectsReturned and ObjectNotUpdated, or from the parent model’s classes; Model.NotUpdated derives from DatabaseError too, for backward compatibility (ModelBase: a class statement becomes a model). An except can name one model’s error or every model’s.
ObjectDoesNotExist is true. When a template variable’s lookup raises an exception with that attribute true, Variable gives the engine’s "string_if_invalid", empty by default, in its place; any other exception goes on up (Variables and filters: FilterExpression and Variable).
# a run: Valletta has two ships, Piraeus none
Port.objects.get(name="Rhodes") raises Port.DoesNotExist: Port matching query does not exist.
caught by except ObjectDoesNotExist Port.DoesNotExist
[{{ port.ships.get }}], port Piraeus '[]'
[{{ port.ships.get }}], port Valletta raises Ship.MultipleObjectsReturned: get() returned more than one Ship -- it returned 2!
SynchronousOnlyOperation and async_unsafe
The documentation says some of Django relies on thread-safety and does not work under coroutines sharing a thread; async_unsafe marks such code. Its wrapper asks asyncio whether a loop runs in the calling thread; if one does, and the environment variable "DJANGO_ALLOW_ASYNC_UNSAFE" is empty or unset, it raises SynchronousOnlyOperation, and otherwise calls the function. It asks about the thread, not the caller: a plain function called from a coroutine is refused too, and the same call through sync_to_async, on a thread with no loop, is not. The variable is read at each call.
Given a function, async_unsafe wraps it with a message of its own; given a message, it returns a decorator that raises with that one. BaseDatabaseWrapper marks the methods that open and close a connection, make a cursor, commit, roll back and keep savepoints, so a query run in a coroutine fails (The asynchronous methods of a queryset). A deployment check reports the variable set.
# harbour/office/notices.py
from django.utils.asyncio import async_unsafe
@async_unsafe
def post_notice(text):
return f"posted: {text}"
@async_unsafe("The notice board is written from a thread.")
def pin_notice(text):
return f"pinned: {text}"
# a run of the two functions
in a coroutine:
post_notice("Beagle sails") raises SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
pin_notice("Beagle sails") raises SynchronousOnlyOperation: The notice board is written from a thread.
the same, DJANGO_ALLOW_ASYNC_UNSAFE set 'pinned: Beagle sails'
await sync_to_async(pin_notice)("Beagle sails") 'pinned: Beagle sails'