diff --git a/c_src/pythonx/pythonx.cpp b/c_src/pythonx/pythonx.cpp index 44d38c5..8f79b65 100644 --- a/c_src/pythonx/pythonx.cpp +++ b/c_src/pythonx/pythonx.cpp @@ -1378,7 +1378,7 @@ extern "C" void pythonx_handle_io_write(const char *message, enif_free_env(env); } else { std::cerr << "[pythonx] whereis(Pythonx.Janitor) failed. This is " - "unexpected and an output ill be dropped" + "unexpected and an output will be dropped" << std::endl; } } diff --git a/lib/pythonx.ex b/lib/pythonx.ex index 5513791..b922eec 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -137,6 +137,14 @@ defmodule Pythonx do > those releases the GIL. GIL is also released when waiting on I/O > operations. + ## Options + + * `:stdout_device` - IO process to send Python stdout output to. + Defaults to the caller's group leader. + + * `:stderr_device` - IO process to send Python stderr output to. + Defaults to the global `:standard_error`. + ## Examples iex> {result, globals} = @@ -201,9 +209,12 @@ defmodule Pythonx do > ''' - @spec eval(String.t(), %{optional(String.t()) => term()}) :: + @spec eval(String.t(), %{optional(String.t()) => term()}, keyword()) :: {Object.t() | nil, %{optional(String.t()) => Object.t()}} - def eval(code, globals) do + def eval(code, globals, opts \\ []) + when is_binary(code) and is_map(globals) and is_list(opts) do + opts = Keyword.validate!(opts, [:stdout_device, :stderr_device]) + globals = for {key, value} <- globals do if not is_binary(key) do @@ -214,8 +225,11 @@ defmodule Pythonx do end code_md5 = :erlang.md5(code) - stdout_device = Process.group_leader() - stderr_device = Process.whereis(:standard_error) + + stdout_device = Keyword.get_lazy(opts, :stdout_device, fn -> Process.group_leader() end) + + stderr_device = + Keyword.get_lazy(opts, :stderr_device, fn -> Process.whereis(:standard_error) end) result = Pythonx.NIF.eval(code, code_md5, globals, stdout_device, stderr_device) diff --git a/test/pythonx_test.exs b/test/pythonx_test.exs index 65815c8..7f57e1a 100644 --- a/test/pythonx_test.exs +++ b/test/pythonx_test.exs @@ -269,6 +269,36 @@ defmodule PythonxTest do end) =~ "error from Python\n" end + test "sends standard output and error to custom processes when specified" do + {:ok, io} = StringIO.open("") + + Pythonx.eval( + """ + import sys + import threading + + print("hello from Python") + print("error from Python", file=sys.stderr) + + def run(): + print("hello from thread") + + thread = threading.Thread(target=run) + thread.start() + thread.join() + """, + %{}, + stdout_device: io, + stderr_device: io + ) + + {:ok, {_, output}} = StringIO.close(io) + + assert output =~ "hello from Python" + assert output =~ "error from Python" + assert output =~ "hello from thread" + end + test "raises Python error on stdin attempt" do assert_raise Pythonx.Error, ~r/RuntimeError: stdin not supported/, fn -> Pythonx.eval(