Rupyex (Rupyex v0.1.0)

Copy Markdown View Source

Python as an embedded language for Elixir, powered by RustPython through Rustler.

No Python installation is involved: the interpreter — and, by default, the standard library — is compiled into the NIF.

{:ok, session} = Rupyex.open()

{:ok, 3} = Rupyex.eval(session, "1 + 2")
{:ok, nil} = Rupyex.eval(session, "def double(x): return x * 2")
{:ok, 42} = Rupyex.call(session, "double", [21])

{:ok, %{"total" => 6}} =
  Rupyex.eval(session, "{'total': sum(xs)}", bind: %{"xs" => [1, 2, 3]})

Sessions hold state

A session is a live namespace: names bound by one call are visible to the next, exactly like a REPL. Sessions are cheap but not free (each owns an OS thread), so keep one around for as long as the state matters and close it when you are done. For a single throwaway snippet, use eval_once/2.

Values

PythonElixir
Nonenil
True / Falsetrue / false
intinteger (of any size)
floatfloat
strbinary
bytes, bytearrayRupyex.Bytes
listlist
tupletuple
dictmap
set, frozensetRupyex.Set
nan, inf, -inf:nan, :infinity, :neg_infinity
anything elseRupyex.Object (class name and repr)

Going the other way, atoms other than nil/true/false become strings, and a binary becomes a str when it is valid UTF-8 and bytes when it is not. Rupyex.Object values cannot be passed back in — the object never left the interpreter, so refer to it by name instead.

Values are copied rather than shared, and anything that cannot cross fails with a kind: :conversion error. See the data exchange guide for the whole story.

Timeouts

Every call takes a :timeout (default 5s). Python code that overruns it is interrupted with a KeyboardInterrupt at its next safe point, so a runaway loop cannot pin a scheduler or leak a thread. Python never runs on a BEAM scheduler in the first place: each session owns a thread, and calls are ordinary message round-trips.

Output

print output is captured per call. eval/3 discards it; run/3 returns it in a Rupyex.Result, and an error carries whatever was printed before it was raised. Pass capture_output: false to open/1 to let Python write to the BEAM's own stdout instead.

Summary

Types

Options accepted by eval/3, run/3 and call/4

Functions

Call a Python callable by name and return its value.

Same as call/4, but returns the value directly and raises on failure.

Stop a session.

Unbind a name from the session namespace.

Run Python source and return its value.

Same as eval/3, but returns the value directly and raises Rupyex.Error on failure.

Run a snippet in a fresh session and throw the session away.

Read a name from the session namespace.

Abort whatever the session is running right now.

Start a session. See Rupyex.Session.open/1 for the options.

Start a session, raising on failure.

Bind a name in the session namespace.

Throw the namespace away and start from a clean one.

Run Python source and return its value along with everything it printed.

Whether this build embeds the Python standard library.

The names bound in the session namespace, excluding dunders.

Types

option()

@type option() ::
  {:timeout, timeout()}
  | {:bind, map() | keyword()}
  | {:mode, :block | :eval | :exec}
  | {:file, String.t()}

Options accepted by eval/3, run/3 and call/4:

  • :timeout — override the session default for this call
  • :bind — names to bind in the session namespace before running the code, as a map or keyword list (eval/3 and run/3 only)
  • :mode — how to compile the source (eval/3 and run/3 only):
    • :block (default) — run the statements, return the value of the last one
    • :eval — a single expression
    • :exec — statements only, always returns nil
  • :file — the file name shown in tracebacks (default "<rupyex>")

session()

@type session() :: Rupyex.Session.t()

value()

@type value() :: term()

Functions

call(session, target, args \\ [], kwargs \\ [])

@spec call(session(), String.t(), [value()], keyword() | map()) ::
  {:ok, value()} | {:error, Rupyex.Error.t()}

Call a Python callable by name and return its value.

The name is resolved against the session namespace first, then the builtins, and may walk attributes:

