1 /*
2 Copyright (c) 2006 Kirk McDonald
3 
4 Permission is hereby granted, free of charge, to any person obtaining a copy of
5 this software and associated documentation files (the "Software"), to deal in
6 the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 of the Software, and to permit persons to whom the Software is furnished to do
9 so, subject to the following conditions:
10 
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13 
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 */
22 
23 /**
24   Contains utilities for safely wrapping python exceptions in D and vice versa.
25   */
26 module pyd.exception;
27 
28 import std.conv;
29 import std.string: format;
30 import std.string;
31 import deimos.python.Python;
32 import std.traits : fullyQualifiedName;
33 
34 /**
35  * This function first checks if a Python exception is set, and then (if one
36  * is) pulls it out, stuffs it in a PythonException, and throws that exception.
37  *
38  * If this exception is never caught, it will be handled by exception_catcher
39  * (below) and passed right back into Python as though nothing happened.
40  */
41 void handle_exception(string file = __FILE__, size_t line = __LINE__) {
42     PyObject* type, value, traceback;
43     if (PyErr_Occurred() !is null) {
44         PyErr_Fetch(&type, &value, &traceback);
45         PyErr_NormalizeException(&type, &value, &traceback);
46         throw new PythonException(type, value, traceback,file,line);
47     }
48 }
49 
50 // Used internally.
51 T error_code(T) () {
52     static if (is(T == PyObject*)) {
53         return null;
54     } else static if (is(T == int)) {
55         return -1;
56     } else static if (is(T == Py_ssize_t)) {
57         return -1;
58     } else static if (is(T == void)) {
59         return;
60     } else static assert(false, "exception_catcher cannot handle return type " ~ fullyQualifiedName!T);
61 }
62 
63 /**
64  * It is intended that any functions that interface directly with Python which
65  * have the possibility of a D exception being raised wrap their contents in a
66  * call to this function, e.g.:
67  *
68  *$(D_CODE extern (C)
69  *PyObject* some_func(PyObject* self) {
70  *    return _exception_catcher({
71  *        // ...
72  *    });
73  *})
74  */
75 T exception_catcher(T) (T delegate() dg) {
76     try {
77         return dg();
78     }
79     // A Python exception was raised and duly re-thrown as a D exception.
80     // It should now be re-raised as a Python exception.
81     catch (PythonException e) {
82         PyErr_Restore(e.type(), e.value(), e.traceback());
83         return error_code!(T)();
84     }
85     // A D exception was raised and should be translated into a meaningful
86     // Python exception.
87     catch (Exception e) {
88         PyErr_SetString(PyExc_RuntimeError, ("D Exception:\n" ~ e.toString() ~ "\0").ptr);
89         return error_code!(T)();
90     }
91     // Some other D object was thrown. Deal with it.
92     catch (Throwable o) {
93         PyErr_SetString(PyExc_RuntimeError, ("thrown D Object: " ~ o.classinfo.name ~ ": " ~ o.toString() ~ "\0").ptr);
94         return error_code!(T)();
95     }
96 }
97 
98 // waaa! std.string.format (and likely Object.toString) do gc allocations!
99 T exception_catcher_nogc(T) (T delegate() dg) {
100     try {
101         return dg();
102     }
103     // A Python exception was raised and duly re-thrown as a D exception.
104     // It should now be re-raised as a Python exception.
105     catch (PythonException e) {
106         PyErr_Restore(e.type(), e.value(), e.traceback());
107         return error_code!(T)();
108     }
109     // A D exception was raised and should be translated into a meaningful
110     // Python exception.
111     catch (Throwable e) {
112         //auto clz1 = e.classinfo;
113         //const(char)* clz = e.classinfo.name.ptr;
114         //const(char)* msg = e.msg.ptr;
115         //const(char)* file = e.file.ptr;
116         PyObject* p = PyBytes_FromFormat("some thrown D object:\0",
117                 /*clz, msg, file, e.line*/);
118         PyErr_SetObject(PyExc_RuntimeError, p);
119         Py_DECREF(p); // PyErr_SetObject has ownership of it now
120         return error_code!(T)();
121     }
122 }
123 
124 alias exception_catcher!(PyObject*) exception_catcher_PyObjectPtr;
125 alias exception_catcher!(int) exception_catcher_int;
126 alias exception_catcher!(void) exception_catcher_void;
127 
128 string printSyntaxError(PyObject* type, PyObject* value, PyObject* traceback) {
129     if(value is null) return "";
130     string text;
131     auto ptext = PyObject_GetAttrString(value, "text");
132     if(ptext) {
133         version(Python_3_0_Or_Later) {
134             ptext = PyUnicode_AsUTF8String(ptext);
135         }
136         auto p2text = PyBytes_AsString(ptext);
137         if(p2text) text = strip(to!string(p2text));
138     }
139     C_long offset;
140     auto poffset = PyObject_GetAttrString(value, "offset");
141     if(poffset) {
142         offset = PyLong_AsLong(poffset);
143     }
144     auto valtype = to!string(value.ob_type.tp_name);
145 
146     string message;
147     auto pmsg = PyObject_GetAttrString(value, "msg");
148     if(pmsg) {
149         version(Python_3_0_Or_Later) {
150             pmsg = PyUnicode_AsUTF8String(pmsg);
151         }
152         auto cmsg = PyBytes_AsString(pmsg);
153         if(cmsg) message = to!string(cmsg);
154     }
155     string space = "";
156     foreach(i; 0 .. offset-1) space ~= " ";
157     return format(q"{
158     %s
159     %s^
160 %s: %s}", text, space,valtype, message);
161 }
162 
163 string printGenericError(PyObject* type, PyObject* value, PyObject* traceback) {
164     if(value is null) return "";
165     auto valtype = to!string(value.ob_type.tp_name);
166 
167     string message;
168     version(Python_3_0_Or_Later) {
169         PyObject* uni = PyObject_Str(value);
170     }else{
171         PyObject* uni = PyObject_Unicode(value);
172     }
173     if(!uni) {
174         PyErr_Clear();
175         return "";
176     }
177     PyObject* str = PyUnicode_AsUTF8String(uni);
178     if(!str) {
179         PyErr_Clear();
180         return "";
181     }
182     auto cmsg = PyBytes_AsString(str);
183     if(cmsg) message = to!string(cmsg);
184     return format(q"{
185 %s: %s}", valtype, message);
186 }
187 
188 /**
189  * This simple exception class holds a Python exception.
190  */
191 class PythonException : Exception {
192 protected:
193     PyObject* m_type, m_value, m_trace;
194 public:
195     this(PyObject* type, PyObject* value, PyObject* traceback, string file = __FILE__, size_t line = __LINE__) {
196         if(PyObject_IsInstance(value, cast(PyObject*)PyExc_SyntaxError)) {
197             super(printSyntaxError(type, value, traceback), file, line);
198         }else{
199             super(printGenericError(type, value, traceback), file, line);
200         }
201         m_type = type;
202         m_value = value;
203         m_trace = traceback;
204     }
205 
206     ~this() {
207         if (m_type) Py_DECREF(m_type);
208         if (m_value) Py_DECREF(m_value);
209         if (m_trace) Py_DECREF(m_trace);
210     }
211 
212     PyObject* type() {
213         if (m_type) Py_INCREF(m_type);
214         return m_type;
215     }
216     PyObject* value() {
217         if (m_value) Py_INCREF(m_value);
218         return m_value;
219     }
220     PyObject* traceback() {
221         if (m_trace) Py_INCREF(m_trace);
222         return m_trace;
223     }
224 
225     @property py_message() {
226         string message;
227         PyObject* pmsg;
228         if(m_value) {
229             if(PyObject_IsInstance(m_value, cast(PyObject*)PyExc_SyntaxError)) {
230                 pmsg = PyObject_GetAttrString(m_value, "msg");
231             }else{
232                 // todo: test this on other versions..
233                 version(Python_3_2_Or_Later) {
234                     pmsg = PyObject_GetAttrString(m_value, "args");
235                     if(pmsg != null && PyTuple_Check(pmsg) &&
236                             PyTuple_Size(pmsg) >= 1) {
237                         pmsg = cast(PyObject*) PyTuple_GetItem(pmsg, 0);
238                     }
239 
240                 }else{
241                     pmsg = PyObject_GetAttrString(m_value, "message");
242                 }
243             }
244             if(pmsg) {
245                 import pyd.make_object;
246                 message = python_to_d!string(pmsg);
247             }
248         }
249 
250         return message;
251     }
252 
253     @property py_offset() {
254         C_long offset = -1;
255         if(m_value) {
256             auto poffset = PyObject_GetAttrString(m_value, "offset");
257             if(poffset) {
258                 offset = PyLong_AsLong(poffset);
259             }
260         }
261         return offset;
262     }
263 }
264