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
11 changes: 8 additions & 3 deletions docs/uv.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,14 @@ uv.override_package(
use_repo(uv, "pypi")
```

We can configure a default dependency group by setting the `dep_group` flag on our hub as part of the `.bazelrc`.
Each `[dependency-group]` of the `pyproject.toml` is registered as a named dependency group.
If no dependency groups are listed, an implicit default group with the name of the project itself is created.
We can configure a default dependency group by setting the `dep_group` flag on
our hub as part of the `.bazelrc`. Each entry in `[dependency-groups]` of the
`pyproject.toml` is registered as a named dependency group. Requirements in
`project.dependencies` remain available in every named group; each dependency
group adds its own requirements to that shared base. Set
`include_project_dependencies = False` on `uv.project()` when named groups
should expose only their own requirements. If no dependency groups are listed,
an implicit default group with the name of the project itself is created.

```
# .bazelrc
Expand Down
14 changes: 8 additions & 6 deletions examples/dev_deps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ dev = [
```

The `dev` group composes `prod` (the runtime deps) with dev-only tools using
PEP 735's `include-group` syntax. `uv lock` resolves all groups into a
single lockfile.
PEP 735's `include-group` syntax. The project's default `build` and
`setuptools` dependencies remain available in both groups. `uv lock` resolves
all groups into a single lockfile.

### 2. Venv selection via `.bazelrc`

Expand All @@ -46,8 +47,9 @@ common:release --stamp
```

The `--@pypi//dep_group=` flag controls which dependency group the hub makes
available. The default is `dev` (everything importable). `--config=release`
switches to `prod` (runtime deps only).
available in addition to the project's default dependencies. The default is
`dev` (everything importable); `--config=release` switches to `prod` (the
runtime group, without dev-only tools).

### 3. A `string_flag` to control dep inclusion

Expand Down Expand Up @@ -112,8 +114,8 @@ The venv flag and the mode flag work together:

| `.bazelrc` config | `--@pypi//dep_group=` | `--//:mode=` | Effect |
| ------------------ | ---------------- | ------------ | ------------------------------------------------------------ |
| _(default)_ | `dev` | `dev` | Hub exposes all packages; `select()` includes dev_deps |
| `--config=release` | `prod` | `prod` | Hub exposes only prod packages; `select()` excludes dev_deps |
| _(default)_ | `dev` | `dev` | Default + prod + dev; `select()` includes dev_deps |
| `--config=release` | `prod` | `prod` | Default + prod; `select()` excludes dev_deps |

The venv flag controls which packages the hub _makes available_ (which wheels
are fetched and linked). The mode flag controls which packages the target
Expand Down
17 changes: 16 additions & 1 deletion uv/private/extension/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,16 @@ def _parse_projects(module_ctx, hub_specs):

whl_configurations.update(collect_configurations(lock_data))

configuration_names, activated_extras = collect_activated_extras(project.lock, project_id, project_data, lock_data, default_versions, marker_graph, package_versions)
configuration_names, activated_extras = collect_activated_extras(
project.lock,
project_id,
project_data,
lock_data,
default_versions,
marker_graph,
package_versions,
include_project_dependencies = project.include_project_dependencies,
)
version_activations = collate_versions_by_name(activated_extras)

# Mapping from SCC ID to marked SCC members
Expand Down Expand Up @@ -791,6 +800,12 @@ _project_tag = tag_class(
"build",
],
),
"include_project_dependencies": attr.bool(
default = True,
doc = "Whether `project.dependencies` remain available in every explicit " +
"dependency group. Set to false to make selected groups expose only " +
"their own requirements.",
),
"unstable_configure_command": attr.string_list(
mandatory = False,
doc = "Command to run as the sdist configure tool. Each element is either " +
Expand Down
25 changes: 22 additions & 3 deletions uv/private/extension/projectfile.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,15 @@ def _extract_lockfile_group_versions(lock_id, lock_data):
result.setdefault(group_name, {})[pkg_name] = (lock_id, pkg_name, dep["version"], "__base__")
return result

def collect_activated_extras(projectfile, lock_id, project_data, lock_data, default_versions, graph, package_versions = {}):
def collect_activated_extras(
projectfile,
lock_id,
project_data,
lock_data,
default_versions,
graph,
package_versions = {},
include_project_dependencies = True):
"""Collects the set of transitively activated extras for each configuration.

This function determines the full set of extras that are activated for each
Expand All @@ -162,6 +170,10 @@ def collect_activated_extras(projectfile, lock_id, project_data, lock_data, defa
default_versions: A dictionary mapping package names to their default
version dependency tuples.
graph: The dependency graph, as returned by `build_marker_graph`.
include_project_dependencies: Whether requirements from
`project.dependencies` are included in every explicit dependency
group. Set to false to make named groups expose only their own
resolved requirements.

Returns:
A tuple containing:
Expand All @@ -172,12 +184,19 @@ def collect_activated_extras(projectfile, lock_id, project_data, lock_data, defa
`{dep: {cfg: {extra_dep: {marker: 1}}}}`.
"""

# If no dependency-groups are specified, use the lock members manifest, or just the self-list
# If no dependency-groups are specified, use the lock members manifest, or just the self-list.
# Otherwise, project dependencies are the common base of every explicit group
# unless the project explicitly opts out.
# PEP 735 groups add requirements to a project; they do not replace
# `project.dependencies`.
dep_groups = project_data.get("dependency-groups", {
project_data["project"]["name"]: lock_data.get("manifest", {}).get("members", [
project_data["project"]["name"],
]),
})
default_specs = project_data["project"].get("dependencies", []) if (
include_project_dependencies and "dependency-groups" in project_data
) else []

# Normalize dep groups to our dependency triples (graph keys)
normalized_dep_groups = {}
Expand All @@ -190,7 +209,7 @@ def collect_activated_extras(projectfile, lock_id, project_data, lock_data, defa
lockfile_group_versions = _extract_lockfile_group_versions(lock_id, lock_data)

for group_name in dep_groups.keys():
resolved_specs = resolve_dependency_group_specs(dep_groups, group_name)
resolved_specs = default_specs + resolve_dependency_group_specs(dep_groups, group_name)

group_preferences = dict(lockfile_group_versions.get(group_name, {}))

Expand Down
69 changes: 69 additions & 0 deletions uv/private/extension/test_projectfile.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,74 @@ collect_activated_extras_transitive_remap_test = unittest.make(
_collect_activated_extras_transitive_remap_test_impl,
)

def _collect_activated_extras_keeps_project_dependencies_in_groups_test_impl(ctx):
"""Dependency groups don't replace project dependencies through their existence."""
env = unittest.begin(ctx)
project_data = {
"project": {
"name": "test_project",
"dependencies": ["base"],
},
"dependency-groups": {
"prod": ["runtime"],
"dev": [
{"include-group": "prod"},
"devtool",
],
},
}
default_versions = {
name: ("lock", name, "1.0.0", "__base__")
for name in ["base", "runtime", "devtool"]
}
graph = {
("lock", name, "1.0.0", "__base__"): {}
for name in ["base", "runtime", "devtool"]
}

configuration_names, activated_extras = collect_activated_extras(
"//:pyproject.toml",
"lock",
project_data,
{"package": []},
default_versions,
graph,
)

base = ("lock", "base", "1.0.0", "__base__")
runtime = ("lock", "runtime", "1.0.0", "__base__")
devtool = ("lock", "devtool", "1.0.0", "__base__")

asserts.equals(env, {"prod": 1, "dev": 1}, configuration_names)
asserts.true(env, "prod" in activated_extras[base])
asserts.true(env, "dev" in activated_extras[base])
asserts.true(env, "prod" in activated_extras[runtime])
asserts.true(env, "dev" in activated_extras[runtime])
asserts.false(env, "prod" in activated_extras[devtool])
asserts.true(env, "dev" in activated_extras[devtool])

_configuration_names, group_only_extras = collect_activated_extras(
"//:pyproject.toml",
"lock",
project_data,
{"package": []},
default_versions,
graph,
include_project_dependencies = False,
)

asserts.false(env, base in group_only_extras)
asserts.true(env, "prod" in group_only_extras[runtime])
asserts.true(env, "dev" in group_only_extras[runtime])
asserts.false(env, "prod" in group_only_extras[devtool])
asserts.true(env, "dev" in group_only_extras[devtool])

return unittest.end(env)

collect_activated_extras_keeps_project_dependencies_in_groups_test = unittest.make(
_collect_activated_extras_keeps_project_dependencies_in_groups_test_impl,
)

def projectfile_test_suite():
unittest.suite(
"extract_requirement_marker_pairs_tests",
Expand All @@ -210,4 +278,5 @@ def projectfile_test_suite():
extract_requirement_marker_pairs_preferred_overrides_version_map_test,
extract_requirement_marker_pairs_preferred_overrides_multi_version_test,
collect_activated_extras_transitive_remap_test,
collect_activated_extras_keeps_project_dependencies_in_groups_test,
)
Loading