Skip to content

Commit

Permalink
Add Pythonx.eval/3 options for customizing stdout and stderr destinat…
Browse files Browse the repository at this point in the history
…ion (#5)
  • Loading branch information
jonatanklosko authored Feb 21, 2025
1 parent f9dfafe commit 1c1dc1a
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 5 deletions.
2 changes: 1 addition & 1 deletion c_src/pythonx/pythonx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
22 changes: 18 additions & 4 deletions lib/pythonx.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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} =
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down
30 changes: 30 additions & 0 deletions test/pythonx_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 1c1dc1a

Please sign in to comment.