@@ -86,8 +86,7 @@ class ControlError(Exception):
8686 when no handler handles the signal.
8787 """
8888
89- # TODO: now that signals have tracebacks (in Python 3.7+), "signal from" (like "raise from") would be nice.
90- def signal (condition ):
89+ def signal (condition , * , cause = None ):
9190 """Signal a condition.
9291
9392 Signaling a condition works similarly to raising an exception (pass an
@@ -120,6 +119,11 @@ def signal(condition):
120119 otherwise the same as `signal`, except it raises if `signal` would have
121120 returned normally.
122121
122+ The optional `cause` argument is as in `unpythonic.misc.raisef`.
123+ In other words, if we pretend for a moment that `signal` is a Python
124+ keyword, it essentially performs a `signal ... from ...`. The default
125+ `cause=None` performs a plain `signal ...`.
126+
123127 **Notes**
124128
125129 This condition system is implemented on top of exceptions. The magic trick
@@ -159,17 +163,21 @@ def accepts_arg(f):
159163 # special handling for the "class raised" case.
160164 # https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement
161165 # https://stackoverflow.com/questions/19768515/is-there-a-difference-between-raising-exception-class-and-exception-instance/19768732
162- def canonize (exc ):
166+ def canonize (exc , err_reason ):
167+ if exc is None :
168+ return None
163169 if isinstance (exc , BaseException ): # "signal(SomeError())"
164170 return exc
165171 try :
166172 if issubclass (exc , BaseException ): # "signal(SomeError)"
167173 return exc () # instantiate with no args, like `raise` does
168174 except TypeError : # "issubclass() arg 1 must be a class"
169175 pass
170- error (ControlError ("Only exceptions and subclasses of Exception can be signaled ; got {} with value '{}'." .format (type (condition ), condition )))
176+ error (ControlError ("Only exceptions and subclasses of Exception can {} ; got {} with value '{}'." .format (err_reason , type (condition ), condition )))
171177
172- condition = canonize (condition )
178+ condition = canonize (condition , "be signaled" )
179+ cause = canonize (cause , "act as the cause of another signal" )
180+ condition .__cause__ = cause
173181
174182 # Embed a stack trace in the signal, like Python does for raised exceptions.
175183 # This only works on Python 3.7 and later, because we need to create a traceback object in pure Python code.
@@ -666,8 +674,9 @@ def call_with_restarts(f):
666674 return call_with_restarts
667675
668676# Common Lisp standard error handling protocols, building on the `signal` function.
677+ # Pythonified to add the `cause` argument.
669678
670- def error (condition ):
679+ def error (condition , * , cause = None ):
671680 """Like `signal`, but raise `ControlError` if the condition is not handled.
672681
673682 Note **raise**, not **signal**. Keep in mind the original Common Lisp
@@ -678,17 +687,23 @@ def error(condition):
678687
679688 Note *handled* means that a handler must actually invoke a restart; a
680689 condition does not count as handled simply because a handler was triggered.
690+
691+ The optional `cause` argument is as in `unpythonic.misc.raisef`.
692+ In other words, if we pretend for a moment that `error` is a Python
693+ keyword, it essentially performs a `error ... from ...`. The default
694+ `cause=None` performs a plain `error ...`.
681695 """
682- signal (condition )
696+ signal (condition , cause = cause )
683697 # TODO: If we want to support the debugger at some point in the future,
684698 # TODO: this is the appropriate point to ask the user what to do,
685699 # TODO: before the call stack unwinds.
686700 #
687701 # TODO: Do we want to give one last chance to handle the ControlError?
688702 # TODO: And do we want to raise ControlError, or the original condition?
703+ condition .__cause__ = cause # chain the causes, since we'll add a new one next.
689704 raise ControlError ("Unhandled error condition" ) from condition
690705
691- def cerror (condition ):
706+ def cerror (condition , * , cause = None ):
692707 """Like `error`, but allow a handler to instruct the caller to ignore the error.
693708
694709 `cerror` internally establishes a restart named `proceed`, which can be
@@ -699,6 +714,11 @@ def cerror(condition):
699714 We use the name "proceed" instead of Common Lisp's "continue", because in
700715 Python `continue` is a reserved word.
701716
717+ The optional `cause` argument is as in `unpythonic.misc.raisef`.
718+ In other words, if we pretend for a moment that `cerror` is a Python
719+ keyword, it essentially performs a `cerror ... from ...`. The default
720+ `cause=None` performs a plain `cerror ...`.
721+
702722 Example::
703723
704724 # If your condition needs data, it can be passed to __init__.
@@ -717,9 +737,9 @@ def __init__(self, value):
717737
718738 """
719739 with restarts (proceed = (lambda : None )): # just for control, no return value
720- error (condition )
740+ error (condition , cause = cause )
721741
722- def warn (condition ):
742+ def warn (condition , * , cause = None ):
723743 """Like `signal`, but emit a warning if the condition is not handled.
724744
725745 For emitting the warning, we use Python's standard `warnings.warn` mechanism.
@@ -732,6 +752,11 @@ def warn(condition):
732752 If not (i.e. some other subclass of `Exception` is being signaled), the
733753 generic category `Warning` is used, with the message set to `str(condition)`.
734754
755+ The optional `cause` argument is as in `unpythonic.misc.raisef`.
756+ In other words, if we pretend for a moment that `warn` is a Python
757+ keyword, it essentially performs a `warn ... from ...`. The default
758+ `cause=None` performs a plain `warn ...`.
759+
735760 Example::
736761
737762 class HelpMe(Warning):
@@ -762,7 +787,7 @@ def __init__(self, value):
762787 """
763788 with restarts (muffle = (lambda : None )): # just for control, no return value
764789 with restarts (_proceed = (lambda : None )): # for internal use by unpythonic.test.fixtures
765- signal (condition )
790+ signal (condition , cause = cause )
766791 if isinstance (condition , Warning ):
767792 warnings .warn (condition , stacklevel = 2 ) # 2 to ignore our lispy `warn` wrapper.
768793 else :
0 commit comments