Skip to content

Commit 1c1dc1a

Browse files
Add Pythonx.eval/3 options for customizing stdout and stderr destination (#5)
1 parent f9dfafe commit 1c1dc1a

File tree

3 files changed

+49
-5
lines changed

3 files changed

+49
-5
lines changed

c_src/pythonx/pythonx.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,7 @@ extern "C" void pythonx_handle_io_write(const char *message,
13781378
enif_free_env(env);
13791379
} else {
13801380
std::cerr << "[pythonx] whereis(Pythonx.Janitor) failed. This is "
1381-
"unexpected and an output ill be dropped"
1381+
"unexpected and an output will be dropped"
13821382
<< std::endl;
13831383
}
13841384
}

lib/pythonx.ex

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ defmodule Pythonx do
137137
> those releases the GIL. GIL is also released when waiting on I/O
138138
> operations.
139139
140+
## Options
141+
142+
* `:stdout_device` - IO process to send Python stdout output to.
143+
Defaults to the caller's group leader.
144+
145+
* `:stderr_device` - IO process to send Python stderr output to.
146+
Defaults to the global `:standard_error`.
147+
140148
## Examples
141149
142150
iex> {result, globals} =
@@ -201,9 +209,12 @@ defmodule Pythonx do
201209
>
202210
203211
'''
204-
@spec eval(String.t(), %{optional(String.t()) => term()}) ::
212+
@spec eval(String.t(), %{optional(String.t()) => term()}, keyword()) ::
205213
{Object.t() | nil, %{optional(String.t()) => Object.t()}}
206-
def eval(code, globals) do
214+
def eval(code, globals, opts \\ [])
215+
when is_binary(code) and is_map(globals) and is_list(opts) do
216+
opts = Keyword.validate!(opts, [:stdout_device, :stderr_device])
217+
207218
globals =
208219
for {key, value} <- globals do
209220
if not is_binary(key) do
@@ -214,8 +225,11 @@ defmodule Pythonx do
214225
end
215226

216227
code_md5 = :erlang.md5(code)
217-
stdout_device = Process.group_leader()
218-
stderr_device = Process.whereis(:standard_error)
228+
229+
stdout_device = Keyword.get_lazy(opts, :stdout_device, fn -> Process.group_leader() end)
230+
231+
stderr_device =
232+
Keyword.get_lazy(opts, :stderr_device, fn -> Process.whereis(:standard_error) end)
219233

220234
result = Pythonx.NIF.eval(code, code_md5, globals, stdout_device, stderr_device)
221235

test/pythonx_test.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,36 @@ defmodule PythonxTest do
269269
end) =~ "error from Python\n"
270270
end
271271

272+
test "sends standard output and error to custom processes when specified" do
273+
{:ok, io} = StringIO.open("")
274+
275+
Pythonx.eval(
276+
"""
277+
import sys
278+
import threading
279+
280+
print("hello from Python")
281+
print("error from Python", file=sys.stderr)
282+
283+
def run():
284+
print("hello from thread")
285+
286+
thread = threading.Thread(target=run)
287+
thread.start()
288+
thread.join()
289+
""",
290+
%{},
291+
stdout_device: io,
292+
stderr_device: io
293+
)
294+
295+
{:ok, {_, output}} = StringIO.close(io)
296+
297+
assert output =~ "hello from Python"
298+
assert output =~ "error from Python"
299+
assert output =~ "hello from thread"
300+
end
301+
272302
test "raises Python error on stdin attempt" do
273303
assert_raise Pythonx.Error, ~r/RuntimeError: stdin not supported/, fn ->
274304
Pythonx.eval(

0 commit comments

Comments
 (0)