From 3015eb717bb2d9e4595c619231b9c8a3f5267d1a Mon Sep 17 00:00:00 2001 From: Nikolaus Wittenstein Date: Thu, 16 Nov 2023 15:48:29 -0800 Subject: [PATCH] feat(gazelle): create py_binary targets for `if __name__ == "__main__"` Previously a `py_binary` target was only created if a file named `__main__.py` existed in the package. Now we look at each file that would otherwise be added to a `py_library` target and see if it contains a check for `if __name__ == "__main__"`. If such a check is found in the file, then we create a `py_binary` target for it instead. No existing tests fail with this change, though I think in real code it's possible this could change target generation. New tests have been added to check the new functionality. --- CHANGELOG.md | 3 ++ gazelle/python/generate.go | 47 ++++++++++++++----- .../python/testdata/binary_targets/BUILD.in | 0 .../python/testdata/binary_targets/BUILD.out | 35 ++++++++++++++ .../python/testdata/binary_targets/README.md | 4 ++ .../python/testdata/binary_targets/WORKSPACE | 1 + gazelle/python/testdata/binary_targets/bar.py | 0 .../testdata/binary_targets/bar_binary.py | 4 ++ .../testdata/binary_targets/bar_test.py | 1 + gazelle/python/testdata/binary_targets/baz.py | 0 .../testdata/binary_targets/name_main_test.py | 3 ++ .../binary_targets/single_quote_main.py | 4 ++ .../python/testdata/binary_targets/test.yaml | 1 + 13 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 gazelle/python/testdata/binary_targets/BUILD.in create mode 100644 gazelle/python/testdata/binary_targets/BUILD.out create mode 100644 gazelle/python/testdata/binary_targets/README.md create mode 100644 gazelle/python/testdata/binary_targets/WORKSPACE create mode 100644 gazelle/python/testdata/binary_targets/bar.py create mode 100644 gazelle/python/testdata/binary_targets/bar_binary.py create mode 100644 gazelle/python/testdata/binary_targets/bar_test.py create mode 100644 gazelle/python/testdata/binary_targets/baz.py create mode 100644 gazelle/python/testdata/binary_targets/name_main_test.py create mode 100644 gazelle/python/testdata/binary_targets/single_quote_main.py create mode 100644 gazelle/python/testdata/binary_targets/test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ac2a3f0c3..817761dd47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ A brief description of the categories of changes: `__test__.py` exists in the same package. Previously in these cases there would only be one test target made. +* (gazelle) If a non-test Python file contains `if __name__ == "__main__":`, + then a `py_binary` target is made for it instead of a `py_library` target. + Breaking changes: * (pip) `pip_install` repository rule in this release has been disabled and diff --git a/gazelle/python/generate.go b/gazelle/python/generate.go index 25fb194370..2fc04fa4c1 100644 --- a/gazelle/python/generate.go +++ b/gazelle/python/generate.go @@ -20,6 +20,7 @@ import ( "log" "os" "path/filepath" + "regexp" "strings" "github.com/bazelbuild/bazel-gazelle/config" @@ -85,14 +86,11 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes packageName := filepath.Base(args.Dir) + pyBinaryFilenames := treeset.NewWith(godsutils.StringComparator) pyLibraryFilenames := treeset.NewWith(godsutils.StringComparator) pyTestFilenames := treeset.NewWith(godsutils.StringComparator) pyFileNames := treeset.NewWith(godsutils.StringComparator) - // hasPyBinary controls whether a py_binary target should be generated for - // this package or not. - hasPyBinary := false - // hasPyTestEntryPointFile and hasPyTestEntryPointTarget control whether a py_test target should // be generated for this package or not. hasPyTestEntryPointFile := false @@ -106,14 +104,14 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes ext := filepath.Ext(f) if ext == ".py" { pyFileNames.Add(f) - if !hasPyBinary && f == pyBinaryEntrypointFilename { - hasPyBinary = true - } else if !hasPyTestEntryPointFile && f == pyTestEntrypointFilename { + if !hasPyTestEntryPointFile && f == pyTestEntrypointFilename { hasPyTestEntryPointFile = true } else if f == conftestFilename { hasConftestFile = true } else if strings.HasSuffix(f, "_test.py") || strings.HasPrefix(f, "test_") { pyTestFilenames.Add(f) + } else if f == pyBinaryEntrypointFilename || hasNameEqualsMain(filepath.Join(args.Config.RepoRoot, args.Rel, f)) { + pyBinaryFilenames.Add(f) } else { pyLibraryFilenames.Add(f) } @@ -270,13 +268,19 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes appendPyLibrary(pyLibraryFilenames, cfg.RenderLibraryName(packageName)) } - if hasPyBinary { - deps, err := parser.parseSingle(pyBinaryEntrypointFilename) + pyBinaryFilenames.Each(func(index int, filename interface{}) { + entrypointFilename := filename.(string) + deps, err := parser.parseSingle(entrypointFilename) if err != nil { log.Fatalf("ERROR: %v\n", err) } - pyBinaryTargetName := cfg.RenderBinaryName(packageName) + var pyBinaryTargetName string + if entrypointFilename == pyBinaryEntrypointFilename { + pyBinaryTargetName = cfg.RenderBinaryName(packageName) + } else { + pyBinaryTargetName = strings.TrimSuffix(filepath.Base(filename.(string)), ".py") + } // Check if a target with the same name we are generating already // exists, and if it is of a different kind from the one we are @@ -296,17 +300,20 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } pyBinaryTarget := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames). - setMain(pyBinaryEntrypointFilename). addVisibility(visibility). - addSrc(pyBinaryEntrypointFilename). + addSrc(entrypointFilename). addModuleDependencies(deps). generateImportsAttribute() + if entrypointFilename == pyBinaryEntrypointFilename { + pyBinaryTarget.setMain(pyBinaryEntrypointFilename) + } + pyBinary := pyBinaryTarget.build() result.Gen = append(result.Gen, pyBinary) result.Imports = append(result.Imports, pyBinary.PrivateAttr(config.GazelleImportsKey)) - } + }) var conftest *rule.Rule if hasConftestFile { @@ -463,6 +470,20 @@ func hasEntrypointFile(dir string) bool { return false } +// hasNameEqualsMain determines if the file contains 'if __name__ == "__main__"'. +func hasNameEqualsMain(path string) bool { + searchString := `if __name__ == ['"]__main__['"]:` + bytesContents, err := os.ReadFile(path) + if err != nil { + return false + } + match, err := regexp.Match(searchString, bytesContents) + if err == nil { + return match + } + return false +} + // isEntrypointFile returns whether the given path is an entrypoint file. The // given path can be absolute or relative. func isEntrypointFile(path string) bool { diff --git a/gazelle/python/testdata/binary_targets/BUILD.in b/gazelle/python/testdata/binary_targets/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/binary_targets/BUILD.out b/gazelle/python/testdata/binary_targets/BUILD.out new file mode 100644 index 0000000000..09a69799ba --- /dev/null +++ b/gazelle/python/testdata/binary_targets/BUILD.out @@ -0,0 +1,35 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") + +py_library( + name = "binary_targets", + srcs = [ + "bar.py", + "baz.py", + ], + visibility = ["//:__subpackages__"], +) + +py_binary( + name = "bar_binary", + srcs = ["bar_binary.py"], + visibility = ["//:__subpackages__"], + deps = [":binary_targets"], +) + +py_binary( + name = "single_quote_main", + srcs = ["single_quote_main.py"], + visibility = ["//:__subpackages__"], + deps = [":bar_test"], +) + +py_test( + name = "bar_test", + srcs = ["bar_test.py"], + deps = [":bar_binary"], +) + +py_test( + name = "name_main_test", + srcs = ["name_main_test.py"], +) diff --git a/gazelle/python/testdata/binary_targets/README.md b/gazelle/python/testdata/binary_targets/README.md new file mode 100644 index 0000000000..76ac4df7b1 --- /dev/null +++ b/gazelle/python/testdata/binary_targets/README.md @@ -0,0 +1,4 @@ +# Binary targets + +This test case generates `py_binary` targets for files containing +`if __name__ == "__main__"`. diff --git a/gazelle/python/testdata/binary_targets/WORKSPACE b/gazelle/python/testdata/binary_targets/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/binary_targets/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/binary_targets/bar.py b/gazelle/python/testdata/binary_targets/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/binary_targets/bar_binary.py b/gazelle/python/testdata/binary_targets/bar_binary.py new file mode 100644 index 0000000000..1623c29a64 --- /dev/null +++ b/gazelle/python/testdata/binary_targets/bar_binary.py @@ -0,0 +1,4 @@ +import bar + +if __name__ == "__main__": + pass diff --git a/gazelle/python/testdata/binary_targets/bar_test.py b/gazelle/python/testdata/binary_targets/bar_test.py new file mode 100644 index 0000000000..11de95a1b3 --- /dev/null +++ b/gazelle/python/testdata/binary_targets/bar_test.py @@ -0,0 +1 @@ +import bar_binary diff --git a/gazelle/python/testdata/binary_targets/baz.py b/gazelle/python/testdata/binary_targets/baz.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/binary_targets/name_main_test.py b/gazelle/python/testdata/binary_targets/name_main_test.py new file mode 100644 index 0000000000..b1c81fcc03 --- /dev/null +++ b/gazelle/python/testdata/binary_targets/name_main_test.py @@ -0,0 +1,3 @@ +# This should make a py_test target because of the filename +if __name__ == "__main__": + pass diff --git a/gazelle/python/testdata/binary_targets/single_quote_main.py b/gazelle/python/testdata/binary_targets/single_quote_main.py new file mode 100644 index 0000000000..367012ae92 --- /dev/null +++ b/gazelle/python/testdata/binary_targets/single_quote_main.py @@ -0,0 +1,4 @@ +import bar_test + +if __name__ == '__main__': + pass diff --git a/gazelle/python/testdata/binary_targets/test.yaml b/gazelle/python/testdata/binary_targets/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/binary_targets/test.yaml @@ -0,0 +1 @@ +---