Skip to content

Commit f22eff9

Browse files
authored
docs: document virtual_deps and resolutions (#448)
Fixes a docs FIXME. Also show how a dependency can be dropped altogether using an empty filegroup, as requested by a client. --- ### Changes are visible to end-users: no ### Test plan - Covered by existing test cases
1 parent aae7c24 commit f22eff9

File tree

9 files changed

+144
-12
lines changed

9 files changed

+144
-12
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
docs/*.md linguist-generated=true
22
docs/migrating.md linguist-generated=false
3+
docs/virtual_deps.md linguist-generated=false
34

45
# Configuration for 'git archive'
56
# see https://git-scm.com/docs/git-archive/2.40.0#ATTRIBUTES

docs/py_binary.md

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/py_library.md

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/py_test.md

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/venv.md

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/virtual_deps.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Resolution of "virtual" dependencies
2+
3+
rules_py allows external Python dependencies to be specified by name rather than as a label to an installed package, using a concept called "virtual" dependencies.
4+
5+
Virtual dependencies allow the terminal rule (for example, a `py_binary` or `py_test`) to control the version of the package which is used to satisfy the dependency, by providing a mapping from the package name to the label of an installed package that provides it.
6+
7+
This feature allows:
8+
- for individual projects within a monorepo to upgrade their dependencies independently of other projects within the same repository
9+
- overriding a single version of a dependency for a py_binary or py_test
10+
- to test against a range of different versions of dependencies for a single library
11+
12+
Links to design docs are available on the original feature request:
13+
https://github.com/aspect-build/rules_py/issues/213
14+
15+
## Declaring a dependency as virtual
16+
17+
Simply move an element from the `deps` attribute to `virtual_deps`.
18+
19+
For example, instead of getting a specific version of Django from
20+
`deps = ["@pypi_django//:pkg"]` on a `py_library` target,
21+
provide the package name with `virtual_deps = ["django"]`.
22+
23+
> Note that any `py_binary` or `py_test` transitively depending on this `py_library` must be loaded from `aspect_rules_py` rather than `rules_python`, as the latter does not have a feature of resolving the virtual dep.
24+
25+
## Resolving to a package installed by rules_python
26+
27+
Typically, users write one or more `pip_parse` statements in `WORKSPACE` or `pip.parse` in `MODULE.bazel` to read requirements files, and install the referenced packages into an external repository. For example, from the [rules_python docs](https://rules-python.readthedocs.io/en/latest/pypi-dependencies.html#using-dependencies-from-pypi):
28+
29+
```
30+
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
31+
pip.parse(
32+
hub_name = "my_deps",
33+
python_version = "3.11",
34+
requirements_lock = "//:requirements_lock_3_11.txt",
35+
)
36+
use_repo(pip, "my_deps")
37+
```
38+
39+
rules_python writes a `requirements.bzl` file which provides some symbols to work with the installed packages:
40+
41+
```
42+
load("@my_deps//:requirements.bzl", "all_whl_requirements_by_package", "requirement")
43+
```
44+
45+
These can be used to resolve a virtual dependency. Continuing the Django example above, a binary rule can specify which external repository to resolve to:
46+
47+
```
48+
load("@aspect_rules_py//py:defs.bzl", "resolutions")
49+
50+
py_binary(
51+
name = "manage",
52+
srcs = ["manage.py"],
53+
# Resolve django to the "standard" one from our requirements.txt
54+
resolutions = resolutions.from_requirements(all_whl_requirements_by_package, requirement),
55+
)
56+
```
57+
58+
## Resolving directly to a binary wheel
59+
60+
It's possible to fetch a wheel file directly without using `pip` or any repository rules from `rules_python`, using the Bazel downloader.
61+
62+
`MODULE.bazel`:
63+
64+
```
65+
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
66+
67+
http_file(
68+
name = "django_4_2_4",
69+
urls = ["https://files.pythonhosted.org/packages/7f/9e/fc6bab255ae10bc57fa2f65646eace3d5405fbb7f5678b90140052d1db0f/Django-4.2.4-py3-none-any.whl"],
70+
sha256 = "860ae6a138a238fc4f22c99b52f3ead982bb4b1aad8c0122bcd8c8a3a02e409d",
71+
downloaded_file_path = "Django-4.2.4-py3-none-any.whl",
72+
)
73+
```
74+
75+
Then in a `BUILD` file, extract it to a directory:
76+
77+
```
78+
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_unpacked_wheel")
79+
80+
# Extract the downloaded wheel to a directory
81+
py_unpacked_wheel(
82+
name = "django_4_2_4",
83+
src = "@django_4_2_4//file",
84+
)
85+
86+
py_binary(
87+
name = "manage.override_django",
88+
srcs = ["proj/manage.py"],
89+
resolutions = {
90+
# replace the resolution of django with that specific wheel
91+
"django": ":django_4_2_4",
92+
},
93+
deps = [":proj"],
94+
)
95+
```
96+

py/private/py_library.bzl

+3-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ _attrs = dict({
221221
default = [],
222222
),
223223
"resolutions": attr.label_keyed_string_dict(
224-
doc = "FIXME",
224+
doc = """Satisfy a virtual_dep with a mapping from external package name to the label of an installed package that provides it.
225+
See [virtual dependencies](/docs/virtual_deps.md).
226+
""",
225227
),
226228
})
227229

py/private/py_venv.bzl

+2-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ py_venv_rule = rule(
110110
mandatory = False,
111111
),
112112
"resolutions": attr.label_keyed_string_dict(
113-
doc = "FIXME",
113+
doc = """Satisfy a virtual_dep with a mapping from external package name to the label of an installed package that provides it.
114+
See [virtual dependencies](/docs/virtual_deps.md).""",
114115
),
115116
"package_collisions": attr.string(
116117
doc = """The action that should be taken when a symlink collision is encountered when creating the venv.

py/tests/virtual/django/BUILD.bazel

+38-6
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,13 @@ load("//py:defs.bzl", "py_binary", "py_library", "py_unpacked_wheel", "resolutio
44

55
django_resolutions = resolutions.from_requirements(all_whl_requirements_by_package, requirement)
66

7-
py_unpacked_wheel(
8-
name = "django_wheel",
9-
src = "@django_4_2_4//file",
10-
)
11-
127
compile_pip_requirements(
138
name = "requirements",
149
requirements_in = "requirements.in",
1510
requirements_txt = "requirements.txt",
1611
)
1712

13+
# Test fixture: a library with an external dependency
1814
py_library(
1915
name = "proj",
2016
srcs = glob(["proj/**/*.py"]),
@@ -25,6 +21,9 @@ py_library(
2521
virtual_deps = ["django"],
2622
)
2723

24+
## Use case 1
25+
# Resolve it using the result of a rules_python pip.parse call.
26+
# It will use pip install behind the scenes.
2827
py_binary(
2928
name = "manage",
3029
srcs = ["proj/manage.py"],
@@ -36,13 +35,46 @@ py_binary(
3635
],
3736
)
3837

38+
## Use case 2
39+
# Use a binary wheel that was downloaded with http_file, bypassing rules_python and its
40+
# pip install repository rules.
41+
py_unpacked_wheel(
42+
name = "django_4_2_4",
43+
src = "@django_4_2_4//file",
44+
)
45+
46+
# bazel run //py/tests/virtual/django:manage.override_django -- --version
47+
# Django Version: 4.2.4
3948
py_binary(
4049
name = "manage.override_django",
4150
srcs = ["proj/manage.py"],
51+
# package_collisions = "warning",
4252
# Install the dependencies that the pip_parse rule defined as defaults...
4353
resolutions = django_resolutions.override({
4454
# ...but replace the resolution of django with a specific wheel fetched by http_file.
45-
"django": "//py/tests/virtual/django:django_wheel",
55+
"django": ":django_4_2_4",
56+
}),
57+
deps = [":proj"],
58+
)
59+
60+
## Use case 3
61+
# It's possible to completely remove a dependency.
62+
# For example, to reduce the size of an image when a transitive dep is known to be unused.
63+
filegroup(
64+
name = "empty",
65+
)
66+
67+
# bazel run //py/tests/virtual/django:manage.remove_django -- --version
68+
# ImportError: Couldn't import Django.
69+
# Are you sure it's installed and available on your PYTHONPATH environment variable?
70+
# Did you forget to activate a virtual environment?
71+
py_binary(
72+
name = "manage.remove_django",
73+
srcs = ["proj/manage.py"],
74+
package_collisions = "warning",
75+
resolutions = django_resolutions.override({
76+
# Replace the resolution of django with an empty folder
77+
"django": ":empty",
4678
}),
4779
deps = [":proj"],
4880
)

0 commit comments

Comments
 (0)