Skip to content

Commit 3033b84

Browse files
committed
Mirror Elixir env vars into os.environ on initialization
1 parent 12ece41 commit 3033b84

File tree

8 files changed

+84
-4
lines changed

8 files changed

+84
-4
lines changed

c_src/python.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ DEF_SYMBOL(PyObject_GetIter)
5656
DEF_SYMBOL(PyObject_IsInstance)
5757
DEF_SYMBOL(PyObject_Repr)
5858
DEF_SYMBOL(PyObject_SetAttrString)
59+
DEF_SYMBOL(PyObject_SetItem)
5960
DEF_SYMBOL(PyObject_Str)
6061
DEF_SYMBOL(PySet_Add)
6162
DEF_SYMBOL(PySet_New)
@@ -130,6 +131,7 @@ void load_python_library(std::string path) {
130131
LOAD_SYMBOL(python_library, PyObject_IsInstance)
131132
LOAD_SYMBOL(python_library, PyObject_Repr)
132133
LOAD_SYMBOL(python_library, PyObject_SetAttrString)
134+
LOAD_SYMBOL(python_library, PyObject_SetItem)
133135
LOAD_SYMBOL(python_library, PyObject_Str)
134136
LOAD_SYMBOL(python_library, PySet_Add)
135137
LOAD_SYMBOL(python_library, PySet_New)

c_src/python.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ extern PyObjectPtr (*PyObject_GetIter)(PyObjectPtr);
110110
extern int (*PyObject_IsInstance)(PyObjectPtr, PyObjectPtr);
111111
extern PyObjectPtr (*PyObject_Repr)(PyObjectPtr);
112112
extern int (*PyObject_SetAttrString)(PyObjectPtr, const char *, PyObjectPtr);
113+
extern int (*PyObject_SetItem)(PyObjectPtr, PyObjectPtr, PyObjectPtr);
113114
extern PyObjectPtr (*PyObject_Str)(PyObjectPtr);
114115
extern int (*PySet_Add)(PyObjectPtr, PyObjectPtr);
115116
extern PyObjectPtr (*PySet_New)(PyObjectPtr);

c_src/pythonx.cpp

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <string>
1010
#include <thread>
1111
#include <tuple>
12+
#include <unordered_map>
1213

1314
#include "python.hpp"
1415

@@ -287,7 +288,8 @@ ERL_NIF_TERM py_str_to_binary_term(ErlNifEnv *env, PyObjectPtr py_object) {
287288
fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
288289
ErlNifBinary python_home_path,
289290
ErlNifBinary python_executable_path,
290-
std::vector<ErlNifBinary> sys_paths) {
291+
std::vector<ErlNifBinary> sys_paths,
292+
std::vector<std::tuple<ErlNifBinary, ErlNifBinary>> envs) {
291293
auto init_guard = std::lock_guard<std::mutex>(init_mutex);
292294

293295
if (is_initialized) {
@@ -377,6 +379,38 @@ fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
377379
raise_if_failed(env, PyList_Append(py_sys_path, py_path));
378380
}
379381

382+
// We set env vars to match Elixir at the time of initialization.
383+
// Note that the interpreter initializes its env vars from the OS
384+
// process, however we also want to account for env vars set
385+
// dynamically, for example via System.put_env/2.
386+
387+
auto py_os = PyImport_AddModule("os");
388+
raise_if_failed(env, py_os);
389+
390+
auto py_os_environ = PyObject_GetAttrString(py_os, "environ");
391+
raise_if_failed(env, py_os_environ);
392+
auto py_os_environ_guard = PyDecRefGuard(py_os_environ);
393+
394+
auto py_os_environ_clear = PyObject_GetAttrString(py_os_environ, "clear");
395+
raise_if_failed(env, py_os_environ_clear);
396+
auto py_os_environ_clear_guard = PyDecRefGuard(py_os_environ_clear);
397+
auto result = PyObject_CallNoArgs(py_os_environ_clear);
398+
raise_if_failed(env, result);
399+
400+
for (const auto &[key, value] : envs) {
401+
auto py_key = PyUnicode_FromStringAndSize(
402+
reinterpret_cast<const char *>(key.data), key.size);
403+
raise_if_failed(env, py_key);
404+
auto py_key_guard = PyDecRefGuard(py_key);
405+
auto py_value = PyUnicode_FromStringAndSize(
406+
reinterpret_cast<const char *>(value.data), value.size);
407+
raise_if_failed(env, py_value);
408+
auto py_value_guard = PyDecRefGuard(py_value);
409+
410+
auto result = PyObject_SetItem(py_os_environ, py_key, py_value);
411+
raise_if_failed(env, result);
412+
}
413+
380414
// Define global stdout and stdin overrides
381415

382416
auto py_builtins = PyEval_GetBuiltins();

lib/pythonx.ex

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ defmodule Pythonx do
4747
4848
For more configuration options, refer to the [uv documentation](https://docs.astral.sh/uv/concepts/projects/dependencies/).
4949
50+
> #### Environment variables {: .info}
51+
>
52+
> As part of the initialization, Python's `os.environ` is modified
53+
> to match `System.get_env/1`. Therefore, `os.environ` accounts for
54+
> prior changes to the environment, such as `System.put_env/2`.
55+
>
56+
> Subsequent changes to the environment, both via Elixir and Python,
57+
> are not synchronized.
58+
>
59+
> Also note that contrarily to Elixir, changes to `os.environ` are
60+
> automatically mirrored to the OS process environment.
61+
5062
## Options
5163
5264
* `:force` - if true, runs with empty project cache. Defaults to `false`.
@@ -196,7 +208,18 @@ defmodule Pythonx do
196208
raise ArgumentError, "the given python executable does not exist: #{python_executable_path}"
197209
end
198210

199-
Pythonx.NIF.init(python_dl_path, python_home_path, python_executable_path, opts[:sys_paths])
211+
envs =
212+
for {k, v} <- :os.env() do
213+
{IO.chardata_to_string(k), IO.chardata_to_string(v)}
214+
end
215+
216+
Pythonx.NIF.init(
217+
python_dl_path,
218+
python_home_path,
219+
python_executable_path,
220+
opts[:sys_paths],
221+
envs
222+
)
200223
end
201224

202225
@doc ~S'''

lib/pythonx/nif.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ defmodule Pythonx.NIF do
1212
end
1313
end
1414

15-
def init(_python_dl_path, _python_home_path, _python_executable_path, _sys_paths), do: err!()
15+
def init(_python_dl_path, _python_home_path, _python_executable_path, _sys_paths, _envs),
16+
do: err!()
17+
1618
def janitor_decref(_ptr), do: err!()
1719
def none_new(), do: err!()
1820
def false_new(), do: err!()

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"},
44
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
55
"ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"},
6-
"fine": {:hex, :fine, "0.1.2", "85cf7dd190c7c6c54c2840754ae977c9acc0417316255b674fad9f2678e4ecc7", [:mix], [], "hexpm", "9113531982c2b60dbea6c7233917ddf16806947cd7104b5d03011bf436ca3072"},
6+
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
77
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
88
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
99
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},

test/pythonx_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,22 @@ defmodule PythonxTest do
477477
end
478478
end
479479

480+
test "inherits env vars from elixir" do
481+
# We set PYTHONX_TEST_ENV_VAR in test_helper.exs, before initializing
482+
# Pythonx. That env var should be available to Python.
483+
484+
assert {result, %{}} =
485+
Pythonx.eval(
486+
"""
487+
import os
488+
os.environ["PYTHONX_TEST_ENV_VAR"]
489+
""",
490+
%{}
491+
)
492+
493+
assert repr(result) == "'value'"
494+
end
495+
480496
defp repr(object) do
481497
assert %Pythonx.Object{} = object
482498

test/test_helper.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
python_minor = System.get_env("PYTHONX_TEST_PYTHON_MINOR", "13") |> String.to_integer()
22

3+
System.put_env("PYTHONX_TEST_ENV_VAR", "value")
4+
35
Pythonx.uv_init("""
46
[project]
57
name = "project"

0 commit comments

Comments
 (0)