Skip to content

Commit 0788cbc

Browse files
committed
Add path registry for sys.path management
- py:add_path/1: Add a single path to sys.path in all interpreters - py:add_paths/1: Add multiple paths to sys.path - py:all_paths/0: List all registered paths - py:clear_paths/0: Clear the path registry - py:is_path_added/1: Check if a path is registered Paths are stored in a registry (like imports) and applied to new interpreters/subinterpreters when created. Paths are inserted at the beginning of sys.path to take precedence over system paths.
1 parent 5a3c308 commit 0788cbc

5 files changed

Lines changed: 366 additions & 3 deletions

File tree

c_src/py_nif.c

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3224,6 +3224,77 @@ static void owngil_execute_apply_imports(py_context_t *ctx) {
32243224
ctx->response_ok = true;
32253225
}
32263226

3227+
/**
3228+
* @brief Apply paths to sys.path in OWN_GIL context
3229+
*
3230+
* Paths are inserted at the beginning of sys.path.
3231+
*/
3232+
static void owngil_execute_apply_paths(py_context_t *ctx) {
3233+
/* Get sys.path */
3234+
PyObject *sys_module = PyImport_ImportModule("sys");
3235+
if (sys_module == NULL) {
3236+
PyErr_Clear();
3237+
ctx->response_term = enif_make_tuple2(ctx->shared_env,
3238+
enif_make_atom(ctx->shared_env, "error"),
3239+
enif_make_atom(ctx->shared_env, "sys_import_failed"));
3240+
ctx->response_ok = false;
3241+
return;
3242+
}
3243+
3244+
PyObject *sys_path = PyObject_GetAttrString(sys_module, "path");
3245+
Py_DECREF(sys_module);
3246+
if (sys_path == NULL || !PyList_Check(sys_path)) {
3247+
Py_XDECREF(sys_path);
3248+
PyErr_Clear();
3249+
ctx->response_term = enif_make_tuple2(ctx->shared_env,
3250+
enif_make_atom(ctx->shared_env, "error"),
3251+
enif_make_atom(ctx->shared_env, "sys_path_not_list"));
3252+
ctx->response_ok = false;
3253+
return;
3254+
}
3255+
3256+
/* Count paths first */
3257+
ERL_NIF_TERM head, tail = ctx->request_term;
3258+
int path_count = 0;
3259+
while (enif_get_list_cell(ctx->shared_env, tail, &head, &tail)) {
3260+
path_count++;
3261+
}
3262+
3263+
/* Insert in reverse order so first path ends up first */
3264+
for (int i = 0; i < path_count; i++) {
3265+
/* Skip to the i-th element from the end */
3266+
ERL_NIF_TERM current = ctx->request_term;
3267+
for (int j = 0; j < path_count - 1 - i; j++) {
3268+
enif_get_list_cell(ctx->shared_env, current, &head, &current);
3269+
}
3270+
enif_get_list_cell(ctx->shared_env, current, &head, &current);
3271+
3272+
ErlNifBinary path_bin;
3273+
if (!enif_inspect_binary(ctx->shared_env, head, &path_bin)) {
3274+
continue;
3275+
}
3276+
3277+
/* Convert to Python string */
3278+
PyObject *path_str = PyUnicode_FromStringAndSize((char *)path_bin.data, path_bin.size);
3279+
if (path_str == NULL) {
3280+
PyErr_Clear();
3281+
continue;
3282+
}
3283+
3284+
/* Check if already in sys.path */
3285+
int already_present = PySequence_Contains(sys_path, path_str);
3286+
if (already_present <= 0) {
3287+
/* Insert at position 0 */
3288+
PyList_Insert(sys_path, 0, path_str);
3289+
}
3290+
Py_DECREF(path_str);
3291+
}
3292+
3293+
Py_DECREF(sys_path);
3294+
ctx->response_term = enif_make_atom(ctx->shared_env, "ok");
3295+
ctx->response_ok = true;
3296+
}
3297+
32273298
/**
32283299
* @brief Execute a request based on its type
32293300
*/
@@ -3262,6 +3333,9 @@ static void owngil_execute_request(py_context_t *ctx) {
32623333
case CTX_REQ_APPLY_IMPORTS:
32633334
owngil_execute_apply_imports(ctx);
32643335
break;
3336+
case CTX_REQ_APPLY_PATHS:
3337+
owngil_execute_apply_paths(ctx);
3338+
break;
32653339
default:
32663340
ctx->response_term = enif_make_tuple2(ctx->shared_env,
32673341
enif_make_atom(ctx->shared_env, "error"),
@@ -3871,6 +3945,50 @@ static ERL_NIF_TERM dispatch_apply_imports_to_owngil(
38713945
return result;
38723946
}
38733947

3948+
/**
3949+
* @brief Dispatch apply_paths request to OWN_GIL worker thread
3950+
*
3951+
* @param env Current NIF environment
3952+
* @param ctx OWN_GIL context
3953+
* @param paths_term List of path binaries
3954+
* @return ok | {error, Reason}
3955+
*/
3956+
static ERL_NIF_TERM dispatch_apply_paths_to_owngil(
3957+
ErlNifEnv *env, py_context_t *ctx, ERL_NIF_TERM paths_term
3958+
) {
3959+
if (!atomic_load(&ctx->thread_running)) {
3960+
return make_error(env, "thread_not_running");
3961+
}
3962+
3963+
pthread_mutex_lock(&ctx->request_mutex);
3964+
3965+
enif_clear_env(ctx->shared_env);
3966+
ctx->request_term = enif_make_copy(ctx->shared_env, paths_term);
3967+
ctx->request_type = CTX_REQ_APPLY_PATHS;
3968+
3969+
pthread_cond_signal(&ctx->request_ready);
3970+
3971+
/* Wait for response with timeout */
3972+
struct timespec deadline;
3973+
clock_gettime(CLOCK_REALTIME, &deadline);
3974+
deadline.tv_sec += OWNGIL_DISPATCH_TIMEOUT_SECS;
3975+
3976+
while (ctx->request_type != CTX_REQ_NONE) {
3977+
int rc = pthread_cond_timedwait(&ctx->response_ready, &ctx->request_mutex, &deadline);
3978+
if (rc == ETIMEDOUT) {
3979+
atomic_store(&ctx->thread_running, false);
3980+
pthread_mutex_unlock(&ctx->request_mutex);
3981+
fprintf(stderr, "OWN_GIL apply_paths dispatch timeout: worker thread unresponsive\n");
3982+
return make_error(env, "worker_timeout");
3983+
}
3984+
}
3985+
3986+
ERL_NIF_TERM result = enif_make_copy(env, ctx->response_term);
3987+
pthread_mutex_unlock(&ctx->request_mutex);
3988+
3989+
return result;
3990+
}
3991+
38743992
#endif /* HAVE_SUBINTERPRETERS */
38753993

38763994
/**
@@ -4935,6 +5053,104 @@ static ERL_NIF_TERM nif_interp_apply_imports(ErlNifEnv *env, int argc, const ERL
49355053
return ATOM_OK;
49365054
}
49375055

5056+
/**
5057+
* @brief Apply a list of paths to an interpreter's sys.path
5058+
*
5059+
* nif_interp_apply_paths(Ref, Paths) -> ok | {error, Reason}
5060+
*
5061+
* Paths: [PathBin, ...]
5062+
* Inserts paths at the beginning of sys.path so they take precedence.
5063+
*/
5064+
static ERL_NIF_TERM nif_interp_apply_paths(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
5065+
(void)argc;
5066+
py_context_t *ctx;
5067+
5068+
if (!runtime_is_running()) {
5069+
return make_error(env, "python_not_running");
5070+
}
5071+
5072+
if (!enif_get_resource(env, argv[0], PY_CONTEXT_RESOURCE_TYPE, (void **)&ctx)) {
5073+
return make_error(env, "invalid_context");
5074+
}
5075+
5076+
if (ctx->destroyed) {
5077+
return make_error(env, "context_destroyed");
5078+
}
5079+
5080+
#ifdef HAVE_SUBINTERPRETERS
5081+
/* OWN_GIL mode: dispatch to the dedicated thread */
5082+
if (ctx->uses_own_gil) {
5083+
return dispatch_apply_paths_to_owngil(env, ctx, argv[1]);
5084+
}
5085+
#endif
5086+
5087+
py_context_guard_t guard = py_context_acquire(ctx);
5088+
if (!guard.acquired) {
5089+
return make_error(env, "acquire_failed");
5090+
}
5091+
5092+
/* Get sys.path */
5093+
PyObject *sys_module = PyImport_ImportModule("sys");
5094+
if (sys_module == NULL) {
5095+
py_context_release(&guard);
5096+
return make_error(env, "sys_import_failed");
5097+
}
5098+
5099+
PyObject *sys_path = PyObject_GetAttrString(sys_module, "path");
5100+
Py_DECREF(sys_module);
5101+
if (sys_path == NULL || !PyList_Check(sys_path)) {
5102+
Py_XDECREF(sys_path);
5103+
py_context_release(&guard);
5104+
return make_error(env, "sys_path_not_list");
5105+
}
5106+
5107+
/* Process each path - insert at beginning in reverse order */
5108+
/* First, collect all paths */
5109+
ERL_NIF_TERM head, tail = argv[1];
5110+
int path_count = 0;
5111+
ERL_NIF_TERM paths_list = argv[1];
5112+
5113+
/* Count paths */
5114+
while (enif_get_list_cell(env, tail, &head, &tail)) {
5115+
path_count++;
5116+
}
5117+
5118+
/* Insert in reverse order so first path ends up first */
5119+
tail = paths_list;
5120+
for (int i = 0; i < path_count; i++) {
5121+
/* Skip to the i-th element from the end */
5122+
ERL_NIF_TERM current = paths_list;
5123+
for (int j = 0; j < path_count - 1 - i; j++) {
5124+
enif_get_list_cell(env, current, &head, &current);
5125+
}
5126+
enif_get_list_cell(env, current, &head, &current);
5127+
5128+
ErlNifBinary path_bin;
5129+
if (!enif_inspect_binary(env, head, &path_bin)) {
5130+
continue;
5131+
}
5132+
5133+
/* Convert to Python string */
5134+
PyObject *path_str = PyUnicode_FromStringAndSize((char *)path_bin.data, path_bin.size);
5135+
if (path_str == NULL) {
5136+
PyErr_Clear();
5137+
continue;
5138+
}
5139+
5140+
/* Check if already in sys.path */
5141+
int already_present = PySequence_Contains(sys_path, path_str);
5142+
if (already_present <= 0) {
5143+
/* Insert at position 0 */
5144+
PyList_Insert(sys_path, 0, path_str);
5145+
}
5146+
Py_DECREF(path_str);
5147+
}
5148+
5149+
Py_DECREF(sys_path);
5150+
py_context_release(&guard);
5151+
return ATOM_OK;
5152+
}
5153+
49385154
/**
49395155
* @brief Execute Python statements using a process-local environment
49405156
*
@@ -7065,6 +7281,7 @@ static ErlNifFunc nif_funcs[] = {
70657281
{"context_call", 6, nif_context_call_with_env, ERL_NIF_DIRTY_JOB_CPU_BOUND},
70667282
{"create_local_env", 1, nif_create_local_env, 0},
70677283
{"interp_apply_imports", 2, nif_interp_apply_imports, ERL_NIF_DIRTY_JOB_CPU_BOUND},
7284+
{"interp_apply_paths", 2, nif_interp_apply_paths, ERL_NIF_DIRTY_JOB_CPU_BOUND},
70687285
{"context_call_method", 4, nif_context_call_method, ERL_NIF_DIRTY_JOB_CPU_BOUND},
70697286
{"context_to_term", 1, nif_context_to_term, 0},
70707287
{"context_interp_id", 1, nif_context_interp_id, 0},

c_src/py_nif.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,8 @@ typedef enum {
731731
CTX_REQ_EVAL_WITH_ENV, /**< Eval with process-local environment */
732732
CTX_REQ_EXEC_WITH_ENV, /**< Exec with process-local environment */
733733
CTX_REQ_CREATE_LOCAL_ENV, /**< Create process-local env dicts */
734-
CTX_REQ_APPLY_IMPORTS /**< Apply imports to module cache */
734+
CTX_REQ_APPLY_IMPORTS, /**< Apply imports to module cache */
735+
CTX_REQ_APPLY_PATHS /**< Apply paths to sys.path */
735736
} ctx_request_type_t;
736737

737738
/**

0 commit comments

Comments
 (0)