Skip to content

Commit d90249c

Browse files
chrisrink10Christopher Rink
andauthored
Add basilisp.stacktrace namespace (#864)
Fixes #721 --------- Co-authored-by: Christopher Rink <[email protected]>
1 parent b6d7fa2 commit d90249c

File tree

3 files changed

+136
-6
lines changed

3 files changed

+136
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
* Added filename metadata to compiler exceptions (#844)
1010
* Added a compile-time warning for attempting to call a function with an unsupported number of arguments (#671)
1111
* Added support for explicit cause exception chaining to the `throw` special form (#862)
12+
* Added `basilisp.stacktrace` namespace (#721)
1213

1314
### Changed
1415
* Cause exceptions arising from compilation issues during macroexpansion will no longer be nested for each level of macroexpansion (#852)

src/basilisp/stacktrace.lpy

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,83 @@
11
(ns basilisp.stacktrace
2-
"Prints stacktraces."
2+
"Utility functions for printing stack traces."
33
(:require [basilisp.string :as str])
44
(:import [traceback :as tb]))
55

6+
(defn root-cause
7+
"Return the root cause exception of the possible chain of exceptions ``exc``."
8+
[^python/BaseException exc]
9+
(loop [e exc]
10+
(if-let [cause (.-__cause__ e)]
11+
(recur cause)
12+
e)))
13+
14+
(defn context
15+
"Return any context exception to the exception ``exc``.
16+
17+
Context exceptions may be the same as cause exceptions. Typically, when throwing an
18+
exception with an explicit cause the context exception is suppressed (via
19+
``BaseException.__suppress_context__``). If called with one argument, this function
20+
will use the value of ``__suppress_context__`` for ``suppress-context?``. If called
21+
with two arguments, the caller can specify if context should be returned or suppressed."
22+
([^python/BaseException exc]
23+
(context exc (.-__suppress_context__ exc)))
24+
([^python/BaseException exc suppress-context?]
25+
(when-not suppress-context?
26+
(.-__context__ exc))))
27+
28+
(defn print-stack-trace
29+
"Prints up to ``n`` stack frames from the traceback of the exception ``exc``, not
30+
including chained exceptions (causes and context exceptions).
31+
32+
To print exception tracebacks including causes, use :lpy:fn:`print-cause-trace`.
33+
34+
If ``n`` is not given, return all frames."
35+
([exc]
36+
(print-stack-trace exc nil))
37+
([exc n]
38+
(->> (tb/format_exception (python/type exc)
39+
exc
40+
(.-__traceback__ exc)
41+
**
42+
:limit n
43+
:chain false)
44+
(str/join " ")
45+
print)))
46+
647
(defn print-cause-trace
7-
"Prints the stacktrace of chained ``exc`` (cause), using ``n`` stack
8-
frames (defaults to all)."
48+
"Prints up to ``n`` stack frames from the traceback of the exception ``exc``,
49+
including chained exceptions (causes and context exceptions).
50+
51+
To print only the trace for the given exception, use :lpy:fn:`print-stack-trace`.
52+
53+
If ``n`` is not given, return all frames."
954
([exc]
1055
(print-cause-trace exc nil))
1156
([exc n]
12-
(print (str/join " " (tb/format_exception (python/type exc) exc (.-__traceback__ exc)
13-
** :limit n :chain true)))))
57+
(->> (tb/format_exception (python/type exc)
58+
exc
59+
(.-__traceback__ exc)
60+
**
61+
:limit n
62+
:chain true)
63+
(str/join " ")
64+
print)))
65+
66+
(defn print-throwable
67+
"Print the type and message of exception ``exc``.
68+
69+
Prints the :lpy:fn:`ex-data` map if present."
70+
[exc]
71+
(let [exc-type (type exc)
72+
data-str (if-let [d (ex-data exc)]
73+
(str " " d)
74+
"")]
75+
(println
76+
(str (.-__module__ exc-type) "." (.-__qualname__ exc-type) ": " (ex-message exc) data-str))))
77+
78+
(defn e
79+
"REPL utility for printing the root cause (via :lpy:fn:`root-cause`) of :lpy:var:`*e`
80+
if an exception is bound."
81+
[]
82+
(when *e
83+
(print-stack-trace (root-cause *e))))

tests/basilisp/test_stacktrace.lpy

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,53 @@
77
(defn- exception-test []
88
(/ 5 0))
99

10-
(deftest stacktrace-basic
10+
(defn- chained-exception []
11+
(try
12+
(/ 5.0 0)
13+
(catch python/ZeroDivisionError e
14+
(throw (python/ValueError "Division by zero") e))))
15+
16+
(defn- context-exception []
17+
(try
18+
(/ 5.0 0)
19+
(catch python/ZeroDivisionError e
20+
(throw (python/ValueError "Division by zero")))))
21+
22+
(deftest root-cause-test
23+
(testing "no root cause"
24+
(try
25+
(exception-test)
26+
(catch python/ZeroDivisionError e
27+
(is (identical? e (s/root-cause e))))))
28+
29+
(testing "with root cause"
30+
(try
31+
(chained-exception)
32+
(catch python/ZeroDivisionError _
33+
(is false))
34+
(catch python/ValueError e
35+
(is (instance? python/ZeroDivisionError (s/root-cause e)))))))
36+
37+
(deftest context-test
38+
(testing "context only exception"
39+
(try
40+
(context-exception)
41+
(catch python/ZeroDivisionError _
42+
(is false))
43+
(catch python/ValueError e
44+
(is (identical? e (s/root-cause e)))
45+
(is (instance? python/ZeroDivisionError (s/context e))))))
46+
47+
(testing "explicit cause exception"
48+
(try
49+
(chained-exception)
50+
(catch python/ValueError e
51+
(is (instance? python/ZeroDivisionError (s/root-cause e)))
52+
(is (instance? python/ZeroDivisionError (s/context e false)))
53+
(is (identical? (s/root-cause e) (s/context e false)))
54+
(is (nil? (s/context e)))))))
55+
56+
(deftest print-cause-trace-test
1157
(try
1258
(exception-test)
1359
(catch python/Exception e
@@ -25,3 +71,16 @@
2571
(is (= "Traceback (most recent call last):" (first trace)))
2672
(is (= [" raise ZeroDivisionError('Fraction(%s, 0)' % numerator)"
2773
" ZeroDivisionError: Fraction(5, 0)" ] (take-last 2 trace)))))))
74+
75+
(deftest print-throwable
76+
(try
77+
(exception-test)
78+
(catch python/ZeroDivisionError e
79+
(is (= "builtins.ZeroDivisionError: Fraction(5, 0)"
80+
(str/trim (with-out-str (s/print-throwable e)))))))
81+
82+
(try
83+
(throw (ex-info "Super bad exception" {:severity :bad!}))
84+
(catch python/Exception e
85+
(is (= "basilisp.lang.exception.ExceptionInfo: Super bad exception {:severity :bad!}"
86+
(str/trim (with-out-str (s/print-throwable e))))))))

0 commit comments

Comments
 (0)