0

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.

Watchduck
  • 1,076
  • 1
  • 9
  • 29

2 Answers2

0

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')
Siddardha
  • 510
  • 1
  • 7
  • 17
  • But you still use `status`, which is essentially an error code. How would I find out in the view, whether the problem is _can not be spamified_ or _has no eggs_, using only exceptions? – Watchduck Aug 29 '17 at 21:10
  • You can replace `return 'Bar has no eggs.'` with `raise MyException("Bar has no eggs.")` then you don't need to check for `status`. – Siddardha Aug 30 '17 at 16:12
  • I got that. But how would I check in the view (or in the tests) what the actual problem was - `MyException("Bar can not be spamified.")` or `MyException("Bar has no eggs.")`? I want to check the error message. And I don't think that making as many subclasses as I have error messages (`UnpamifyableException`, `NoEggsException`) would be a sane thing to do. – Watchduck Aug 30 '17 at 17:45
  • I will edit my answer, according to my understanding of your question. I am assuming that you ant to do `messages.error(request, status)` if that exception is caught. – Siddardha Aug 30 '17 at 17:49
  • Yes. So if the solution involves something like `except MyException as e:` in the view (which did not work for me), something like `messages.error(request, e.message)` would come below. – Watchduck Aug 30 '17 at 17:52
  • if you do `except MyException as e:` then you can use `str(e)` in your except block to get the error message. I hope I understood your question correctly. – Siddardha Aug 30 '17 at 17:54
  • Maybe you are forgetting to import `MyException` from utils. Use: `from .utils import add_foo, MyException` in your views.py – Siddardha Aug 30 '17 at 17:55
  • Also, I think what you were doing with `return 'Bar has no eggs.'` is not a bad choice. However, I would call it something other than 'status code' though – Siddardha Aug 30 '17 at 18:04
  • It works, and I think it's easy to read, so it can't be a terrible choice. But I think there must be a more commonly accepted way to do this in Python. The whole `except MyException as e` thing does not work for me, because I get the following error: `catching classes that do not inherit from BaseException is not allowed` – Watchduck Aug 30 '17 at 18:11
  • check this: https://www.reddit.com/r/learnpython/comments/3czr99/python_3_custom_exception_error/ – Siddardha Aug 30 '17 at 18:18
  • Found that already. Also happens when I do `except Exception('blah')`. The argument is the problem. Just `except Exception as e` works. – Watchduck Aug 30 '17 at 18:22
0

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')
Watchduck
  • 1,076
  • 1
  • 9
  • 29