Skip to content

Commit fb46c5f

Browse files
Add type, value, traceback objects back to Pythonx.Error (#47)
1 parent 173d14b commit fb46c5f

3 files changed

Lines changed: 118 additions & 88 deletions

File tree

c_src/pythonx.cpp

Lines changed: 58 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ auto map_set = fine::Atom("map_set");
146146
auto output = fine::Atom("output");
147147
auto remote_info = fine::Atom("remote_info");
148148
auto resource = fine::Atom("resource");
149+
auto traceback = fine::Atom("traceback");
149150
auto tuple = fine::Atom("tuple");
151+
auto type = fine::Atom("type");
152+
auto value = fine::Atom("value");
150153
} // namespace atoms
151154

152155
struct PyObjectResource {
@@ -221,14 +224,23 @@ struct ExObject {
221224

222225
struct ExError {
223226
std::vector<fine::Term> lines;
227+
ExObject type;
228+
ExObject value;
229+
ExObject traceback;
224230

225231
ExError() {}
226-
ExError(std::vector<fine::Term> lines) : lines(lines) {}
232+
ExError(std::vector<fine::Term> lines, ExObject type, ExObject value,
233+
ExObject traceback)
234+
: lines(lines), type(type), value(value), traceback(traceback) {}
227235

228236
static constexpr auto module = &atoms::ElixirPythonxError;
229237

230238
static constexpr auto fields() {
231-
return std::make_tuple(std::make_tuple(&ExError::lines, &atoms::lines));
239+
return std::make_tuple(
240+
std::make_tuple(&ExError::lines, &atoms::lines),
241+
std::make_tuple(&ExError::type, &atoms::type),
242+
std::make_tuple(&ExError::value, &atoms::value),
243+
std::make_tuple(&ExError::traceback, &atoms::traceback));
232244
}
233245

234246
static constexpr auto is_exception = true;
@@ -241,23 +253,9 @@ struct EvalInfo {
241253
std::thread::id thread_id;
242254
};
243255

244-
void raise_formatting_error_if_failed(PyObjectPtr py_object) {
245-
if (py_object == NULL) {
246-
throw std::runtime_error("failed while formatting a python error");
247-
}
248-
}
249-
250-
void raise_formatting_error_if_failed(const char *buffer) {
251-
if (buffer == NULL) {
252-
throw std::runtime_error("failed while formatting a python error");
253-
}
254-
}
255-
256-
void raise_formatting_error_if_failed(Py_ssize_t size) {
257-
if (size == -1) {
258-
throw std::runtime_error("failed while formatting a python error");
259-
}
260-
}
256+
std::vector<fine::Term> py_error_lines(ErlNifEnv *env, PyObjectPtr py_type,
257+
PyObjectPtr py_value,
258+
PyObjectPtr py_traceback);
261259

262260
ExError build_py_error_from_current(ErlNifEnv *env) {
263261
PyObjectPtr py_type, py_value, py_traceback;
@@ -270,58 +268,16 @@ ExError build_py_error_from_current(ErlNifEnv *env) {
270268
"called when the error indicator is set");
271269
}
272270

273-
auto type = ExObject(fine::make_resource<PyObjectResource>(py_type));
274-
275271
// Default value and traceback to None object.
276272
py_value = py_value == NULL ? Py_BuildValue("") : py_value;
277273
py_traceback = py_traceback == NULL ? Py_BuildValue("") : py_traceback;
278274

279-
// Format the exception. Note that if anything raises an error here,
280-
// we throw a runtime exception, instead of a Python one, otherwise
281-
// we could go into an infinite loop.
282-
283-
auto py_traceback_module = PyImport_ImportModule("traceback");
284-
raise_formatting_error_if_failed(py_traceback_module);
285-
auto py_traceback_module_guard = PyDecRefGuard(py_traceback_module);
286-
287-
auto format_exception =
288-
PyObject_GetAttrString(py_traceback_module, "format_exception");
289-
raise_formatting_error_if_failed(format_exception);
290-
auto format_exception_guard = PyDecRefGuard(format_exception);
291-
292-
auto format_exception_args = PyTuple_Pack(3, py_type, py_value, py_traceback);
293-
raise_formatting_error_if_failed(format_exception_args);
294-
auto format_exception_args_guard = PyDecRefGuard(format_exception_args);
295-
296-
auto py_lines = PyObject_Call(format_exception, format_exception_args, NULL);
297-
raise_formatting_error_if_failed(py_lines);
298-
auto py_lines_guard = PyDecRefGuard(py_lines);
299-
300-
auto size = PyList_Size(py_lines);
301-
raise_formatting_error_if_failed(size);
302-
303-
auto terms = std::vector<fine::Term>();
304-
terms.reserve(size);
305-
306-
for (Py_ssize_t i = 0; i < size; i++) {
307-
auto py_line = PyList_GetItem(py_lines, i);
308-
raise_formatting_error_if_failed(py_line);
309-
310-
Py_ssize_t size;
311-
auto buffer = PyUnicode_AsUTF8AndSize(py_line, &size);
312-
raise_formatting_error_if_failed(buffer);
275+
auto lines = py_error_lines(env, py_type, py_value, py_traceback);
276+
auto type = fine::make_resource<PyObjectResource>(py_type);
277+
auto value = fine::make_resource<PyObjectResource>(py_value);
278+
auto traceback = fine::make_resource<PyObjectResource>(py_traceback);
313279

314-
// The buffer is immutable and lives as long as the Python object,
315-
// so we create the term as a resource binary to make it zero-copy.
316-
Py_IncRef(py_line);
317-
auto ex_object_resource = fine::make_resource<PyObjectResource>(py_line);
318-
auto binary_term =
319-
fine::make_resource_binary(env, ex_object_resource, buffer, size);
320-
321-
terms.push_back(binary_term);
322-
}
323-
324-
return ExError(std::move(terms));
280+
return ExError(lines, type, value, traceback);
325281
}
326282

327283
void raise_py_error(ErlNifEnv *env) {
@@ -371,6 +327,42 @@ ERL_NIF_TERM py_bytes_to_binary_term(ErlNifEnv *env, PyObjectPtr py_object) {
371327
return fine::make_resource_binary(env, ex_object_resource, buffer, size);
372328
}
373329

330+
std::vector<fine::Term> py_error_lines(ErlNifEnv *env, PyObjectPtr py_type,
331+
PyObjectPtr py_value,
332+
PyObjectPtr py_traceback) {
333+
auto py_traceback_module = PyImport_ImportModule("traceback");
334+
raise_if_failed(env, py_traceback_module);
335+
auto py_traceback_module_guard = PyDecRefGuard(py_traceback_module);
336+
337+
auto format_exception =
338+
PyObject_GetAttrString(py_traceback_module, "format_exception");
339+
raise_if_failed(env, format_exception);
340+
auto format_exception_guard = PyDecRefGuard(format_exception);
341+
342+
auto format_exception_args = PyTuple_Pack(3, py_type, py_value, py_traceback);
343+
raise_if_failed(env, format_exception_args);
344+
auto format_exception_args_guard = PyDecRefGuard(format_exception_args);
345+
346+
auto py_lines = PyObject_Call(format_exception, format_exception_args, NULL);
347+
raise_if_failed(env, py_lines);
348+
auto py_lines_guard = PyDecRefGuard(py_lines);
349+
350+
auto size = PyList_Size(py_lines);
351+
raise_if_failed(env, size);
352+
353+
auto terms = std::vector<fine::Term>();
354+
terms.reserve(size);
355+
356+
for (Py_ssize_t i = 0; i < size; i++) {
357+
auto py_line = PyList_GetItem(py_lines, i);
358+
raise_if_failed(env, py_line);
359+
360+
terms.push_back(py_str_to_binary_term(env, py_line));
361+
}
362+
363+
return terms;
364+
}
365+
374366
fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
375367
ErlNifBinary python_home_path,
376368
ErlNifBinary python_executable_path,

lib/pythonx.ex

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -606,33 +606,51 @@ defmodule Pythonx do
606606
{:ok, binary} ->
607607
Pythonx.NIF.load_object(binary)
608608

609-
{:error, "pickle", %Pythonx.Error{} = error} ->
610-
raise ArgumentError, """
611-
failed to serialize the given object using the built-in pickle module. The pickle module does not support all object types, for extended pickling support add the following package:
612-
613-
cloudpickle==3.1.2
614-
615-
Original error: #{Exception.message(error)}
616-
"""
617-
618-
{:error, module, %Pythonx.Error{} = error} ->
619-
raise RuntimeError, """
620-
failed to serialize the given object using the #{module} module.
621-
622-
Original error: #{Exception.message(error)}
623-
"""
624-
625-
{:exception, exception} ->
609+
{:error, exception} ->
626610
raise exception
627611
end
628612
end
629613

630614
@doc false
631615
def __dump__(object) do
632616
try do
633-
Pythonx.NIF.dump_object(object)
617+
case Pythonx.NIF.dump_object(object) do
618+
{:ok, binary} ->
619+
{:ok, binary}
620+
621+
{:error, "pickle", %Pythonx.Error{} = error} ->
622+
{:error,
623+
ArgumentError.exception("""
624+
failed to serialize the given object using the built-in pickle module. The pickle module does not support all object types, for extended pickling support add the following package:
625+
626+
cloudpickle==3.1.2
627+
628+
Original error: #{Exception.message(error)}
629+
""")}
630+
631+
{:error, module, %Pythonx.Error{} = error} ->
632+
{:error,
633+
RuntimeError.exception("""
634+
failed to serialize the given object using the #{module} module.
635+
636+
Original error: #{Exception.message(error)}
637+
""")}
638+
end
634639
rescue
635-
error -> {:exception, error}
640+
error in Pythonx.Error ->
641+
# We don't want to return Pythonx.Error as is, because we
642+
# would need more elaborate logic to track it, like we do in
643+
# remote_eval/4, so we convert it into a RuntimeError instead.
644+
# This should only really happen if there is an implementation
645+
# error in Pythonx itself, since pickling errors are handled
646+
# explicitly above.
647+
{:error,
648+
RuntimeError.exception("""
649+
failed to serialize the given object, got Python exception: #{Exception.message(error)}
650+
""")}
651+
652+
error ->
653+
{:error, error}
636654
end
637655
end
638656

@@ -688,6 +706,13 @@ defmodule Pythonx do
688706

689707
{^message_ref, {:exception, error}} ->
690708
Process.demonitor(monitor_ref, [:flush])
709+
710+
error =
711+
case error do
712+
%Pythonx.Error{} = error -> track_object(error)
713+
error -> error
714+
end
715+
691716
send(child, {message_ref, :ok})
692717
raise error
693718

@@ -737,10 +762,18 @@ defmodule Pythonx do
737762

738763
defp encode_with_copy_remote(value, encoder), do: Pythonx.Encoder.encode(value, encoder)
739764

740-
defp track_object(object) do
765+
defp track_object(%Pythonx.Object{} = object) do
741766
case Pythonx.ObjectTracker.track_remote_object(object) do
742767
{:noop, object} -> object
743768
{:ok, object, _marker_pid} -> object
744769
end
745770
end
771+
772+
defp track_object(%Pythonx.Error{type: type, value: value, traceback: traceback}) do
773+
%Pythonx.Error{
774+
type: track_object(type),
775+
value: track_object(value),
776+
traceback: track_object(traceback)
777+
}
778+
end
746779
end

lib/pythonx/error.ex

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ defmodule Pythonx.Error do
33
An exception raised when Python raises an exception.
44
"""
55

6-
defexception [:lines]
6+
defexception [:lines, :type, :value, :traceback]
77

8-
@type t :: %__MODULE__{lines: [String.t()]}
8+
@type t :: %__MODULE__{
9+
lines: [String.t()],
10+
type: Pythonx.Object.t(),
11+
value: Pythonx.Object.t(),
12+
traceback: Pythonx.Object.t()
13+
}
914

1015
@impl true
1116
def message(error) do

0 commit comments

Comments
 (0)