From 9de764b6445abb08b62aca33f6230ff63aa8daac Mon Sep 17 00:00:00 2001 From: Coby Benveniste Date: Thu, 7 Aug 2025 13:23:03 +0300 Subject: [PATCH 1/6] Allow passing the uv version to download through the configurations --- README.md | 24 ++++++++++++++++++++++ lib/pythonx.ex | 5 +++-- lib/pythonx/application.ex | 10 ++++++--- lib/pythonx/uv.ex | 42 ++++++++++++++++++++------------------ 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d67f387..bfc3694 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,30 @@ config :pythonx, :uv_init, """ ``` +Additionally, if you want to configure options that are passed to +the `Pythonx.uv_init` functions, you can configure the `opts` parameter as well. +```elixir +import Config + +config :pythonx, :uv_init, + pyproject_toml: """ + [project] + name = "project" + version = "0.0.0" + requires-python = "==3.13.*" + dependencies = [ + "numpy==2.2.2" + ] + """, + opts: [uv_version: "0.7.21"] +``` + + +The `opts` parameter currently accepts 2 options: +* `:force` - if true, runs with empty project cache. Defaults to `false`. +* `:uv_version` - select the version of the uv package manager to use. Defaults to `Pythonx.Uv.default_uv_version()`. + + With that, you can use `Pythonx.eval/2` and other APIs in your application. The downloads will happen at compile time, and the interpreter will get initialized automatically on boot. All necessary diff --git a/lib/pythonx.ex b/lib/pythonx.ex index d50b802..a81575f 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -48,14 +48,15 @@ defmodule Pythonx do ## Options * `:force` - if true, runs with empty project cache. Defaults to `false`. + * `:uv_version` - select the version of the uv package manager to use. Defaults to `Pythonx.Uv.default_uv_version()`. ''' @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) + opts = Keyword.validate!(opts, force: false, uv_version: Pythonx.Uv.default_uv_version()) Pythonx.Uv.fetch(pyproject_toml, false, opts) - Pythonx.Uv.init(pyproject_toml, false) + Pythonx.Uv.init(pyproject_toml, false, opts) end # Initializes the Python interpreter. diff --git a/lib/pythonx/application.ex b/lib/pythonx/application.ex index 1be6aa8..980c120 100644 --- a/lib/pythonx/application.ex +++ b/lib/pythonx/application.ex @@ -22,9 +22,13 @@ defmodule Pythonx.Application do # If configured, we fetch Python and dependencies at compile time # and we automatically initialize the interpreter on boot. - if pyproject_toml = Application.compile_env(:pythonx, :uv_init)[:pyproject_toml] do - Pythonx.Uv.fetch(pyproject_toml, true) - defp maybe_uv_init(), do: Pythonx.Uv.init(unquote(pyproject_toml), true) + uv_init_env = Application.compile_env(:pythonx, :uv_init) + pyproject_toml = uv_init_env[:pyproject_toml] + opts = uv_init_env[:opts] || [] + + if pyproject_toml do + Pythonx.Uv.fetch(pyproject_toml, true, opts) + defp maybe_uv_init(), do: Pythonx.Uv.init(unquote(pyproject_toml), true, unquote(opts)) else defp maybe_uv_init(), do: :noop end diff --git a/lib/pythonx/uv.ex b/lib/pythonx/uv.ex index e274c3f..185baee 100644 --- a/lib/pythonx/uv.ex +++ b/lib/pythonx/uv.ex @@ -3,17 +3,17 @@ defmodule Pythonx.Uv do require Logger - @uv_version "0.5.21" + def default_uv_version(), do: "0.8.5" @doc """ Fetches Python and dependencies based on the given configuration. """ @spec fetch(String.t(), boolean(), keyword()) :: :ok def fetch(pyproject_toml, priv?, opts \\ []) do - opts = Keyword.validate!(opts, force: false) + opts = Keyword.validate!(opts, force: false, uv_version: default_uv_version()) - project_dir = project_dir(pyproject_toml, priv?) - python_install_dir = python_install_dir(priv?) + project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version]) + python_install_dir = python_install_dir(priv?, opts[:uv_version]) if opts[:force] || priv? do _ = File.rm_rf(project_dir) @@ -40,15 +40,15 @@ defmodule Pythonx.Uv do :ok end - defp python_install_dir(priv?) do + defp python_install_dir(priv?, uv_version) do if priv? do Path.join(:code.priv_dir(:pythonx), "uv/python") else - Path.join(cache_dir(), "python") + Path.join(cache_dir(uv_version), "python") end end - defp project_dir(pyproject_toml, priv?) do + defp project_dir(pyproject_toml, priv?, uv_version) do if priv? do Path.join(:code.priv_dir(:pythonx), "uv/project") else @@ -57,7 +57,7 @@ defmodule Pythonx.Uv do |> :erlang.md5() |> Base.encode32(case: :lower, padding: false) - Path.join([cache_dir(), "projects", cache_id]) + Path.join([cache_dir(uv_version), "projects", cache_id]) end end @@ -66,8 +66,9 @@ defmodule Pythonx.Uv do fetched by `fetch/3`. """ @spec init(String.t(), boolean()) :: :ok - def init(pyproject_toml, priv?) do - project_dir = project_dir(pyproject_toml, priv?) + def init(pyproject_toml, priv?, opts \\ []) do + opts = Keyword.validate!(opts, uv_version: default_uv_version()) + project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version]) # Uv stores Python installations in versioned directories in the # Python install dir. To find the versioned name for this project, @@ -91,7 +92,7 @@ defmodule Pythonx.Uv do {:unix, _osname} -> Path.basename(Path.dirname(abs_executable_dir)) end - root_dir = Path.join(python_install_dir(priv?), versioned_dir_name) + root_dir = Path.join(python_install_dir(priv?, opts[:uv_version]), versioned_dir_name) case :os.type() do {:win32, _osname} -> @@ -158,10 +159,11 @@ defmodule Pythonx.Uv do defp make_windows_slashes(path), do: String.replace(path, "/", "\\") defp run!(args, opts) do - path = uv_path() + {uv_version, opts} = Keyword.pop(opts, :uv_version, default_uv_version()) + path = uv_path(uv_version) if not File.exists?(path) do - download!() + download!(uv_version) end {_stream, status} = @@ -170,13 +172,13 @@ defmodule Pythonx.Uv do status end - defp uv_path() do - Path.join([cache_dir(), "bin", "uv"]) + defp uv_path(uv_version) do + Path.join([cache_dir(uv_version), "bin", "uv"]) end @version Mix.Project.config()[:version] - defp cache_dir() do + defp cache_dir(uv_version) do base_dir = if dir = System.get_env("PYTHONX_CACHE_DIR") do Path.expand(dir) @@ -184,17 +186,17 @@ defmodule Pythonx.Uv do :filename.basedir(:user_cache, "pythonx") end - Path.join([base_dir, @version, "uv", @uv_version]) + Path.join([base_dir, @version, "uv", uv_version]) end - defp download!() do + defp download!(uv_version) do {archive_type, archive_name} = archive_name() - url = "https://github.com/astral-sh/uv/releases/download/#{@uv_version}/#{archive_name}" + url = "https://github.com/astral-sh/uv/releases/download/#{uv_version}/#{archive_name}" Logger.debug("Downloading uv archive from #{url}") archive_binary = Pythonx.Utils.fetch_body!(url) - path = uv_path() + path = uv_path(uv_version) {:ok, uv_binary} = extract_executable(archive_type, archive_binary) File.mkdir_p!(Path.dirname(path)) File.write!(path, uv_binary) From 79428f0619aaa1ed57583715f9eb359002b022f8 Mon Sep 17 00:00:00 2001 From: Coby Benveniste Date: Thu, 7 Aug 2025 13:41:26 +0300 Subject: [PATCH 2/6] Pass the uv_version in the run! function on fetch --- 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 185baee..6cc147e 100644 --- a/lib/pythonx/uv.ex +++ b/lib/pythonx/uv.ex @@ -30,7 +30,8 @@ defmodule Pythonx.Uv do # We always use uv-managed Python, so the paths are predictable. if run!(["sync", "--python-preference", "only-managed"], cd: project_dir, - env: %{"UV_PYTHON_INSTALL_DIR" => python_install_dir} + env: %{"UV_PYTHON_INSTALL_DIR" => python_install_dir}, + uv_version: opts[:uv_version] ) != 0 do _ = File.rm_rf(project_dir) raise "fetching Python and dependencies failed, see standard output for details" From a56aba92e6dd75776e66127bbc5a5930a73cf63e Mon Sep 17 00:00:00 2001 From: Coby Benveniste Date: Thu, 7 Aug 2025 16:22:53 +0300 Subject: [PATCH 3/6] Explicitly accept only uv version from compile opts --- README.md | 11 ++--------- lib/pythonx.ex | 5 +++-- lib/pythonx/application.ex | 3 ++- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bfc3694..ae16f23 100644 --- a/README.md +++ b/README.md @@ -130,8 +130,7 @@ config :pythonx, :uv_init, """ ``` -Additionally, if you want to configure options that are passed to -the `Pythonx.uv_init` functions, you can configure the `opts` parameter as well. +Additionally, you may configure an explicit version of the uv package manager to use with Pythonx. ```elixir import Config @@ -145,15 +144,9 @@ config :pythonx, :uv_init, "numpy==2.2.2" ] """, - opts: [uv_version: "0.7.21"] + uv_version: "0.7.21" ``` - -The `opts` parameter currently accepts 2 options: -* `:force` - if true, runs with empty project cache. Defaults to `false`. -* `:uv_version` - select the version of the uv package manager to use. Defaults to `Pythonx.Uv.default_uv_version()`. - - With that, you can use `Pythonx.eval/2` and other APIs in your application. The downloads will happen at compile time, and the interpreter will get initialized automatically on boot. All necessary diff --git a/lib/pythonx.ex b/lib/pythonx.ex index a81575f..0c80715 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -12,7 +12,7 @@ defmodule Pythonx do @type encoder :: (term(), encoder() -> Object.t()) - @doc ~S''' + @doc ~s''' Installs Python and dependencies using [uv](https://docs.astral.sh/uv) package manager and initializes the interpreter. @@ -48,7 +48,8 @@ defmodule Pythonx do ## Options * `:force` - if true, runs with empty project cache. Defaults to `false`. - * `:uv_version` - select the version of the uv package manager to use. Defaults to `Pythonx.Uv.default_uv_version()`. + * `:uv_version` - select the version of the uv package manager to use. + Defaults to `#{inspect(Pythonx.Uv.default_uv_version())}`. ''' @spec uv_init(String.t(), keyword()) :: :ok diff --git a/lib/pythonx/application.ex b/lib/pythonx/application.ex index 980c120..e6bea9f 100644 --- a/lib/pythonx/application.ex +++ b/lib/pythonx/application.ex @@ -24,7 +24,8 @@ defmodule Pythonx.Application do # and we automatically initialize the interpreter on boot. uv_init_env = Application.compile_env(:pythonx, :uv_init) pyproject_toml = uv_init_env[:pyproject_toml] - opts = uv_init_env[:opts] || [] + uv_version = uv_init_env[:uv_version] || Pythonx.Uv.default_uv_version() + opts = [uv_version: uv_version] if pyproject_toml do Pythonx.Uv.fetch(pyproject_toml, true, opts) From 755a5010e8cc6000e0a9271037f78a4c141deabc Mon Sep 17 00:00:00 2001 From: Coby Benveniste Date: Thu, 7 Aug 2025 16:24:31 +0300 Subject: [PATCH 4/6] Make sure to only take the uv_version option for Uv.init --- lib/pythonx.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pythonx.ex b/lib/pythonx.ex index 0c80715..bf6e9cc 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -57,7 +57,7 @@ defmodule Pythonx do opts = Keyword.validate!(opts, force: false, uv_version: Pythonx.Uv.default_uv_version()) Pythonx.Uv.fetch(pyproject_toml, false, opts) - Pythonx.Uv.init(pyproject_toml, false, opts) + Pythonx.Uv.init(pyproject_toml, false, Keyword.take(opts, [:uv_version])) end # Initializes the Python interpreter. From aa5c86c87f2ed1a588eef0829196341dd40ce117 Mon Sep 17 00:00:00 2001 From: Coby Benveniste <41164052+probably-not@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:53:51 +0300 Subject: [PATCH 5/6] Simplify README example for configuring `uv_version` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jonatan Kłosko --- README.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ae16f23..78b2456 100644 --- a/README.md +++ b/README.md @@ -130,20 +130,13 @@ config :pythonx, :uv_init, """ ``` -Additionally, you may configure an explicit version of the uv package manager to use with Pythonx. +Additionally, you can configure a specific version of the uv package manager for Pythonx to use. This can impact the available Python versions. + ```elixir import Config config :pythonx, :uv_init, - pyproject_toml: """ - [project] - name = "project" - version = "0.0.0" - requires-python = "==3.13.*" - dependencies = [ - "numpy==2.2.2" - ] - """, + ..., uv_version: "0.7.21" ``` From b92ff05d3646b77ce79ca213b02684d803ac6846 Mon Sep 17 00:00:00 2001 From: Coby Benveniste <41164052+probably-not@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:54:20 +0300 Subject: [PATCH 6/6] Fix spacing in `pythonx.ex` docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jonatan Kłosko --- lib/pythonx.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pythonx.ex b/lib/pythonx.ex index bf6e9cc..1e5453a 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -48,6 +48,7 @@ defmodule Pythonx do ## Options * `:force` - if true, runs with empty project cache. Defaults to `false`. + * `:uv_version` - select the version of the uv package manager to use. Defaults to `#{inspect(Pythonx.Uv.default_uv_version())}`.