enthought / traits

Observable typed attributes for Python classes
Other
432 stars 85 forks source link

Explain why exceptions don't bubble up in observers #1726

Open achabotl opened 1 year ago

achabotl commented 1 year ago

In the sample below exceptions that are raised through observe cannot be caught in unit tests. It's expected behavior, but it may be surprising to some.

import unittest

from traits.api import HasTraits, Int, observe

class Person(HasTraits):
    age = Int()

    @observe("age")
    def update_age(self, event):
        if self.age > 15:
            print("Raising an exception")
            raise ValueError("I do not want to get old")

class TestPerson(unittest.TestCase):
    def test_update_age(self):
        # given
        steve = Person(age=10)

        # when / then
        with self.assertRaises(ValueError) as err:
            steve.age = 18

        self.assertIn("I do not want to get old", str(err.exception))

The following explanation from @mdickinson could be cleaned up and integrated in the docs:

There are mechanisms that allow exceptions in observe handlers to be propagated; that's sometimes useful during testing. But raising an exception in an observer, while it might be tempting, isn't really a terribly useful thing to do - there's no sensible place for that exception to be propagated to. IOW, an exception raised in an observer is almost always a coding bug. In particular, you should never try to use observers for validation of an assignment to a trait - that doesn't work, because the assignment has already happened.

For testing and debugging, see https://docs.enthought.com/traits/traits_user_manual/debugging.html#re-raising-exceptions-in-change-handlers, and in particular push_exception_handler. Note that there are two versions of push_exception_handler: one for on_trait_change and one for observe, so you need to use both if you want to catch all exceptions. But beware doing this in tests by default, since now your test logic isn't necessarily exercising the same code paths that your business logic is. It's quite possible to end up with broken business logic that passes test cases as a result.

Internal discussion thread: https://enthought.slack.com/archives/C024RRPHD/p1667297991245249