Skip to content

Commit d194440

Browse files
Allow configuring the uv version that is downloaded (#30)
Co-authored-by: Jonatan Kłosko <[email protected]>
1 parent d09229c commit d194440

File tree

4 files changed

+48
-27
lines changed

4 files changed

+48
-27
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ config :pythonx, :uv_init,
130130
"""
131131
```
132132

133+
Additionally, you can configure a specific version of the uv package manager for Pythonx to use. This can impact the available Python versions.
134+
135+
```elixir
136+
import Config
137+
138+
config :pythonx, :uv_init,
139+
...,
140+
uv_version: "0.7.21"
141+
```
142+
133143
With that, you can use `Pythonx.eval/2` and other APIs in your
134144
application. The downloads will happen at compile time, and the
135145
interpreter will get initialized automatically on boot. All necessary

lib/pythonx.ex

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule Pythonx do
1212

1313
@type encoder :: (term(), encoder() -> Object.t())
1414

15-
@doc ~S'''
15+
@doc ~s'''
1616
Installs Python and dependencies using [uv](https://docs.astral.sh/uv)
1717
package manager and initializes the interpreter.
1818
@@ -49,13 +49,16 @@ defmodule Pythonx do
4949
5050
* `:force` - if true, runs with empty project cache. Defaults to `false`.
5151
52+
* `:uv_version` - select the version of the uv package manager to use.
53+
Defaults to `#{inspect(Pythonx.Uv.default_uv_version())}`.
54+
5255
'''
5356
@spec uv_init(String.t(), keyword()) :: :ok
5457
def uv_init(pyproject_toml, opts \\ []) when is_binary(pyproject_toml) and is_list(opts) do
55-
opts = Keyword.validate!(opts, force: false)
58+
opts = Keyword.validate!(opts, force: false, uv_version: Pythonx.Uv.default_uv_version())
5659

5760
Pythonx.Uv.fetch(pyproject_toml, false, opts)
58-
Pythonx.Uv.init(pyproject_toml, false)
61+
Pythonx.Uv.init(pyproject_toml, false, Keyword.take(opts, [:uv_version]))
5962
end
6063

6164
# Initializes the Python interpreter.

lib/pythonx/application.ex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ defmodule Pythonx.Application do
2222

2323
# If configured, we fetch Python and dependencies at compile time
2424
# and we automatically initialize the interpreter on boot.
25-
if pyproject_toml = Application.compile_env(:pythonx, :uv_init)[:pyproject_toml] do
26-
Pythonx.Uv.fetch(pyproject_toml, true)
27-
defp maybe_uv_init(), do: Pythonx.Uv.init(unquote(pyproject_toml), true)
25+
uv_init_env = Application.compile_env(:pythonx, :uv_init)
26+
pyproject_toml = uv_init_env[:pyproject_toml]
27+
uv_version = uv_init_env[:uv_version] || Pythonx.Uv.default_uv_version()
28+
opts = [uv_version: uv_version]
29+
30+
if pyproject_toml do
31+
Pythonx.Uv.fetch(pyproject_toml, true, opts)
32+
defp maybe_uv_init(), do: Pythonx.Uv.init(unquote(pyproject_toml), true, unquote(opts))
2833
else
2934
defp maybe_uv_init(), do: :noop
3035
end

lib/pythonx/uv.ex

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ defmodule Pythonx.Uv do
33

44
require Logger
55

6-
@uv_version "0.5.21"
6+
def default_uv_version(), do: "0.8.5"
77

88
@doc """
99
Fetches Python and dependencies based on the given configuration.
1010
"""
1111
@spec fetch(String.t(), boolean(), keyword()) :: :ok
1212
def fetch(pyproject_toml, priv?, opts \\ []) do
13-
opts = Keyword.validate!(opts, force: false)
13+
opts = Keyword.validate!(opts, force: false, uv_version: default_uv_version())
1414

15-
project_dir = project_dir(pyproject_toml, priv?)
16-
python_install_dir = python_install_dir(priv?)
15+
project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version])
16+
python_install_dir = python_install_dir(priv?, opts[:uv_version])
1717

1818
if opts[:force] || priv? do
1919
_ = File.rm_rf(project_dir)
@@ -30,7 +30,8 @@ defmodule Pythonx.Uv do
3030
# We always use uv-managed Python, so the paths are predictable.
3131
if run!(["sync", "--python-preference", "only-managed"],
3232
cd: project_dir,
33-
env: %{"UV_PYTHON_INSTALL_DIR" => python_install_dir}
33+
env: %{"UV_PYTHON_INSTALL_DIR" => python_install_dir},
34+
uv_version: opts[:uv_version]
3435
) != 0 do
3536
_ = File.rm_rf(project_dir)
3637
raise "fetching Python and dependencies failed, see standard output for details"
@@ -40,15 +41,15 @@ defmodule Pythonx.Uv do
4041
:ok
4142
end
4243

43-
defp python_install_dir(priv?) do
44+
defp python_install_dir(priv?, uv_version) do
4445
if priv? do
4546
Path.join(:code.priv_dir(:pythonx), "uv/python")
4647
else
47-
Path.join(cache_dir(), "python")
48+
Path.join(cache_dir(uv_version), "python")
4849
end
4950
end
5051

51-
defp project_dir(pyproject_toml, priv?) do
52+
defp project_dir(pyproject_toml, priv?, uv_version) do
5253
if priv? do
5354
Path.join(:code.priv_dir(:pythonx), "uv/project")
5455
else
@@ -57,7 +58,7 @@ defmodule Pythonx.Uv do
5758
|> :erlang.md5()
5859
|> Base.encode32(case: :lower, padding: false)
5960

60-
Path.join([cache_dir(), "projects", cache_id])
61+
Path.join([cache_dir(uv_version), "projects", cache_id])
6162
end
6263
end
6364

@@ -66,8 +67,9 @@ defmodule Pythonx.Uv do
6667
fetched by `fetch/3`.
6768
"""
6869
@spec init(String.t(), boolean()) :: :ok
69-
def init(pyproject_toml, priv?) do
70-
project_dir = project_dir(pyproject_toml, priv?)
70+
def init(pyproject_toml, priv?, opts \\ []) do
71+
opts = Keyword.validate!(opts, uv_version: default_uv_version())
72+
project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version])
7173

