diff --git a/lib/pythonx.ex b/lib/pythonx.ex index 2db4ced..8fd9c12 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -65,6 +65,11 @@ defmodule Pythonx do of vendored rustls. This is useful in corporate environments where the system certificate store must be used. Defaults to `false`. + * `:python` - specifies the Python version to install. It is preferred to + specify the version in `pyproject_toml` via the `requires-python` field. + However, the `:python` option can be used to install a specific variant + of Python, such as a free-threaded Python build, for example `"3.14t"`. + ''' @spec uv_init(String.t(), keyword()) :: :ok def uv_init(pyproject_toml, opts \\ []) when is_binary(pyproject_toml) and is_list(opts) do @@ -72,11 +77,14 @@ defmodule Pythonx do Keyword.validate!(opts, force: false, uv_version: Pythonx.Uv.default_uv_version(), - native_tls: false + native_tls: false, + python: nil ) Pythonx.Uv.fetch(pyproject_toml, false, opts) - install_paths = Pythonx.Uv.init(pyproject_toml, false, Keyword.take(opts, [:uv_version])) + + install_paths = + Pythonx.Uv.init(pyproject_toml, false, Keyword.take(opts, [:uv_version, :python])) init_state = %{ type: :uv_init, diff --git a/lib/pythonx/uv.ex b/lib/pythonx/uv.ex index 8fb039e..d6cad97 100644 --- a/lib/pythonx/uv.ex +++ b/lib/pythonx/uv.ex @@ -11,9 +11,14 @@ 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) - - project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version]) + Keyword.validate!(opts, + force: false, + uv_version: default_uv_version(), + native_tls: false, + python: nil + ) + + project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version], opts[:python]) python_install_dir = python_install_dir(priv?, opts[:uv_version]) if opts[:force] || priv? do @@ -30,6 +35,7 @@ defmodule Pythonx.Uv do # We always use uv-managed Python, so the paths are predictable. base_args = ["sync", "--managed-python", "--no-config"] + base_args = if opts[:python], do: base_args ++ ["--python", opts[:python]], else: base_args uv_args = if opts[:native_tls], do: base_args ++ ["--native-tls"], else: base_args if run!(uv_args, @@ -53,12 +59,13 @@ defmodule Pythonx.Uv do end end - defp project_dir(pyproject_toml, priv?, uv_version) do + defp project_dir(pyproject_toml, priv?, uv_version, python) do if priv? do Path.join(:code.priv_dir(:pythonx), "uv/project") else cache_id = - pyproject_toml + [pyproject_toml, python || ""] + |> IO.iodata_to_binary() |> :erlang.md5() |> Base.encode32(case: :lower, padding: false) @@ -72,8 +79,8 @@ defmodule Pythonx.Uv do """ @spec init(String.t(), boolean()) :: list(String.t()) 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]) + opts = Keyword.validate!(opts, uv_version: default_uv_version(), python: nil) + project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version], opts[:python]) # Uv stores Python installations in versioned directories in the # Python install dir. To find the versioned name for this project,