Skip to content

Commit d60ce2c

Browse files
fmeumcopybara-github
authored andcommitted
Add CurrentRepository() to Python runfiles library
`runfiles.CurrentRepository()` can be used to get the canonical name of the Bazel repository containing the caller at runtime. This information is required to look up runfiles while taking repository mappings into account. Work towards bazelbuild#16124 Closes bazelbuild#16341. PiperOrigin-RevId: 483400557 Change-Id: Ia906c42bad6ca0935b86b958e4401c6745789dff
1 parent 6578b5b commit d60ce2c

File tree

6 files changed

+238
-3
lines changed

6 files changed

+238
-3
lines changed

src/test/py/bazel/py_test.py

+111-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def testSmoke(self):
4949
self.createSimpleFiles()
5050
exit_code, stdout, stderr = self.RunBazel(['run', '//a:a'])
5151
self.AssertExitCode(exit_code, 0, stderr)
52-
self.assertTrue('Hello, World' in stdout)
52+
self.assertIn('Hello, World', stdout)
5353

5454
def testRunfilesSymlinks(self):
5555
if test_base.TestBase.IsWindows():
@@ -208,5 +208,115 @@ def testPyTestWithStdlibCollisionRunsRemotely(self):
208208
self.assertIn('Test ran', stdout)
209209

210210