7274
# Uv stores Python installations in versioned directories in the
7375
# Python install dir. To find the versioned name for this project,
@@ -91,7 +93,7 @@ defmodule Pythonx.Uv do
9193
{:unix, _osname} -> Path.basename(Path.dirname(abs_executable_dir))
9294
end
9395

94-
root_dir = Path.join(python_install_dir(priv?), versioned_dir_name)
96+
root_dir = Path.join(python_install_dir(priv?, opts[:uv_version]), versioned_dir_name)
9597

9698
case :os.type() do
9799
{:win32, _osname} ->
@@ -158,10 +160,11 @@ defmodule Pythonx.Uv do
158160
defp make_windows_slashes(path), do: String.replace(path, "/", "\\")
159161

160162
defp run!(args, opts) do
161-
path = uv_path()
163+
{uv_version, opts} = Keyword.pop(opts, :uv_version, default_uv_version())
164+
path = uv_path(uv_version)
162165

163166
if not File.exists?(path) do
164-
download!()
167+
download!(uv_version)
165168
end
166169

167170
{_stream, status} =
@@ -170,31 +173,31 @@ defmodule Pythonx.Uv do
170173
status
171174
end
172175

173-
defp uv_path() do
174-
Path.join([cache_dir(), "bin", "uv"])
176+
defp uv_path(uv_version) do
177+
Path.join([cache_dir(uv_version), "bin", "uv"])
175178
end
176179

177180
@version Mix.Project.config()[:version]
178181

179-
defp cache_dir() do
182+
defp cache_dir(uv_version) do
180183
base_dir =
181184
if dir = System.get_env("PYTHONX_CACHE_DIR") do
182185
Path.expand(dir)
183186
else
184187
:filename.basedir(:user_cache, "pythonx")
185188
end
186189

187-
Path.join([base_dir, @version, "uv", @uv_version])
190+
Path.join([base_dir, @version, "uv", uv_version])
188191
end
189192

190-
defp download!() do
193+
defp download!(uv_version) do
191194
{archive_type, archive_name} = archive_name()
192195

193-
url = "https://github.com/astral-sh/uv/releases/download/#{@uv_version}/#{archive_name}"
196+
url = "https://github.com/astral-sh/uv/releases/download/#{uv_version}/#{archive_name}"
194197
Logger.debug("Downloading uv archive from #{url}")
195198
archive_binary = Pythonx.Utils.fetch_body!(url)
196199

197-
path = uv_path()
200+
path = uv_path(uv_version)
198201
{:ok, uv_binary} = extract_executable(archive_type, archive_binary)
199202
File.mkdir_p!(Path.dirname(path))
200203
File.write!(path, uv_binary)

0 commit comments

Comments
 (0)