Skip to content

Commit 1249567

Browse files
UebelAndreaignas
andauthored
feat(runfiles): Add static methods to Runfiles class to simplify interface (#1656)
With this change we allow for more ergonomic creation of the runfiles object and using type annotations: ```python from python.runfiles import Runfiles def main() -> None: runfiles: Runfiles | None = Runfiles.Create() ``` Furthermore, the docs have been updated to describe pulling `runfiles` library directly from `rules_python` to avoid the complexity of needing to setup `pip_parse` to use this behavior. --------- Co-authored-by: Ignas Anikevicius <[email protected]>
1 parent 4fe0db3 commit 1249567

File tree

5 files changed

+149
-93
lines changed

5 files changed

+149
-93
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ A brief description of the categories of changes:
4040
Python. This means that `WORKSPACE` users can now copy the `requirements.bzl`
4141
file for vendoring as seen in the updated `pip_parse_vendored` example.
4242

43+
* (runfiles) `rules_python.python.runfiles.Runfiles` now has a static `Create`
44+
method to make imports more ergonomic. Users should only need to import the
45+
`Runfiles` object to locate runfiles.
46+
4347
## [0.28.0] - 2024-01-07
4448

4549
[0.28.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.28.0

python/runfiles/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ py_wheel(
4646
"Development Status :: 5 - Production/Stable",
4747
"License :: OSI Approved :: Apache Software License",
4848
],
49-
description_file = "README.rst",
49+
description_file = "README.md",
5050
dist_folder = "dist",
5151
distribution = "bazel_runfiles",
5252
homepage = "https://github.com/bazelbuild/rules_python",

python/runfiles/README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# bazel-runfiles library
2+
3+
This is a Bazel Runfiles lookup library for Bazel-built Python binaries and tests.
4+
5+
Learn about runfiles: read [Runfiles guide](https://bazel.build/extending/rules#runfiles)
6+
or watch [Fabian's BazelCon talk](https://www.youtube.com/watch?v=5NbgUMH1OGo).
7+
8+
## Importing
9+
10+
The Runfiles API is available from two sources, a direct Bazel target, and a [pypi](https://pypi.org/) package.
11+
12+
## Pure Bazel imports
13+
14+
1. Depend on this runfiles library from your build rule, like you would other third-party libraries:
15+
16+
```python
17+
py_binary(
18+
name = "my_binary",
19+
# ...
20+
deps = ["@rules_python//python/runfiles"],
21+
)
22+
```
23+
24+
2. Import the runfiles library:
25+
26+
```python
27+
from python.runfiles import Runfiles
28+
```
29+
30+
## Pypi imports
31+
32+
1. Add the 'bazel-runfiles' dependency along with other third-party dependencies, for example in your `requirements.txt` file.
33+
34+
2. Depend on this runfiles library from your build rule, like you would other third-party libraries:
35+
```python
36+
load("@pip_deps//:requirements.bzl", "requirement")
37+
38+
py_binary(
39+
name = "my_binary",
40+
...
41+
deps = [requirement("bazel-runfiles")],
42+
)
43+
```
44+
45+
3. Import the runfiles library:
46+
```python
47+
from runfiles import Runfiles
48+
```
49+
50+
## Typical Usage
51+
52+
Create a `Runfiles` object and use `Rlocation` to look up runfile paths:
53+
54+
```python
55+
r = Runfiles.Create()
56+
# ...
57+
with open(r.Rlocation("my_workspace/path/to/my/data.txt"), "r") as f:
58+
contents = f.readlines()
59+
# ...
60+
```
61+
62+
The code above creates a manifest- or directory-based implementation based on the environment variables in `os.environ`. See `Runfiles.Create()` for more info.
63+
64+
If you want to explicitly create a manifest- or directory-based
65+
implementation, you can do so as follows:
66+
67+
```python
68+
r1 = Runfiles.CreateManifestBased("path/to/foo.runfiles_manifest")
69+
70+
r2 = Runfiles.CreateDirectoryBased("path/to/foo.runfiles/")
71+
```
72+
73+
If you want to start subprocesses, and the subprocess can't automatically
74+
find the correct runfiles directory, you can explicitly set the right
75+
environment variables for them:
76+
77+
```python
78+
import subprocess
79+
from python.runfiles import Runfiles
80+
81+
r = Runfiles.Create()
82+
env = {}
83+
# ...
84+
env.update(r.EnvVars())
85+
p = subprocess.run(
86+
[r.Rlocation("path/to/binary")],
87+
env=env,
88+
# ...
89+
)
90+
```

python/runfiles/README.rst

Lines changed: 0 additions & 56 deletions
This file was deleted.

python/runfiles/runfiles.py

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
"""Runfiles lookup library for Bazel-built Python binaries and tests.
1616
17-
See @rules_python//python/runfiles/README.rst for usage instructions.
17+
See @rules_python//python/runfiles/README.md for usage instructions.
1818
"""
1919
import inspect
2020
import os
@@ -267,6 +267,56 @@ def CurrentRepository(self, frame: int = 1) -> str:
267267
# canonical name.
268268
return caller_runfiles_directory
269269

270+
# TODO: Update return type to Self when 3.11 is the min version
271+
# https://peps.python.org/pep-0673/
272+
@staticmethod
273+
def CreateManifestBased(manifest_path: str) -> "Runfiles":
274+
return Runfiles(_ManifestBased(manifest_path))
275+
276+
# TODO: Update return type to Self when 3.11 is the min version
277+
# https://peps.python.org/pep-0673/
278+
@staticmethod
279+
def CreateDirectoryBased(runfiles_dir_path: str) -> "Runfiles":
280+
return Runfiles(_DirectoryBased(runfiles_dir_path))
281+
282+
# TODO: Update return type to Self when 3.11 is the min version
283+
# https://peps.python.org/pep-0673/
284+
@staticmethod
285+
def Create(env: Optional[Dict[str, str]] = None) -> Optional["Runfiles"]:
286+
"""Returns a new `Runfiles` instance.
287+
288+
The returned object is either:
289+
- manifest-based, meaning it looks up runfile paths from a manifest file, or
290+
- directory-based, meaning it looks up runfile paths under a given directory
291+
path
292+
293+
If `env` contains "RUNFILES_MANIFEST_FILE" with non-empty value, this method
294+
returns a manifest-based implementation. The object eagerly reads and caches
295+
the whole manifest file upon instantiation; this may be relevant for
296+
performance consideration.
297+
298+
Otherwise, if `env` contains "RUNFILES_DIR" with non-empty value (checked in
299+
this priority order), this method returns a directory-based implementation.
300+
301+
If neither cases apply, this method returns null.
302+
303+
Args:
304+
env: {string: string}; optional; the map of environment variables. If None,
305+
this function uses the environment variable map of this process.
306+
Raises:
307+
IOError: if some IO error occurs.
308+
"""
309+
env_map = os.environ if env is None else env
310+
manifest = env_map.get("RUNFILES_MANIFEST_FILE")
311+
if manifest:
312+
return CreateManifestBased(manifest)
313+
314+
directory = env_map.get("RUNFILES_DIR")
315+
if directory:
316+
return CreateDirectoryBased(directory)
317+
318+
return None
319+
270320

271321
# Support legacy imports by defining a private symbol.
272322
_Runfiles = Runfiles
@@ -309,44 +359,12 @@ def _ParseRepoMapping(repo_mapping_path: Optional[str]) -> Dict[Tuple[str, str],
309359

310360

311361
def CreateManifestBased(manifest_path: str) -> Runfiles:
312-
return Runfiles(_ManifestBased(manifest_path))
362+
return Runfiles.CreateManifestBased(manifest_path)
313363

314364

315365
def CreateDirectoryBased(runfiles_dir_path: str) -> Runfiles:
316-
return Runfiles(_DirectoryBased(runfiles_dir_path))
366+
return Runfiles.CreateDirectoryBased(runfiles_dir_path)
317367

318368

319369
def Create(env: Optional[Dict[str, str]] = None) -> Optional[Runfiles]:
320-
"""Returns a new `Runfiles` instance.
321-
322-
The returned object is either:
323-
- manifest-based, meaning it looks up runfile paths from a manifest file, or
324-
- directory-based, meaning it looks up runfile paths under a given directory
325-
path
326-
327-
If `env` contains "RUNFILES_MANIFEST_FILE" with non-empty value, this method
328-
returns a manifest-based implementation. The object eagerly reads and caches
329-
the whole manifest file upon instantiation; this may be relevant for
330-
performance consideration.
331-
332-
Otherwise, if `env` contains "RUNFILES_DIR" with non-empty value (checked in
333-
this priority order), this method returns a directory-based implementation.
334-
335-
If neither cases apply, this method returns null.
336-
337-
Args:
338-
env: {string: string}; optional; the map of environment variables. If None,
339-
this function uses the environment variable map of this process.
340-
Raises:
341-
IOError: if some IO error occurs.
342-
"""
343-
env_map = os.environ if env is None else env
344-
manifest = env_map.get("RUNFILES_MANIFEST_FILE")
345-
if manifest:
346-
return CreateManifestBased(manifest)
347-
348-
directory = env_map.get("RUNFILES_DIR")
349-
if directory:
350-
return CreateDirectoryBased(directory)
351-
352-
return None
370+
return Runfiles.Create(env)

0 commit comments

Comments
 (0)