Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here - I wasn't sure if I should reference this or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we want to nest it under opts? Perhaps just uv_version: ... directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way works! I was going more in the direction of being able to just send the opts value directly to the functions. If we just use uv_version:... and force:... directly I think I probably just need to change the application.ex to pop the pyproject_toml value from the config and then pass the rest of the config down into the Uv module?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's how i'd do it but let's wait for @jonatanklosko's opinion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely prefer it not nested. I also don't think we need :force as a global option, or at least I would wait for a use case and then evaluate other solutions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, will update so that only the uv_version can be set 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated so that it's explicitly only allowing the uv_version as an option from the compile envs - let me know if this works or if there are any more changes needed here



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
Expand Down
5 changes: 3 additions & 2 deletions lib/pythonx.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 7 additions & 3 deletions lib/pythonx/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 24 additions & 21 deletions lib/pythonx/uv.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"
Expand All @@ -40,15 +41,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
Expand All @@ -57,7 +58,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

Expand All @@ -66,8 +67,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,
Expand All @@ -91,7 +93,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} ->
Expand Down Expand Up @@ -158,10 +160,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} =
Expand All @@ -170,31 +173,31 @@ 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)
else
: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)
Expand Down