I would like to raise an exception in a function, and then check somewhere else (in the Django view and my unit tests) if it was raised. The following code uses status codes, and it works. But I can't figure out how to do the same thing with exceptions - which, everyone seems to agree, are the right way to do this kind of thing.

It is important to me to use custom error messages. Not to print them, but to detect and use them in the code (mainly to forward them to the end user with Django messages).

I have no idea how I would check in add_foo_view if an exception was raised in utils.add_foo.

In the unit test I have tried things like assertWarnsRegex(Warning, 'blah went wrong'), but that did not bother to check if the message is actually the same.

views.py:

from django.contrib import messages

from .utils import add_foo


def add_foo_view(request):
    if request.method == 'POST':

        status = add_foo(request.POST['bar'])
        if not status == 'Bar was added.':
            messages.error(request, status)

        return render(request, 'index.html')
    else:
        return render(request, 'add_foo.html')

utils.py:

def add_foo(bar):

    if not spamifyable(bar):
        return 'Bar can not be spamified.'

    try:
        eggs = Egg.objects.get(baz=bar)
    except:
        return 'Bar has no eggs.'

    do_things(bar)

    return 'Bar was added.'

tests.py:

def test_bar_without_eggs(self):

    status = add_foo(eggless_bar)

    assertEqual(status, 'Bar has no eggs.')

I use Python 3.5.2 and Django 1.11.4.

Edit: I am not actually sure if exceptions would be the correct choice here. I often read, that exceptions are only for things that are unexpected. But the cases I am catching here are wrong inputs by the user, which are very much expected. So my question is not really how to make this with exceptions, but how to make this the right and pythonic way. In any case I want the validation to happen in the separate utils place (plain Python, no Django), and not in the view.

2

There are 2 answers

0
Watchduck On BEST ANSWER

An explicit success message returned by add_foo may seem clearer, but is probably not a good idea. The function does not need to return anything. If no exception is raised, it can be assumed that it was successful.

I can create a custom exception and add the message property in the __init__. This way it can be accessed as e.message, which in my case will be relayed to the index page in views.py.

utils.py

class AddFooException(Exception):

    def __init__(self, message):
        self.message = message


def add_foo(bar):

    if not spamifyable(bar):
        raise AddFooException('Bar can not be spamified.')

    try:
        eggs = Egg.objects.get(baz=bar)
    except:
        raise AddFooException('Bar has no eggs.')

    do_things(bar)

views.py

from django.contrib import messages

from .utils import add_foo, AddFooException


def add_foo_view(request):
    if request.method == 'POST':
        bar = request.POST['bar']

        try:
            add_foo(bar)
        except AddFooException as e:
            messages.error(request, e.message)


        return render(request, 'index.html')
    else:
        return render(request, 'add_foo.html')
11
Siddardha On

You can use the 'raise' statement to raise an exception, like:

raise Exception("Bar has no eggs.")

You can also create custom exceptions by inheriting from the Exception in-built class, like:

class MyException(Exception):
    pass

then you can do:

raise MyException("Bar has no eggs.")

And you can catch the raised exceptions using a try-except block:

try:
    function_that_raises_exception(args)
except MyException:
    function_to _handle_exception(args)

So in your views.py you can do:

from django.contrib import messages

from .utils import add_foo, MyException


def add_foo_view(request):
    if request.method == 'POST':
        try:
            add_foo(request.POST['bar'])
        except MyException as e:
            messages.error(request, str(e))

        return render(request, 'index.html')
    else:
        return render(request, 'add_foo.html')