Skip to content

Commit daca843

Browse files
authored
feat: generate py_library per file (#1398)
fixes #1150 fixes #1323 you can no longer pre-define the name of the target by creating an empty `py_library` (see 3c84655). I don't think this was being used and it's straightforward to rename the generated per-project or per-package target if you want
1 parent 09109e3 commit daca843

File tree

35 files changed

+346
-22
lines changed

35 files changed

+346
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ A brief description of the categories of changes:
3535
the `py_binary` rule used to build it.
3636
* New Python versions available: `3.8.17`, `3.9.18`, `3.10.13`, `3.11.5` using
3737
https://github.com/indygreg/python-build-standalone/releases/tag/20230826.
38+
* (gazelle) New `# gazelle:python_generation_mode file` directive to support
39+
generating one `py_library` per file.
3840

3941
### Removed
4042

gazelle/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ Python-specific directives are as follows:
189189
| `# gazelle:python_validate_import_statements`| `true` |
190190
| Controls whether the Python import statements should be validated. Can be "true" or "false" | |
191191
| `# gazelle:python_generation_mode`| `package` |
192-
| Controls the target generation mode. Can be "package" or "project" | |
192+
| Controls the target generation mode. Can be "file", "package", or "project" | |
193193
| `# gazelle:python_library_naming_convention`| `$package_name$` |
194-
| Controls the `py_library` naming convention. It interpolates $package_name$ with the Bazel package name. E.g. if the Bazel package name is `foo`, setting this to `$package_name$_my_lib` would result in a generated target named `foo_my_lib`. | |
194+
| Controls the `py_library` naming convention. It interpolates \$package_name\$ with the Bazel package name. E.g. if the Bazel package name is `foo`, setting this to `$package_name$_my_lib` would result in a generated target named `foo_my_lib`. | |
195195
| `# gazelle:python_binary_naming_convention` | `$package_name$_bin` |
196196
| Controls the `py_binary` naming convention. Follows the same interpolation rules as `python_library_naming_convention`. | |
197197
| `# gazelle:python_test_naming_convention` | `$package_name$_test` |
@@ -206,11 +206,15 @@ Python source files are those ending in `.py` but not ending in `_test.py`.
206206
First, we look for the nearest ancestor BUILD file starting from the folder
207207
containing the Python source file.
208208

209-
If there is no `py_library` in this BUILD file, one is created, using the
210-
package name as the target's name. This makes it the default target in the
211-
package.
209+
In package generation mode, if there is no `py_library` in this BUILD file, one
210+
is created using the package name as the target's name. This makes it the
211+
default target in the package. Next, all source files are collected into the
212+
`srcs` of the `py_library`.
212213

213-
Next, all source files are collected into the `srcs` of the `py_library`.
214+
In project generation mode, all source files in subdirectories (that don't have
215+
BUILD files) are also collected.
216+
217+
In file generation mode, each file is given its own target.
214218

215219
Finally, the `import` statements in the source files are parsed, and
216220
dependencies are added to the `deps` attribute.

gazelle/python/configure.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,13 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
137137
switch pythonconfig.GenerationModeType(strings.TrimSpace(d.Value)) {
138138
case pythonconfig.GenerationModePackage:
139139
config.SetCoarseGrainedGeneration(false)
140+
config.SetPerFileGeneration(false)
141+
case pythonconfig.GenerationModeFile:
142+
config.SetCoarseGrainedGeneration(false)
143+
config.SetPerFileGeneration(true)
140144
case pythonconfig.GenerationModeProject:
141145
config.SetCoarseGrainedGeneration(true)
146+
config.SetPerFileGeneration(false)
142147
default:
143148
err := fmt.Errorf("invalid value for directive %q: %s",
144149
pythonconfig.GenerationMode, d.Value)

gazelle/python/generate.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,17 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
153153
if entry.IsDir() {
154154
// If we are visiting a directory, we determine if we should
155155
// halt digging the tree based on a few criterias:
156-
// 1. The directory has a BUILD or BUILD.bazel files. Then
156+
// 1. We are using per-file generation.
157+
// 2. The directory has a BUILD or BUILD.bazel files. Then
157158
// it doesn't matter at all what it has since it's a
158159
// separate Bazel package.
159-
// 2. (only for fine-grained generation) The directory has
160-
// an __init__.py, __main__.py or __test__.py, meaning
161-
// a BUILD file will be generated.
160+
// 3. (only for package generation) The directory has an
161+
// __init__.py, __main__.py or __test__.py, meaning a
162+
// BUILD file will be generated.
163+
if cfg.PerFileGeneration() {
164+
return fs.SkipDir
165+
}
166+
162167
if isBazelPackage(path) {
163168
boundaryPackages[path] = struct{}{}
164169
return nil
@@ -213,15 +218,12 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
213218

214219
collisionErrors := singlylinkedlist.New()
215220

216-
var pyLibrary *rule.Rule
217-
if !pyLibraryFilenames.Empty() {
218-
deps, err := parser.parse(pyLibraryFilenames)
221+
appendPyLibrary := func(srcs *treeset.Set, pyLibraryTargetName string) {
222+
deps, err := parser.parse(srcs)
219223
if err != nil {
220224
log.Fatalf("ERROR: %v\n", err)
221225
}
222226

223-
pyLibraryTargetName := cfg.RenderLibraryName(packageName)
224-
225227
// Check if a target with the same name we are generating already
226228
// exists, and if it is of a different kind from the one we are
227229
// generating. If so, we have to throw an error since Gazelle won't
@@ -239,16 +241,34 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
239241
}
240242
}
241243

242-
pyLibrary = newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames).
244+
pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames).
243245
addVisibility(visibility).
244-
addSrcs(pyLibraryFilenames).
246+
addSrcs(srcs).
245247
addModuleDependencies(deps).
246248
generateImportsAttribute().
247249
build()
248250

249251
result.Gen = append(result.Gen, pyLibrary)
250252
result.Imports = append(result.Imports, pyLibrary.PrivateAttr(config.GazelleImportsKey))
251253
}
254+
if cfg.PerFileGeneration() {
255+
pyLibraryFilenames.Each(func(index int, filename interface{}) {
256+
if filename == pyLibraryEntrypointFilename {
257+
stat, err := os.Stat(filepath.Join(args.Dir, filename.(string)))
258+
if err != nil {
259+
log.Fatalf("ERROR: %v\n", err)
260+
}
261+
if stat.Size() == 0 {
262+
return // ignore empty __init__.py
263+
}
264+
}
265+
srcs := treeset.NewWith(godsutils.StringComparator, filename)
266+
pyLibraryTargetName := strings.TrimSuffix(filepath.Base(filename.(string)), ".py")
267+
appendPyLibrary(srcs, pyLibraryTargetName)
268+
})
269+
} else if !pyLibraryFilenames.Empty() {
270+
appendPyLibrary(pyLibraryFilenames, cfg.RenderLibraryName(packageName))
271+
}
252272

253273
if hasPyBinary {
254274
deps, err := parser.parseSingle(pyBinaryEntrypointFilename)

gazelle/python/kinds.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ var pyKinds = map[string]rule.KindInfo{
4949
},
5050
},
5151
pyLibraryKind: {
52-
MatchAny: true,
52+
MatchAny: false,
53+
MatchAttrs: []string{"srcs"},
5354
NonEmptyAttrs: map[string]bool{
5455
"deps": true,
5556
"srcs": true,

gazelle/python/resolve.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@ func (py *Resolver) Resolve(
151151
for len(moduleParts) > 1 {
152152
// Iterate back through the possible imports until
153153
// a match is found.
154-
// For example, "from foo.bar import baz" where bar is a variable, we should try
155-
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
156-
// in the directory `foo/bar`.
157-
// Or, the import could be variable `bar` in file `foo/bar.py`.
154+
// For example, "from foo.bar import baz" where baz is a module, we should try `foo.bar.baz` first, then
155+
// `foo.bar`, then `foo`.
156+
// In the first case, the import could be file `baz.py` in the directory `foo/bar`.
157+
// Or, the import could be variable `baz` in file `foo/bar.py`.
158158
// The import could also be from a standard module, e.g. `six.moves`, where
159159
// the dependency is actually `six`.
160160
moduleParts = moduleParts[:len(moduleParts)-1]

gazelle/python/testdata/dont_rename_target/BUILD.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ load("@rules_python//python:defs.bzl", "py_library")
22

33
py_library(
44
name = "my_custom_target",
5+
srcs = ["__init__.py"],
56
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
load("@rules_python//python:defs.bzl", "py_library")
2+
3+
# gazelle:python_generation_mode file
4+
5+
# This target should be kept unmodified by Gazelle.
6+
py_library(
7+
name = "custom",
8+
srcs = ["bar.py"],
9+
visibility = ["//visibility:private"],
10+
tags = ["cant_touch_this"],
11+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load("@rules_python//python:defs.bzl", "py_library")
2+
3+
# gazelle:python_generation_mode file
4+
5+
# This target should be kept unmodified by Gazelle.
6+
py_library(
7+
name = "custom",
8+
srcs = ["bar.py"],
9+
tags = ["cant_touch_this"],
10+
visibility = ["//visibility:private"],
11+
)
12+
13+
py_library(
14+
name = "baz",
15+
srcs = ["baz.py"],
16+
visibility = ["//:__subpackages__"],
17+
)
18+
19+
py_library(
20+
name = "foo",
21+
srcs = ["foo.py"],
22+
visibility = ["//:__subpackages__"],
23+
deps = [":custom"],
24+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Per-file generation
2+
3+
This test case generates one `py_library` per file.
4+
5+
`__init__.py` is left empty so no target is generated for it.

0 commit comments

Comments
 (0)