From 8c0b6922663fb5eaa804ea4f64c96c73c509f4a2 Mon Sep 17 00:00:00 2001 From: GenericJam Date: Fri, 16 Jan 2026 09:28:52 -0700 Subject: [PATCH 1/4] Add native_tls option to Pythonx.uv_init/2 (#40) --- lib/pythonx.ex | 23 +++++++++++++++++++- lib/pythonx/uv.ex | 7 ++++-- test/pythonx_test.exs | 50 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/lib/pythonx.ex b/lib/pythonx.ex index 4600022..be91a14 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -45,6 +45,23 @@ defmodule Pythonx do ] """) + In some environments, you may need to pass additional flags to the `uv sync` + command. For example, in corporate environments with strict security policies, + you might need to use native TLS: + + Pythonx.uv_init( + """ + [project] + name = "project" + version = "0.0.0" + requires-python = "==3.13.*" + dependencies = [ + "numpy==2.2.2" + ] + """, + native_tls: true + ) + For more configuration options, refer to the [uv documentation](https://docs.astral.sh/uv/concepts/projects/dependencies/). ## Options @@ -54,10 +71,14 @@ defmodule Pythonx do * `:uv_version` - select the version of the uv package manager to use. Defaults to `#{inspect(Pythonx.Uv.default_uv_version())}`. + * `:native_tls` - if true, uses the system's native TLS implementation instead + of vendored rustls. This is useful in corporate environments where the system + certificate store must be used. Defaults to `false`. + ''' @spec uv_init(String.t(), keyword()) :: :ok def uv_init(pyproject_toml, opts \\ []) when is_binary(pyproject_toml) and is_list(opts) do - opts = Keyword.validate!(opts, force: false, uv_version: Pythonx.Uv.default_uv_version()) + opts = Keyword.validate!(opts, force: false, uv_version: Pythonx.Uv.default_uv_version(), native_tls: false) Pythonx.Uv.fetch(pyproject_toml, false, opts) install_paths = Pythonx.Uv.init(pyproject_toml, false, Keyword.take(opts, [:uv_version])) diff --git a/lib/pythonx/uv.ex b/lib/pythonx/uv.ex index 78312b9..0ef9095 100644 --- a/lib/pythonx/uv.ex +++ b/lib/pythonx/uv.ex @@ -10,7 +10,7 @@ defmodule Pythonx.Uv do """ @spec fetch(String.t(), boolean(), keyword()) :: :ok def fetch(pyproject_toml, priv?, opts \\ []) do - opts = Keyword.validate!(opts, force: false, uv_version: default_uv_version()) + opts = Keyword.validate!(opts, force: false, uv_version: default_uv_version(), native_tls: false) project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version]) python_install_dir = python_install_dir(priv?, opts[:uv_version]) @@ -28,7 +28,10 @@ defmodule Pythonx.Uv do File.write!(Path.join(project_dir, "pyproject.toml"), pyproject_toml) # We always use uv-managed Python, so the paths are predictable. - if run!(["sync", "--managed-python", "--no-config"], + base_args = ["sync", "--managed-python", "--no-config"] + uv_args = if opts[:native_tls], do: base_args ++ ["--native-tls"], else: base_args + + if run!(uv_args, cd: project_dir, env: %{"UV_PYTHON_INSTALL_DIR" => python_install_dir}, uv_version: opts[:uv_version] diff --git a/test/pythonx_test.exs b/test/pythonx_test.exs index 84fdee0..80a02da 100644 --- a/test/pythonx_test.exs +++ b/test/pythonx_test.exs @@ -477,6 +477,56 @@ defmodule PythonxTest do end end + describe "uv_init/2 native_tls option" do + test "accepts native_tls option" do + # Test that native_tls is recognized as a valid option + opts = Keyword.validate!([native_tls: true, force: false], + force: false, + uv_version: Pythonx.Uv.default_uv_version(), + native_tls: false) + + assert opts[:native_tls] == true + assert opts[:force] == false + end + + test "defaults native_tls to false" do + opts = Keyword.validate!([], + force: false, + uv_version: Pythonx.Uv.default_uv_version(), + native_tls: false) + + assert opts[:native_tls] == false + end + + test "native_tls true adds --native-tls flag to uv command" do + # Simulate how Pythonx.Uv.fetch constructs the uv command arguments + base_args = ["sync", "--managed-python", "--no-config"] + native_tls = true + + uv_args = if native_tls, do: base_args ++ ["--native-tls"], else: base_args + + assert uv_args == ["sync", "--managed-python", "--no-config", "--native-tls"] + end + + test "native_tls false does not add --native-tls flag" do + base_args = ["sync", "--managed-python", "--no-config"] + native_tls = false + + uv_args = if native_tls, do: base_args ++ ["--native-tls"], else: base_args + + assert uv_args == ["sync", "--managed-python", "--no-config"] + end + + test "raises error for unknown options" do + assert_raise ArgumentError, ~r/unknown keys/, fn -> + Keyword.validate!([unknown_option: true], + force: false, + uv_version: Pythonx.Uv.default_uv_version(), + native_tls: false) + end + end + end + defp repr(object) do assert %Pythonx.Object{} = object From b48697c6ad631aeec4751587cb3e5e406f4dc7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Fri, 16 Jan 2026 17:40:23 +0100 Subject: [PATCH 2/4] Apply suggestions from code review --- lib/pythonx.ex | 17 --------------- test/pythonx_test.exs | 50 ------------------------------------------- 2 files changed, 67 deletions(-) diff --git a/lib/pythonx.ex b/lib/pythonx.ex index be91a14..dcf455d 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -45,23 +45,6 @@ defmodule Pythonx do ] """) - In some environments, you may need to pass additional flags to the `uv sync` - command. For example, in corporate environments with strict security policies, - you might need to use native TLS: - - Pythonx.uv_init( - """ - [project] - name = "project" - version = "0.0.0" - requires-python = "==3.13.*" - dependencies = [ - "numpy==2.2.2" - ] - """, - native_tls: true - ) - For more configuration options, refer to the [uv documentation](https://docs.astral.sh/uv/concepts/projects/dependencies/). ## Options diff --git a/test/pythonx_test.exs b/test/pythonx_test.exs index 80a02da..84fdee0 100644 --- a/test/pythonx_test.exs +++ b/test/pythonx_test.exs @@ -477,56 +477,6 @@ defmodule PythonxTest do end end - describe "uv_init/2 native_tls option" do - test "accepts native_tls option" do - # Test that native_tls is recognized as a valid option - opts = Keyword.validate!([native_tls: true, force: false], - force: false, - uv_version: Pythonx.Uv.default_uv_version(), - native_tls: false) - - assert opts[:native_tls] == true - assert opts[:force] == false - end - - test "defaults native_tls to false" do - opts = Keyword.validate!([], - force: false, - uv_version: Pythonx.Uv.default_uv_version(), - native_tls: false) - - assert opts[:native_tls] == false - end - - test "native_tls true adds --native-tls flag to uv command" do - # Simulate how Pythonx.Uv.fetch constructs the uv command arguments - base_args = ["sync", "--managed-python", "--no-config"] - native_tls = true - - uv_args = if native_tls, do: base_args ++ ["--native-tls"], else: base_args - - assert uv_args == ["sync", "--managed-python", "--no-config", "--native-tls"] - end - - test "native_tls false does not add --native-tls flag" do - base_args = ["sync", "--managed-python", "--no-config"] - native_tls = false - - uv_args = if native_tls, do: base_args ++ ["--native-tls"], else: base_args - - assert uv_args == ["sync", "--managed-python", "--no-config"] - end - - test "raises error for unknown options" do - assert_raise ArgumentError, ~r/unknown keys/, fn -> - Keyword.validate!([unknown_option: true], - force: false, - uv_version: Pythonx.Uv.default_uv_version(), - native_tls: false) - end - end - end - defp repr(object) do assert %Pythonx.Object{} = object From 20897d3c01869e362decfc46b1cadffa4ca96090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Fri, 16 Jan 2026 17:43:02 +0100 Subject: [PATCH 3/4] Update lib/pythonx.ex --- lib/pythonx.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pythonx.ex b/lib/pythonx.ex index dcf455d..a2dc288 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -61,7 +61,12 @@ defmodule Pythonx do ''' @spec uv_init(String.t(), keyword()) :: :ok def uv_init(pyproject_toml, opts \\ []) when is_binary(pyproject_toml) and is_list(opts) do - opts = Keyword.validate!(opts, force: false, uv_version: Pythonx.Uv.default_uv_version(), native_tls: false) + opts = + Keyword.validate!(opts, + force: false, + uv_version: Pythonx.Uv.default_uv_version(), + native_tls: false + ) Pythonx.Uv.fetch(pyproject_toml, false, opts) install_paths = Pythonx.Uv.init(pyproject_toml, false, Keyword.take(opts, [:uv_version])) From b677310d85ad0716d8bedeeb9b42ba39f5eee95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Fri, 16 Jan 2026 18:07:15 +0100 Subject: [PATCH 4/4] Update lib/pythonx/uv.ex --- lib/pythonx/uv.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pythonx/uv.ex b/lib/pythonx/uv.ex index 0ef9095..8fb039e 100644 --- a/lib/pythonx/uv.ex +++ b/lib/pythonx/uv.ex @@ -10,7 +10,8 @@ defmodule Pythonx.Uv do """ @spec fetch(String.t(), boolean(), keyword()) :: :ok def fetch(pyproject_toml, priv?, opts \\ []) do - opts = Keyword.validate!(opts, force: false, uv_version: default_uv_version(), native_tls: false) + opts = + Keyword.validate!(opts, force: false, uv_version: default_uv_version(), native_tls: false) project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version]) python_install_dir = python_install_dir(priv?, opts[:uv_version])