Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions docs/pypi/download-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

This documentation page covers how to download PyPI dependencies in the legacy `WORKSPACE` setup.

To add pip dependencies to your `WORKSPACE`, load the `pip_parse` function and
call it to create the central external repo and individual wheel external repos.
To add pip dependencies to your `WORKSPACE`, load the {obj}`pip_parse` or
{obj}`uv_lock` function and call it to create the central external repo and
individual wheel external repos.

```starlark
load("@rules_python//python:pip.bzl", "pip_parse")

# Create a central repo that knows about the dependencies needed from
# requirements_lock.txt.
# requirements_lock.txt (or use uv_lock(name = "my_deps", uv_lock = "//path/to:uv.lock")
# for a uv.lock file).
pip_parse(
name = "my_deps",
requirements_lock = "//path/to:requirements_lock.txt",
Expand Down
8 changes: 4 additions & 4 deletions docs/pypi/lock.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
# Lock

:::{note}
`rules_python` supports `requirements.txt` and `uv.lock` (bzlmod only)
formats.
`rules_python` supports `requirements.txt` and `uv.lock` formats.

#{gh-issue}`2787` tracks `pylock.toml` support.
:::
Expand Down Expand Up @@ -231,7 +230,7 @@ lock(
`diff_test` from `bazel_skylib`.
:::

## uv.lock (bzlmod only)
## uv.lock

The {obj}`lock` rule also supports generating cross-platform `uv.lock` files
using `uv lock` when {arg}`lock.out` ends with `.lock` (such as `uv.lock`).
Expand All @@ -251,7 +250,8 @@ lock(
```

Run `bazel run //:uv_lock.update` to generate or update `uv.lock`, then pass it
to {obj}`pip.parse` in `MODULE.bazel` via {attr}`pip.parse.uv_lock`:
to {obj}`pip.parse` in `MODULE.bazel` via {attr}`pip.parse.uv_lock` (or to
{obj}`uv_lock` in `WORKSPACE`):

```starlark
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
Expand Down
2 changes: 2 additions & 0 deletions news/uv-lock-workspace.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(pypi) Added the {obj}`uv_lock` `WORKSPACE` macro in `//python:pip.bzl` to
install PyPI dependencies from a resolved `uv.lock` file.
1 change: 1 addition & 0 deletions python/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ bzl_library(
"//python/private/pypi:package_annotation",
"//python/private/pypi:pip_compile",
"//python/private/pypi:pip_repository",
"//python/private/pypi:uv_lock",
"//python/private/pypi:whl_library_alias",
"//python/private/whl_filegroup",
],
Expand Down
8 changes: 5 additions & 3 deletions python/pip.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"""Rules for pip integration.

This contains a set of rules that are used to support inclusion of third-party
dependencies via fully locked `requirements.txt` files. Some of the exported
symbols should not be used and they are either undocumented here or marked as
for internal use only.
dependencies via fully locked `requirements.txt` or `uv.lock` files. Some of the
exported symbols should not be used and they are either undocumented here or
marked as for internal use only.

If you are using a bazel version 7 or above with `bzlmod`, you should only care
about the {bzl:obj}`compile_pip_requirements` macro exposed in this file. The
Expand All @@ -28,12 +28,14 @@ load("//python/private/pypi:multi_pip_parse.bzl", _multi_pip_parse = "multi_pip_
load("//python/private/pypi:package_annotation.bzl", _package_annotation = "package_annotation")
load("//python/private/pypi:pip_compile.bzl", "pip_compile")
load("//python/private/pypi:pip_repository.bzl", "pip_repository")
load("//python/private/pypi:uv_lock.bzl", _uv_lock = "uv_lock")
load("//python/private/pypi:whl_library_alias.bzl", _whl_library_alias = "whl_library_alias")
load("//python/private/whl_filegroup:whl_filegroup.bzl", _whl_filegroup = "whl_filegroup")

compile_pip_requirements = pip_compile
package_annotation = _package_annotation
pip_parse = pip_repository
uv_lock = _uv_lock
whl_filegroup = _whl_filegroup

# Extra utilities visible to rules_python users.
Expand Down
31 changes: 30 additions & 1 deletion python/private/pypi/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ bzl_library(
":pep508_evaluate",
":pep508_requirement",
":select_whl",
":uv_lock_to_requirements",
"//python/private:normalize_name",
"//python/private:repo_utils",
"//python/uv/private:uv_lock_to_requirements",
],
)

Expand Down Expand Up @@ -435,6 +435,30 @@ bzl_library(
],
)

bzl_library(
name = "uv_lock",
srcs = ["uv_lock.bzl"],
deps = [
":multi_pip_parse",
":uv_lock_repo",
"//python:versions",
"@bazel_skylib//lib:types",
],
)

bzl_library(
name = "uv_lock_repo",
srcs = ["uv_lock_repo.bzl"],
deps = [
":attrs",
":pip_repository",
":pip_repository_attrs",
":uv_lock_to_requirements",
"//python/private:repo_utils",
"@toml.bzl//:toml",
],
)

bzl_library(
name = "venv_entry_point",
srcs = ["venv_entry_point.bzl"],
Expand Down Expand Up @@ -672,6 +696,11 @@ bzl_library(
srcs = ["platform.bzl"],
)

bzl_library(
name = "uv_lock_to_requirements",
srcs = ["uv_lock_to_requirements.bzl"],
)

bzl_library(
name = "version_from_filename",
srcs = ["version_from_filename.bzl"],
Expand Down
4 changes: 2 additions & 2 deletions python/private/pypi/multi_pip_parse.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def install_deps(**whl_library_kwargs):
rctx.file("requirements.bzl", requirements_bzl)
rctx.file("BUILD.bazel", "exports_files(['requirements.bzl'])")

_multi_pip_parse = repository_rule(
multi_pip_parse_hub_repo = repository_rule(
_multi_pip_parse_impl,
attrs = {
"default_version": attr.string(),
Expand Down Expand Up @@ -158,7 +158,7 @@ def multi_pip_parse(name, default_version, python_versions, python_interpreter_t
)
pip_parses[python_version] = pip_parse_name

return _multi_pip_parse(
return multi_pip_parse_hub_repo(
name = name,
default_version = default_version,
pip_parses = pip_parses,
Expand Down
2 changes: 1 addition & 1 deletion python/private/pypi/parse_requirements.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ behavior.

load("//python/private:normalize_name.bzl", "normalize_name")
load("//python/private:repo_utils.bzl", "repo_utils")
load("//python/uv/private:uv_lock_to_requirements.bzl", "uv_lock_extras_map") # buildifier: disable=bzl-visibility
load(":argparse.bzl", "argparse")
load(":hash.bzl", "hash")
load(":index_sources.bzl", "index_sources")
load(":parse_requirements_txt.bzl", "parse_requirements_txt")
load(":pep508_evaluate.bzl", "evaluate")
load(":pep508_requirement.bzl", "requirement")
load(":select_whl.bzl", "select_whl")
load(":uv_lock_to_requirements.bzl", "uv_lock_extras_map")

def parse_requirements(
ctx,
Expand Down
42 changes: 36 additions & 6 deletions python/private/pypi/pip_repository.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,14 @@ package(default_visibility = ["//visibility:public"])
exports_files(["requirements.bzl"])
"""