{:ok, 3} = Rupyex.call(session, "len", ["abc"])
{:ok, "[1, 2]"} = Rupyex.call(session, "json.dumps", [[1, 2]])

Keyword arguments go in a separate list, since Python distinguishes them from positional ones:

{:ok, "{

"a": 1 }"} =

  Rupyex.call(session, "json.dumps", [%{"a" => 1}], indent: 2)

:timeout in that list is read as a call option rather than a keyword argument; pass a map to send it to Python instead.

call!(session, target, args \\ [], kwargs \\ [])

@spec call!(session(), String.t(), [value()], keyword() | map()) :: value()

Same as call/4, but returns the value directly and raises on failure.

close(session)

@spec close(session()) :: :ok

Stop a session.

delete(session, name, opts \\ [])

@spec delete(session(), String.t(), [option()]) :: :ok | {:error, Rupyex.Error.t()}

Unbind a name from the session namespace.

eval(session, code, opts \\ [])

@spec eval(session(), String.t(), [option()]) ::
  {:ok, value()} | {:error, Rupyex.Error.t()}

Run Python source and return its value.

{:ok, 3} = Rupyex.eval(session, "1 + 2")
{:ok, 6} = Rupyex.eval(session, "x = 1 + 2\nx * 2")
{:ok, 5} = Rupyex.eval(session, "a + b", bind: [a: 2, b: 3])

Anything the code prints is discarded; use run/3 to keep it.

eval!(session, code, opts \\ [])

@spec eval!(session(), String.t(), [option()]) :: value()

Same as eval/3, but returns the value directly and raises Rupyex.Error on failure.

eval_once(code, opts \\ [])

@spec eval_once(String.t(), [option() | Rupyex.Session.open_option()]) ::
  {:ok, value()} | {:error, Rupyex.Error.t()}

Run a snippet in a fresh session and throw the session away.

Convenient for one-off evaluation; if you are going to run more than one snippet, open/1 a session and reuse it — starting an interpreter costs far more than a call into one.

{:ok, 4} = Rupyex.eval_once("2 ** 2")

get(session, name, opts \\ [])

@spec get(session(), String.t(), [option()]) ::
  {:ok, value()} | {:error, Rupyex.Error.t()}

Read a name from the session namespace.

:ok = Rupyex.put(session, "x", 1)
{:ok, 1} = Rupyex.get(session, "x")

interrupt(session)

@spec interrupt(session()) :: :ok

Abort whatever the session is running right now.

open(opts \\ [])

@spec open([Rupyex.Session.open_option()]) ::
  {:ok, session()} | {:error, Rupyex.Error.t()}

Start a session. See Rupyex.Session.open/1 for the options.

open!(opts \\ [])

@spec open!([Rupyex.Session.open_option()]) :: session()

Start a session, raising on failure.

put(session, name, value, opts \\ [])

@spec put(session(), String.t(), value(), [option()]) ::
  :ok | {:error, Rupyex.Error.t()}

Bind a name in the session namespace.

:ok = Rupyex.put(session, "config", %{"retries" => 3})
{:ok, 3} = Rupyex.eval(session, "config['retries']")

reset(session, opts \\ [])

@spec reset(session(), [option()]) :: :ok | {:error, Rupyex.Error.t()}

Throw the namespace away and start from a clean one.

Imported modules stay loaded in the interpreter; only the names bound in the session go.

run(session, code, opts \\ [])

@spec run(session(), String.t(), [option()]) ::
  {:ok, Rupyex.Result.t()} | {:error, Rupyex.Error.t()}

Run Python source and return its value along with everything it printed.

{:ok, %Rupyex.Result{value: nil, stdout: "hi\n"}} =
  Rupyex.run(session, "print('hi')")

stdlib_available?()

@spec stdlib_available?() :: boolean()

Whether this build embeds the Python standard library.

vars(session, opts \\ [])

@spec vars(session(), [option()]) :: {:ok, [String.t()]} | {:error, Rupyex.Error.t()}

The names bound in the session namespace, excluding dunders.