Skip to content

Commit 3d68f6c

Browse files
authored
fix: Fix per-file config interaction with one py_binary per main (#1664)
[This previous PR](#1584) added the ability to make a `py_binary` target per file if `if __name__ == "__main__"` tokens were found in the file. This works great in the default case, but when `python_generation_mode` is set to `file`, the plugin now attempts to make both a `py_binary` and a `py_library` target for each main file, which results in an error. This PR modifies the behavior to work properly with per-file target generation, and adds tests for this case.
1 parent 231d3f6 commit 3d68f6c

File tree

13 files changed

+131
-14
lines changed

13 files changed

+131
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ A brief description of the categories of changes:
5757
from the target are not supported yet.
5858
([#1612](https://github.com/bazelbuild/rules_python/issues/1612))
5959

60+
* (gazelle) When `python_generation_mode` is set to `file`, create one `py_binary`
61+
target for each file with `if __name__ == "__main__"` instead of just one
62+
`py_binary` for the whole module.
63+
6064
### Fixed
6165

6266
* (gazelle) The gazelle plugin helper was not working with Python toolchains 3.11

gazelle/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,20 @@ When no such entry point exists, Gazelle will look for a line like this in the t
250250
if __name == "__main__":
251251
```
252252

253-
Gazelle will create `py_binary` target will be created for every module with such line, with the target name
254-
being the same as module name.
253+
Gazelle will create a `py_binary` target for every module with such a line, with
254+
the target name the same as the module name.
255+
256+
If `python_generation_mode` is set to `file`, then instead of one `py_binary`
257+
target per module, Gazelle will create one `py_binary` target for each file with
258+
such a line, and the name of the target will match the name of the script.
259+
260+
Note that it's possible for another script to depend on a `py_binary` target and
261+
import from the `py_binary`'s scripts. This can have possible negative effects on
262+
Bazel analysis time and runfiles size compared to depending on a `py_library`
263+
target. The simplest way to avoid these negative effects is to extract library
264+
code into a separate script without a `main` line. Gazelle will then create a
265+
`py_library` target for that library code, and other scripts can depend on that
266+
`py_library` target.
255267

256268
## Developer Notes
257269

gazelle/python/generate.go

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -225,23 +225,17 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
225225
log.Fatalf("ERROR: %v\n", err)
226226
}
227227

228-
// Check if a target with the same name we are generating already
229-
// exists, and if it is of a different kind from the one we are
230-
// generating. If so, we have to throw an error since Gazelle won't
231-
// generate it correctly.
232-
if err := ensureNoCollision(args.File, pyLibraryTargetName, actualPyLibraryKind); err != nil {
233-
fqTarget := label.New("", args.Rel, pyLibraryTargetName)
234-
err := fmt.Errorf("failed to generate target %q of kind %q: %w. "+
235-
"Use the '# gazelle:%s' directive to change the naming convention.",
236-
fqTarget.String(), actualPyLibraryKind, err, pythonconfig.LibraryNamingConvention)
237-
collisionErrors.Add(err)
238-
}
239-
240228
if !hasPyBinaryEntryPointFile {
241229
// Creating one py_binary target per main module when __main__.py doesn't exist.
242230
mainFileNames := make([]string, 0, len(mainModules))
243231
for name := range mainModules {
244232
mainFileNames = append(mainFileNames, name)
233+
234+
// Remove the file from srcs if we're doing per-file library generation so
235+
// that we don't also generate a py_library target for it.
236+
if cfg.PerFileGeneration() {
237+
srcs.Remove(name)
238+
}
245239
}
246240
sort.Strings(mainFileNames)
247241
for _, filename := range mainFileNames {
@@ -262,6 +256,23 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
262256
}
263257
}
264258

259+
// If we're doing per-file generation, srcs could be empty at this point, meaning we shouldn't make a py_library.
260+
if srcs.Empty() {
261+
return
262+
}
263+
264+
// Check if a target with the same name we are generating already
265+
// exists, and if it is of a different kind from the one we are
266+
// generating. If so, we have to throw an error since Gazelle won't
267+
// generate it correctly.
268+
if err := ensureNoCollision(args.File, pyLibraryTargetName, actualPyLibraryKind); err != nil {
269+
fqTarget := label.New("", args.Rel, pyLibraryTargetName)
270+
err := fmt.Errorf("failed to generate target %q of kind %q: %w. "+
271+
"Use the '# gazelle:%s' directive to change the naming convention.",
272+
fqTarget.String(), actualPyLibraryKind, err, pythonconfig.LibraryNamingConvention)
273+
collisionErrors.Add(err)
274+
}
275+
265276
pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames).
266277
addVisibility(visibility).
267278
addSrcs(srcs).
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# gazelle:python_generation_mode file
2+
3+
# gazelle:resolve py numpy @pip//:numpy
4+
# gazelle:resolve py pandas @pip//:pandas
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
load("@rules_python//python:defs.bzl", "py_binary", "py_library")
2+
3+
# gazelle:python_generation_mode file
4+
5+
# gazelle:resolve py numpy @pip//:numpy
6+
# gazelle:resolve py pandas @pip//:pandas
7+
8+
py_library(
9+
name = "lib",
10+
srcs = ["lib.py"],
11+
visibility = ["//:__subpackages__"],
12+
deps = [
13+
"@pip//:numpy",
14+
"@pip//:pandas",
15+
],
16+
)
17+
18+
py_library(
19+
name = "lib2",
20+
srcs = ["lib2.py"],
21+
visibility = ["//:__subpackages__"],
22+
deps = [
23+
":lib",
24+
":lib_and_main",
25+
],
26+
)
27+
28+
py_binary(
29+
name = "lib_and_main",
30+
srcs = ["lib_and_main.py"],
31+
visibility = ["//:__subpackages__"],
32+
)
33+
34+
py_binary(
35+
name = "main",
36+
srcs = ["main.py"],
37+
visibility = ["//:__subpackages__"],
38+
deps = ["@pip//:pandas"],
39+
)
40+
41+
py_binary(
42+
name = "main2",
43+
srcs = ["main2.py"],
44+
visibility = ["//:__subpackages__"],
45+
deps = [":lib2"],
46+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Binary without entrypoint
2+
3+
This test case asserts that when there is no __main__.py, a py_binary is generated per file main module, and that this
4+
py_binary is instead of (not in addition to) any py_library target.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This is a Bazel workspace for the Gazelle test data.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import numpy
2+
import pandas
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import lib
2+
import lib_and_main
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def library_func():
2+
print("library_func")
3+
4+
5+
if __name__ == "__main__":
6+
library_func()

0 commit comments

Comments
 (0)