211+
class PyRunfilesLibraryTest(test_base.TestBase):
212+
213+
def testPyRunfilesLibraryCurrentRepository(self):
214+
self.CreateWorkspaceWithDefaultRepos('WORKSPACE', [
215+
'local_repository(', ' name = "other_repo",',
216+
' path = "other_repo_path",', ')'
217+
])
218+
219+
self.ScratchFile('pkg/BUILD.bazel', [
220+
'py_library(',
221+
' name = "library",',
222+
' srcs = ["library.py"],',
223+
' visibility = ["//visibility:public"],',
224+
' deps = ["@bazel_tools//tools/python/runfiles"],',
225+
')',
226+
'',
227+
'py_binary(',
228+
' name = "binary",',
229+
' srcs = ["binary.py"],',
230+
' deps = [',
231+
' ":library",',
232+
' "@bazel_tools//tools/python/runfiles",',
233+
' ],',
234+
')',
235+
'',
236+
'py_test(',
237+
' name = "test",',
238+
' srcs = ["test.py"],',
239+
' deps = [',
240+
' ":library",',
241+
' "@bazel_tools//tools/python/runfiles",',
242+
' ],',
243+
')',
244+
])
245+
self.ScratchFile('pkg/library.py', [
246+
'from bazel_tools.tools.python.runfiles import runfiles',
247+
'def print_repo_name():',
248+
' print("in pkg/library.py: \'%s\'" % runfiles.Create().CurrentRepository())',
249+
])
250+
self.ScratchFile('pkg/binary.py', [
251+
'from bazel_tools.tools.python.runfiles import runfiles',
252+
'from pkg import library',
253+
'library.print_repo_name()',
254+
'print("in pkg/binary.py: \'%s\'" % runfiles.Create().CurrentRepository())',
255+
])
256+
self.ScratchFile('pkg/test.py', [
257+
'from bazel_tools.tools.python.runfiles import runfiles',
258+
'from pkg import library',
259+
'library.print_repo_name()',
260+
'print("in pkg/test.py: \'%s\'" % runfiles.Create().CurrentRepository())',
261+
])
262+
263+
self.ScratchFile('other_repo_path/WORKSPACE')
264+
self.ScratchFile('other_repo_path/pkg/BUILD.bazel', [
265+
'py_binary(',
266+
' name = "binary",',
267+
' srcs = ["binary.py"],',
268+
' deps = [',
269+
' "@//pkg:library",',
270+
' "@bazel_tools//tools/python/runfiles",',
271+
' ],',
272+
')',
273+
'',
274+
'py_test(',
275+
' name = "test",',
276+
' srcs = ["test.py"],',
277+
' deps = [',
278+
' "@//pkg:library",',
279+
' "@bazel_tools//tools/python/runfiles",',
280+
' ],',
281+
')',
282+
])
283+
self.ScratchFile('other_repo_path/pkg/binary.py', [
284+
'from bazel_tools.tools.python.runfiles import runfiles',
285+
'from pkg import library',
286+
'library.print_repo_name()',
287+
'print("in external/other_repo/pkg/binary.py: \'%s\'" % runfiles.Create().CurrentRepository())',
288+
])
289+
self.ScratchFile('other_repo_path/pkg/test.py', [
290+
'from bazel_tools.tools.python.runfiles import runfiles',
291+
'from pkg import library',
292+
'library.print_repo_name()',
293+
'print("in external/other_repo/pkg/test.py: \'%s\'" % runfiles.Create().CurrentRepository())',
294+
])
295+
296+
exit_code, stdout, stderr = self.RunBazel(['run', '//pkg:binary'])
297+
self.AssertExitCode(exit_code, 0, stderr, stdout)
298+
self.assertIn('in pkg/binary.py: \'\'', stdout)
299+
self.assertIn('in pkg/library.py: \'\'', stdout)
300+
301+
exit_code, stdout, stderr = self.RunBazel(
302+
['test', '//pkg:test', '--test_output=streamed'])
303+
self.AssertExitCode(exit_code, 0, stderr, stdout)
304+
self.assertIn('in pkg/test.py: \'\'', stdout)
305+
self.assertIn('in pkg/library.py: \'\'', stdout)
306+
307+
exit_code, stdout, stderr = self.RunBazel(
308+
['run', '@other_repo//pkg:binary'])
309+
self.AssertExitCode(exit_code, 0, stderr, stdout)
310+
self.assertIn('in external/other_repo/pkg/binary.py: \'other_repo\'',
311+
stdout)
312+
self.assertIn('in pkg/library.py: \'\'', stdout)
313+
314+
exit_code, stdout, stderr = self.RunBazel(
315+
['test', '@other_repo//pkg:test', '--test_output=streamed'])
316+
self.AssertExitCode(exit_code, 0, stderr, stdout)
317+
self.assertIn('in external/other_repo/pkg/test.py: \'other_repo\'', stdout)
318+
self.assertIn('in pkg/library.py: \'\'', stdout)
319+
320+
211321
if __name__ == '__main__':
212322
unittest.main()
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright 2022 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""A rule to generate a Python source file containing the main repo's
16+
runfiles directory name."""
17+
18+
_RUNFILES_CONSTANTS_TEMPLATE = """# Generated by gen_runfiles_constants.bzl
19+
# Internal-only; do no use.
20+
# The name of the runfiles directory corresponding to the main repository.
21+
MAIN_REPOSITORY_RUNFILES_DIRECTORY = '%s'
22+
"""
23+
24+
def _gen_runfiles_constants_impl(ctx):
25+
out = ctx.actions.declare_file(ctx.attr.name + ".py")
26+
ctx.actions.write(out, _RUNFILES_CONSTANTS_TEMPLATE % ctx.workspace_name)
27+
28+
return DefaultInfo(
29+
files = depset([out]),
30+
runfiles = ctx.runfiles([out]),
31+
)
32+
33+
gen_runfiles_constants = rule(
34+
implementation = _gen_runfiles_constants_impl,
35+
)

tools/python/runfiles/BUILD

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
load("//tools/python:gen_runfiles_constants.bzl", "gen_runfiles_constants")
12
load("//tools/python:private/defs.bzl", "py_library", "py_test")
23

34
package(default_visibility = ["//visibility:private"])
@@ -20,7 +21,14 @@ filegroup(
2021
py_library(
2122
name = "runfiles",
2223
testonly = 1,
23-
srcs = ["runfiles.py"],
24+
srcs = [
25+
"runfiles.py",
26+
":_runfiles_constants",
27+
],
28+
)
29+
30+
gen_runfiles_constants(
31+
name = "_runfiles_constants",
2432
)
2533

2634
py_test(

tools/python/runfiles/BUILD.tools

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
load("//tools/python:gen_runfiles_constants.bzl", "gen_runfiles_constants")
12
load("//tools/python:private/defs.bzl", "py_library")
23

34
py_library(
45
name = "runfiles",
5-
srcs = ["runfiles.py"],
6+
srcs = [
7+
"runfiles.py",
8+
":_runfiles_constants",
9+
],
610
visibility = ["//visibility:public"],
711
)
12+
13+
gen_runfiles_constants(
14+
name = "_runfiles_constants",
15+
)

tools/python/runfiles/runfiles.py

+70
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,12 @@
5858
p = subprocess.Popen([r.Rlocation("path/to/binary")], env, ...)
5959
"""
6060

