diff --git a/docs/uv.md b/docs/uv.md index 6d4e90d7d..4cb52d50d 100644 --- a/docs/uv.md +++ b/docs/uv.md @@ -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 diff --git a/examples/dev_deps/README.md b/examples/dev_deps/README.md index d61274338..ae7f8be7c 100644 --- a/examples/dev_deps/README.md +++ b/examples/dev_deps/README.md @@ -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` @@ -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 @@ -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 diff --git a/uv/private/extension/defs.bzl b/uv/private/extension/defs.bzl index e78b2830b..510c86fdd 100644 --- a/uv/private/extension/defs.bzl +++ b/uv/private/extension/defs.bzl @@ -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 @@ -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 " + diff --git a/uv/private/extension/projectfile.bzl b/uv/private/extension/projectfile.bzl index 56083a3aa..127a3075f 100644 --- a/uv/private/extension/projectfile.bzl +++ b/uv/private/extension/projectfile.bzl @@ -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 @@ -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: @@ -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 = {} @@ -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, {})) diff --git a/uv/private/extension/test_projectfile.bzl b/uv/private/extension/test_projectfile.bzl index 8ba2e112d..02c3ec3ef 100644 --- a/uv/private/extension/test_projectfile.bzl +++ b/uv/private/extension/test_projectfile.bzl @@ -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", @@ -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, )