def _pip_repository_impl(rctx):
def render_pip_repository(rctx, *, requirements_txt = None):
"""Render a PyPI hub repository for WORKSPACE from requirements or uv.lock.

Args:
rctx: {type}`repository_ctx` Handle to the rule repository context.
requirements_txt: {type}`str | None` Optional pre-rendered requirements.txt
string (e.g. converted from `uv.lock`).
"""
logger = repo_utils.logger(rctx)
python_interpreter = pypi_repo_utils.resolve_python_interpreter(
rctx,
Expand All @@ -109,18 +116,38 @@ def _pip_repository_impl(rctx):
"windows_x86_64",
]

marker_env = env(os = rctx.os.name, arch = rctx.os.arch, python_version = python_version)
requirements_by_platform = parse_requirements(
rctx,
requirements_by_platform = requirements_files_by_platform(
if requirements_txt != None:
# Convert uv.lock to requirements.txt text and feed it through the
# requirements_lock parser via a stub context instead of passing uv_lock
# directly to parse_requirements(). The native uv_lock path in
# parse_requirements() requires populated whl_platform_tags/whl_abi_tags
# to select wheels and omits --hash flags from requirement_line, whereas
# WORKSPACE whl_library delegates resolution and hash verification to
# pip using requirement_line.
parse_ctx = struct(
read = lambda _: requirements_txt,
)
req_files_by_platform = requirements_files_by_platform(
requirements_lock = rctx.attr.uv_lock,
extra_pip_args = rctx.attr.extra_pip_args,
platforms = platforms,
)
else:
parse_ctx = rctx
req_files_by_platform = requirements_files_by_platform(
requirements_by_platform = rctx.attr.requirements_by_platform,
requirements_linux = rctx.attr.requirements_linux,
requirements_lock = rctx.attr.requirements_lock,
requirements_osx = rctx.attr.requirements_darwin,
requirements_windows = rctx.attr.requirements_windows,
extra_pip_args = rctx.attr.extra_pip_args,
platforms = platforms,
),
)

marker_env = env(os = rctx.os.name, arch = rctx.os.arch, python_version = python_version)
requirements_by_platform = parse_requirements(
parse_ctx,
requirements_by_platform = req_files_by_platform,
extra_pip_args = rctx.attr.extra_pip_args,
platforms = {
p: struct(
Expand Down Expand Up @@ -271,6 +298,9 @@ def _pip_repository_impl(rctx):

return

def _pip_repository_impl(rctx):
render_pip_repository(rctx)

pip_repository = repository_rule(
attrs = dict(
annotations = attr.string_dict(
Expand Down
83 changes: 83 additions & 0 deletions python/private/pypi/uv_lock.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""A WORKSPACE macro for installing PyPI dependencies from a uv.lock file."""

load("@bazel_skylib//lib:types.bzl", "types")
load("//python:versions.bzl", "MINOR_MAPPING")
load(":multi_pip_parse.bzl", "multi_pip_parse_hub_repo")
load(":uv_lock_repo.bzl", "uv_lock_repo")

def uv_lock(
name,
uv_lock,
default_version = None,
python_versions = None,
python_interpreter_target = None,
minor_mapping = None,
**kwargs):
"""Accepts a resolved `uv.lock` file and installs the dependencies listed within.

Supports both single-version and multi-version Python setups in `WORKSPACE`.

:::{versionadded} VERSION_NEXT_FEATURE
:::

Args:
name: {type}`str` The name of the generated PyPI hub repository.
uv_lock: {type}`Label` A resolved `uv.lock` file.
default_version: {type}`str | None` Optional default Python version when
configuring multiple Python versions.
python_versions: {type}`list[str] | None` Optional list of Python versions
when `python_interpreter_target` is a dict. Defaults to the keys of
`python_interpreter_target`.
python_interpreter_target: {type}`Label | str | dict[str, Label] | None`
Either a single target label for the host Python interpreter, or a
dict mapping Python `X.Y` versions to host interpreter targets.
minor_mapping: {type}`dict[str, str] | None` Optional mapping from `X.Y`
to `X.Y.Z` Python versions for multi-version hub aliases. Defaults
to `MINOR_MAPPING` from `//python:versions.bzl`.
**kwargs: Additional repository rule arguments forwarded to each
underlying PyPI repository (see {obj}`pip_parse` for supported
attributes).
"""
_uv_lock_repo = kwargs.pop("_uv_lock_repo", uv_lock_repo)
_multi_pip_parse_hub_repo = kwargs.pop(
"_multi_pip_parse_hub_repo",
multi_pip_parse_hub_repo,
)

if not types.is_dict(python_interpreter_target):
if python_interpreter_target != None:
kwargs["python_interpreter_target"] = python_interpreter_target
_uv_lock_repo(
name = name,
uv_lock = uv_lock,
**kwargs
)
return

python_versions = python_versions or sorted(python_interpreter_target.keys())
minor_mapping = minor_mapping or MINOR_MAPPING
pip_parses = {}
for python_version in python_versions:
if python_version not in python_interpreter_target:
fail(
"Missing python_interpreter_target for Python version %s in '%s'" % (
python_version,
name,
),
)

spoke_name = name + "_" + python_version.replace(".", "_")
_uv_lock_repo(
name = spoke_name,
python_interpreter_target = python_interpreter_target[python_version],
uv_lock = uv_lock,
**kwargs
)
pip_parses[python_version] = spoke_name

_multi_pip_parse_hub_repo(
name = name,
default_version = default_version,
pip_parses = pip_parses,
minor_mapping = minor_mapping,
)
53 changes: 53 additions & 0 deletions python/private/pypi/uv_lock_repo.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""A WORKSPACE repository rule for installing PyPI dependencies from uv.lock."""

load("@toml.bzl", "toml")
load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR")
load(":attrs.bzl", COMMON_ATTRS = "ATTRS")
load(":pip_repository.bzl", "render_pip_repository")
load(":pip_repository_attrs.bzl", "ATTRS")
load(":uv_lock_to_requirements.bzl", "uv_lock_to_requirements")

def _uv_lock_repo_impl(rctx):
uv_lock_content = toml.decode(rctx.read(rctx.attr.uv_lock))
requirements_txt = uv_lock_to_requirements(uv_lock_content)
render_pip_repository(
rctx,
requirements_txt = requirements_txt,
)

uv_lock_repo = repository_rule(
attrs = dict(
annotations = attr.string_dict(
doc = """
Optional annotations to apply to packages. Keys should be package names, with
capitalization matching the input lockfile, and values should be generated
using the `package_annotation` macro.
""",
),
experimental_requirement_cycles = ATTRS["experimental_requirement_cycles"],
extra_hub_aliases = ATTRS["extra_hub_aliases"],
use_hub_alias_dependencies = ATTRS["use_hub_alias_dependencies"],
uv_lock = attr.label(
allow_single_file = True,
mandatory = True,
doc = """
A resolved `uv.lock` file containing the transitive set of dependencies.
""",
),
_config_template = attr.label(
default = ":config.bzl.tmpl",
),
_template = attr.label(
default = ":requirements.bzl.tmpl.workspace",
),
**COMMON_ATTRS
),
doc = """
Accepts a resolved `uv.lock` file and installs the dependencies listed within.
""",
implementation = _uv_lock_repo_impl,
environ = [
"RULES_PYTHON_PIP_ISOLATED",
REPO_DEBUG_ENV_VAR,
],
)
Loading
Loading