Sometimes, you have an API:
@abc.abstractmethod def get_property_value(self, prop): """Returns the value associated with the given property.
Args: prop (DataProperty): The property.
Returns: The value associated with the given property.
Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError
and you don't want your API to mask bugs. How would it mask bugs? For example, if API consumers do an:
try: x = foo.get_property_value(prop) except ValueError: # handle it
then the following implementation:
def get_property_value(self, prop): iterator = self.get_property_values(prop) try: # note: unpacking ret, = iterator except LookupError as exc: raise RuntimeError from exc # don't accidentally swallow bugs in the iterator return ret def get_property_values(self, prop): try: factory = self.get_supported_properties()[prop] except KeyError as exc: raise PropertyError from exc iterator = factory(self._obj) try: first = next(iterator) except StopIteration: return (x for x in ()) except abdl.exceptions.ValidationError as exc: raise LookupError from exc except LookupError as exc: raise RuntimeError from exc # don't accidentally swallow bugs in the iterator return itertools.chain([first], iterator)
could cause you to accidentally swallow unrelated ValueErrors.
so instead this needs to be rewritten. you can't just unpack things from the iterator, you need to wrap the iterator into another iterator, that then converts ValueError into RuntimeError.
but if that ValueError is part of *your* API requirements... it's impossible to use!
so my proposal is that we get "exception spaces". they'd be used through the 'in' keyword, as in "except in" and "raise in".
for example:
X = object() Y = object()
def foo(): raise LookupError in X
def bar(): try: foo() except LookupError in Y: print("bar: caught LookupError in Y, ignoring")
def baz(): try: bar() except LookupError in X: print("baz: caught LookupError in X, re-raising in Y") raise in Y
def qux(): try: baz() except LookupError in Y: print("qux: caught LookupError in Y, ignoring")
qux()
# would print: # --- # baz: caught LookupError in X, re-raising in Y # qux: caught LookupError in Y, ignoring # --- # note the lack of "bar"
(or perhaps "raise in X LookupError" and "except in Y LookupError" etc)
and then you can adjust the above implementations accordingly: (btw, anyone knows how to tell apart a ValueError from a specific unpacking and a ValueError from an iterator being used in that unpacking?)
def get_property_value(self, prop, espace=None): iterator = self.get_property_values(prop, espace=espace) try: # note: unpacking ret, = iterator except ValueError: raise in espace # except LookupError as exc: raise RuntimeError from exc # no longer needed return ret def get_property_values(self, prop, espace=None): try: factory = self.get_supported_properties()[prop] except KeyError as exc: raise PropertyError in espace from exc iterator = factory(self._obj) try: first = next(iterator) except StopIteration: return (x for x in ()) except abdl.exceptions.ValidationError as exc: raise LookupError in espace from exc # except LookupError as exc: raise RuntimeError from exc # no longer needed return itertools.chain([first], iterator)
as well as the caller:
espace = object() try: x = foo.get_property_value(prop, espace=espace) except ValueError in espace: # handle it
I feel like this feature would significantly reduce bugs in python code, as well as significantly improve the error messages related to bugs. This would be even better than what we did with StopIteration! This would be comparable to Rust's Result type, where you can have Result<Result<YourValue, YourError>, TheirError> and the like (except slightly/a lot more powerful).