61+
import inspect
6162
import os
6263
import posixpath
64+
import sys
65+
66+
from ._runfiles_constants import MAIN_REPOSITORY_RUNFILES_DIRECTORY
6367

6468

6569
def CreateManifestBased(manifest_path):
@@ -114,6 +118,7 @@ class _Runfiles(object):
114118

115119
def __init__(self, strategy):
116120
self._strategy = strategy
121+
self._python_runfiles_root = _FindPythonRunfilesRoot()
117122

118123
def Rlocation(self, path):
119124
"""Returns the runtime path of a runfile.
@@ -161,6 +166,71 @@ def EnvVars(self):
161166
"""
162167
return self._strategy.EnvVars()
163168

169+
def CurrentRepository(self, frame=1):
170+
"""Returns the canonical name of the caller's Bazel repository.
171+
172+
For example, this function returns '' (the empty string) when called from
173+
the main repository and a string of the form 'rules_python~0.13.0` when
174+
called from code in the repository corresponding to the rules_python Bazel
175+
module.
176+
177+
More information about the difference between canonical repository names and
178+
the `@repo` part of labels is available at:
179+
https://bazel.build/build/bzlmod#repository-names
180+
181+
NOTE: This function inspects the callstack to determine where in the
182+
runfiles the caller is located to determine which repository it came from.
183+
This may fail or produce incorrect results depending on who the caller is,
184+
for example if it is not represented by a Python source file. Use the
185+
`frame` argument to control the stack lookup.
186+
187+
Args:
188+
frame: int; the stack frame to return the repository name for. Defaults to
189+
1, the caller of the CurrentRepository function.
190+
191+
Returns:
192+
The canonical name of the Bazel repository containing the file containing
193+
the frame-th caller of this function
194+
Raises:
195+
ValueError: if the caller cannot be determined or the caller's file path
196+
is not contained in the Python runfiles tree
197+
"""
198+
# pylint:disable=protected-access # for sys._getframe
199+
# pylint:disable=raise-missing-from # we're still supporting Python 2...
200+
try:
201+
caller_path = inspect.getfile(sys._getframe(frame))
202+
except (TypeError, ValueError):
203+
raise ValueError("failed to determine caller's file path")
204+
caller_runfiles_path = os.path.relpath(caller_path,
205+
self._python_runfiles_root)
206+
if caller_runfiles_path.startswith(".." + os.path.sep):
207+
raise ValueError("{} does not lie under the runfiles root {}".format(
208+
caller_path, self._python_runfiles_root))
209+
210+
caller_runfiles_directory = caller_runfiles_path[:caller_runfiles_path
211+
.find(os.path.sep)]
212+
if caller_runfiles_directory == MAIN_REPOSITORY_RUNFILES_DIRECTORY:
213+
# The canonical name of the main repository (also known as the workspace)
214+
# is the empty string.
215+
return ""
216+
# For all other repositories, the name of the runfiles directory is the
217+
# canonical name.
218+
return caller_runfiles_directory
219+
220+
221+
def _FindPythonRunfilesRoot():
222+
"""Finds the root of the Python runfiles tree."""
223+
root = __file__
224+
# Walk up our own runfiles path to the root of the runfiles tree from which
225+
# the current file is being run. This path coincides with what the Bazel
226+
# Python stub sets up as sys.path[0]. Since that entry can be changed at
227+
# runtime, we rederive it here.
228+
for _ in range(
229+
"bazel_tools/tools/python/runfiles/runfiles.py".count("/") +
230+
1):
231+
root = os.path.dirname(root)
232+
return root
233+
164234

165235
class _ManifestBased(object):
166236
"""`Runfiles` strategy that parses a runfiles-manifest to look up runfiles."""

tools/python/runfiles/runfiles_test.py

+4
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ def testPathsFromEnvvars(self):
262262
self.assertEqual(mf, "")
263263
self.assertEqual(dr, "")
264264

265+
def testCurrentRepository(self):
266+
self.assertEqual(
267+
runfiles.Create({"RUNFILES_DIR": "whatever"}).CurrentRepository(), "")
268+
265269
@staticmethod
266270
def IsWindows():
267271
return os.name == "nt"

0 commit comments

Comments
 (0)