Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 58 additions & 66 deletions c_src/pythonx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ auto map_set = fine::Atom("map_set");
auto output = fine::Atom("output");
auto remote_info = fine::Atom("remote_info");
auto resource = fine::Atom("resource");
auto traceback = fine::Atom("traceback");
auto tuple = fine::Atom("tuple");
auto type = fine::Atom("type");
auto value = fine::Atom("value");
} // namespace atoms

struct PyObjectResource {
Expand Down Expand Up @@ -221,14 +224,23 @@ struct ExObject {

struct ExError {
std::vector<fine::Term> lines;
ExObject type;
ExObject value;
ExObject traceback;

ExError() {}
ExError(std::vector<fine::Term> lines) : lines(lines) {}
ExError(std::vector<fine::Term> lines, ExObject type, ExObject value,
ExObject traceback)
: lines(lines), type(type), value(value), traceback(traceback) {}

static constexpr auto module = &atoms::ElixirPythonxError;

static constexpr auto fields() {
return std::make_tuple(std::make_tuple(&ExError::lines, &atoms::lines));
return std::make_tuple(
std::make_tuple(&ExError::lines, &atoms::lines),
std::make_tuple(&ExError::type, &atoms::type),
std::make_tuple(&ExError::value, &atoms::value),
std::make_tuple(&ExError::traceback, &atoms::traceback));
}

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

void raise_formatting_error_if_failed(PyObjectPtr py_object) {
if (py_object == NULL) {
throw std::runtime_error("failed while formatting a python error");
}
}

void raise_formatting_error_if_failed(const char *buffer) {
if (buffer == NULL) {
throw std::runtime_error("failed while formatting a python error");
}
}

void raise_formatting_error_if_failed(Py_ssize_t size) {
if (size == -1) {
throw std::runtime_error("failed while formatting a python error");
}
}
std::vector<fine::Term> py_error_lines(ErlNifEnv *env, PyObjectPtr py_type,
PyObjectPtr py_value,
PyObjectPtr py_traceback);

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

auto type = ExObject(fine::make_resource<PyObjectResource>(py_type));

// Default value and traceback to None object.
py_value = py_value == NULL ? Py_BuildValue("") : py_value;
py_traceback = py_traceback == NULL ? Py_BuildValue("") : py_traceback;

// Format the exception. Note that if anything raises an error here,
// we throw a runtime exception, instead of a Python one, otherwise
// we could go into an infinite loop.

auto py_traceback_module = PyImport_ImportModule("traceback");
raise_formatting_error_if_failed(py_traceback_module);
auto py_traceback_module_guard = PyDecRefGuard(py_traceback_module);

auto format_exception =
PyObject_GetAttrString(py_traceback_module, "format_exception");
raise_formatting_error_if_failed(format_exception);
auto format_exception_guard = PyDecRefGuard(format_exception);

auto format_exception_args = PyTuple_Pack(3, py_type, py_value, py_traceback);
raise_formatting_error_if_failed(format_exception_args);
auto format_exception_args_guard = PyDecRefGuard(format_exception_args);

auto py_lines = PyObject_Call(format_exception, format_exception_args, NULL);
raise_formatting_error_if_failed(py_lines);
auto py_lines_guard = PyDecRefGuard(py_lines);

auto size = PyList_Size(py_lines);
raise_formatting_error_if_failed(size);

auto terms = std::vector<fine::Term>();
terms.reserve(size);

for (Py_ssize_t i = 0; i < size; i++) {
auto py_line = PyList_GetItem(py_lines, i);
raise_formatting_error_if_failed(py_line);

Py_ssize_t size;
auto buffer = PyUnicode_AsUTF8AndSize(py_line, &size);
raise_formatting_error_if_failed(buffer);
auto lines = py_error_lines(env, py_type, py_value, py_traceback);
auto type = fine::make_resource<PyObjectResource>(py_type);
auto value = fine::make_resource<PyObjectResource>(py_value);
auto traceback = fine::make_resource<PyObjectResource>(py_traceback);

// The buffer is immutable and lives as long as the Python object,
// so we create the term as a resource binary to make it zero-copy.
Py_IncRef(py_line);
auto ex_object_resource = fine::make_resource<PyObjectResource>(py_line);
auto binary_term =
fine::make_resource_binary(env, ex_object_resource, buffer, size);

terms.push_back(binary_term);
}

return ExError(std::move(terms));
return ExError(lines, type, value, traceback);
}

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

std::vector<fine::Term> py_error_lines(ErlNifEnv *env, PyObjectPtr py_type,
PyObjectPtr py_value,
PyObjectPtr py_traceback) {
auto py_traceback_module = PyImport_ImportModule("traceback");
raise_if_failed(env, py_traceback_module);
auto py_traceback_module_guard = PyDecRefGuard(py_traceback_module);

auto format_exception =
PyObject_GetAttrString(py_traceback_module, "format_exception");
raise_if_failed(env, format_exception);
auto format_exception_guard = PyDecRefGuard(format_exception);

auto format_exception_args = PyTuple_Pack(3, py_type, py_value, py_traceback);
raise_if_failed(env, format_exception_args);
auto format_exception_args_guard = PyDecRefGuard(format_exception_args);

auto py_lines = PyObject_Call(format_exception, format_exception_args, NULL);
raise_if_failed(env, py_lines);
auto py_lines_guard = PyDecRefGuard(py_lines);

auto size = PyList_Size(py_lines);
raise_if_failed(env, size);

auto terms = std::vector<fine::Term>();
terms.reserve(size);

for (Py_ssize_t i = 0; i < size; i++) {
auto py_line = PyList_GetItem(py_lines, i);
raise_if_failed(env, py_line);

terms.push_back(py_str_to_binary_term(env, py_line));
}

return terms;
}

fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
ErlNifBinary python_home_path,
ErlNifBinary python_executable_path,
Expand Down
73 changes: 53 additions & 20 deletions lib/pythonx.ex
Original file line number Diff line number Diff line change
Expand Up @@ -606,33 +606,51 @@ defmodule Pythonx do
{:ok, binary} ->
Pythonx.NIF.load_object(binary)

{:error, "pickle", %Pythonx.Error{} = error} ->
raise ArgumentError, """
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:

cloudpickle==3.1.2

Original error: #{Exception.message(error)}
"""

{:error, module, %Pythonx.Error{} = error} ->
raise RuntimeError, """
failed to serialize the given object using the #{module} module.

Original error: #{Exception.message(error)}
"""

{:exception, exception} ->
{:error, exception} ->
raise exception
end
end

@doc false
def __dump__(object) do
try do
Pythonx.NIF.dump_object(object)
case Pythonx.NIF.dump_object(object) do
{:ok, binary} ->
{:ok, binary}

{:error, "pickle", %Pythonx.Error{} = error} ->
{:error,
ArgumentError.exception("""
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:

cloudpickle==3.1.2

Original error: #{Exception.message(error)}
""")}

{:error, module, %Pythonx.Error{} = error} ->
{:error,
RuntimeError.exception("""
failed to serialize the given object using the #{module} module.

Original error: #{Exception.message(error)}
""")}
end
rescue
error -> {:exception, error}
error in Pythonx.Error ->
# We don't want to return Pythonx.Error as is, because we
# would need more elaborate logic to track it, like we do in
# remote_eval/4, so we convert it into a RuntimeError instead.
# This should only really happen if there is an implementation
# error in Pythonx itself, since pickling errors are handled
# explicitly above.
{:error,
RuntimeError.exception("""
failed to serialize the given object, got Python exception: #{Exception.message(error)}
""")}

error ->
{:error, error}
end
end

Expand Down Expand Up @@ -688,6 +706,13 @@ defmodule Pythonx do

{^message_ref, {:exception, error}} ->
Process.demonitor(monitor_ref, [:flush])

error =
case error do
%Pythonx.Error{} = error -> track_object(error)
error -> error
end

send(child, {message_ref, :ok})
raise error

Expand Down Expand Up @@ -737,10 +762,18 @@ defmodule Pythonx do

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

defp track_object(object) do
defp track_object(%Pythonx.Object{} = object) do
case Pythonx.ObjectTracker.track_remote_object(object) do
{:noop, object} -> object
{:ok, object, _marker_pid} -> object
end
end

defp track_object(%Pythonx.Error{type: type, value: value, traceback: traceback}) do
%Pythonx.Error{
type: track_object(type),
value: track_object(value),
traceback: track_object(traceback)
}
end
end
9 changes: 7 additions & 2 deletions lib/pythonx/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ defmodule Pythonx.Error do
An exception raised when Python raises an exception.
"""

defexception [:lines]
defexception [:lines, :type, :value, :traceback]

@type t :: %__MODULE__{lines: [String.t()]}
@type t :: %__MODULE__{
lines: [String.t()],
type: Pythonx.Object.t(),
value: Pythonx.Object.t(),
traceback: Pythonx.Object.t()
}

@impl true
def message(error) do
Expand Down
Loading