diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000000..6d9c8b9e59 --- /dev/null +++ b/TODO.md @@ -0,0 +1,110 @@ +# Review follow-ups: preserving existing Python source targets + +Status of review findings for the `py_library` / `py_test` preservation +feature. Line references are against `gazelle/python/generate.go` as of the +last commit on `martani/preserve-existing-targets-2`. + +Background on why any of this is destructive: putting a hand-written target into +`result.Gen` moves it from *unmanaged* to *managed*. `srcs` is in +`MergeableAttrs` and `deps` / `pyi_deps` / `pyi_srcs` are in `ResolveAttrs` +(`gazelle/python/kinds.go:57-74` for `py_library`, `:81-98` for `py_test`), and +`rule.MergeRules` drops any value in the existing list that is not in the +generated set unless it carries a `# keep` comment. Attributes present in the +generated rule but absent from the existing one are copied in unconditionally, +regardless of mergeability. + +## Explicitly decided, do not reopen + +- **Pruning hand-written `deps`.** Preserved targets participate in the resolve + phase, so a `deps` entry not derivable from an `import` statement (e.g. an + `importlib` plugin, a `//third_party/...` runtime dep) is deleted. This is + intended behavior and is covered by the release note and user documentation. + +## Fixed + +- **`visibility` injected into previously-unmanaged targets** — preserved + targets no longer get `addVisibility`, since `MergeRules` copies a + non-mergeable attribute the existing rule does not set and offers no `# keep` + for it. +- **Entrypoints and `conftest.py` in a preserved rule** — a rule listing + `__main__.py`, `__test__.py` or `conftest.py` is no longer adopted. +- **The claiming threshold** is now taken from the declared src count rather + than the pruned set, so deleting an unrelated file cannot flip a target + between claiming and not claiming. +- **Name collisions with generated targets** — the names Gazelle will generate + are collected before claiming and existing rules with one of those names are + not adopted. This covers the per-file names, the package library/test names, + the `py_binary` name, `conftest`, and a `py_binary` extracted from a + preserved target's own main module. Without it the two rules merged and + orphaned the sources of whichever lost. +- **A preserved target whose sources are all main modules** is now left as + written instead of emptied and deleted. +- **Stale `deps` inherited from an extracted main module** — dependencies are + recomputed after main modules are removed from `srcs`. +- **User documentation and release notes** now describe eligibility, claiming, + managed attributes, `# keep`, exclusions, and the possible dependency cycle. + +## Correctness + +### 1. Preserved and generated targets can form a dependency cycle + +Once a preserved target claims its sources, imports can point both ways: +`custom`'s `bar.py` imports `foo` so `custom` gets `deps = [":pkg"]`, and if +`pkg`'s `foo.py` imports `bar` then `pkg` gets `deps = [":custom"]`. Bazel +rejects the cycle. Before the feature this was a self-import inside one target +and produced no edge. + +Not reachable in the current fixtures, which are arranged so imports only flow +from the preserved target to the generated one. Detecting it properly needs the +resolve phase, which runs after generation, so this is likely a documentation +item rather than something to block on. It is also already reachable in file +mode without the feature. + +## Test gaps + +Ranked. None of these exist today. + +1. `map_kind` / `alias_kind` with a target name that is not the generated + name — the preservation path is entirely untested for renamed kinds. The + three existing cases (`respect_alias_kind`, `respect_kind_mapping`, + `respect_alias_and_map_kind`) all use names the filters exclude. +2. A preserved `py_test` in file mode. +3. `srcs = glob([...])`: `AttrStrings` returns nothing, so the rule is skipped + and its files are also swept into generated targets. Probably the right + conservative behavior, but it is silent and unasserted — and `glob` is the + commonest hand-written form. +4. Label and non-`.py` srcs (`srcs = ["a.py", ":generated.py"]`, + `srcs = ["a.py", "schema.json"]`), which disqualify the whole rule. +5. Project mode with a preserved target listing subdirectory sources, and any + case in a subpackage — all preservation fixtures sit at the workspace root + (`args.Rel == ""`), so `imports` rendering is never exercised. +6. Custom `python_library_naming_convention` / + `python_test_naming_convention`, which feed the "is this the generated + target?" filters via `RenderLibraryName` / `RenderTestName`. +7. A test-pattern file inside a preserved library's srcs — `knownPySrcs` + merges library and test filenames, so a preserved `py_library` can claim + `foo_test.py` and suppress the generated `py_test`. + +## Diagnostics + +The preservation code emits no log output at all. Every branch that declines to +preserve a rule (no srcs, a label or non-`.py` src, an entrypoint or +`conftest.py` src, no managed src, a name Gazelle generates) leaves Gazelle +generating a competing target over the same sources, which is exactly what the +user needs to know. Consider one log line per declined rule. + +Separately, the `log.Fatalf` calls in `appendPyLibrary` and +`newPyTestTargetBuilder` name no target, and the same file is now parsed twice +(once for the preserved target, once for the generated one), so the message does +not identify which target failed. + +## Cosmetic + +- `testdata/per_file_respect_existing_multiple_srcs/BUILD.in:11` and + `testdata/project_generation_mode_respect_existing_multiple_srcs/BUILD.in:19` + indent `tags` with a literal tab; neighbouring lines use four spaces. +- `testdata/project_generation_mode_respect_existing_multiple_srcs/BUILD.in` + has a stray double blank line, and its `load` statement omits `py_test` + although the file uses it. +- The testdata READMEs added by the first preservation commit use Title Case + headings; the dominant convention across the other ~80 cases is sentence case. diff --git a/gazelle/docs/annotations.md b/gazelle/docs/annotations.md index b3f06b9991..05ac4a6daa 100644 --- a/gazelle/docs/annotations.md +++ b/gazelle/docs/annotations.md @@ -198,3 +198,13 @@ py_test( ``` See {gh-issue}`3076` for more information. + +When a `py_test` has multiple source files, the annotation may be omitted from +some files. If multiple source files set the annotation, they must all set it to +the same value; Gazelle reports an error if the values conflict. + +:::{versionchanged} VERSION_NEXT_PATCH +For multi-source `py_test` targets, annotations in different source files must +agree. An annotation in one source file is no longer overwritten by an unset +value in another source file. +::: diff --git a/gazelle/docs/installation_and_usage.md b/gazelle/docs/installation_and_usage.md index 16bccded02..499895b86d 100644 --- a/gazelle/docs/installation_and_usage.md +++ b/gazelle/docs/installation_and_usage.md @@ -161,8 +161,8 @@ you edit Python code, and it should update your `BUILD` files correctly. ### Libraries Python source files are those ending in `.py` that are not matched as a test -file via the {term}`# gazelle:python_test_file_pattern value` directive. By default, -python source files are all `*.py` files except for `*_test.py` and +file via the {term}`# gazelle:python_test_file_pattern value` directive. By +default, python source files are all `*.py` files except for `*_test.py` and `test_*.py`. First, we look for the nearest ancestor `BUILD(.bazel)` file starting from @@ -170,8 +170,9 @@ the folder containing the Python source file. + In `package` generation mode, if there is no {bzl:obj}`py_library` in this `BUILD(.bazel)` file, one is created using the package name as the target's - name. This makes it the default target in the package. Next, all source - files are collected into the `srcs` of the {bzl:obj}`py_library`. + name. This makes it the default target in the package. Next, source files not + claimed by another target are collected into the `srcs` of the + {bzl:obj}`py_library`. + In `project` generation mode, all source files in subdirectories (that don't have `BUILD(.bazel)` files) are also collected. + In `file` generation mode, each python source file is given its own target. @@ -205,6 +206,45 @@ py_test( You can control the naming convention for test targets using the {term}`# gazelle:python_test_naming_convention value` directive. +### Existing source targets + +Gazelle regenerates eligible hand-written {bzl:obj}`py_library` and +{bzl:obj}`py_test` targets in place. A target is eligible when its `srcs` is a +non-empty list of relative `.py` paths and at least one of those paths is a +source Gazelle manages. Targets are excluded from this behavior when: + +- In file generation mode, their name is one Gazelle will generate for a + source file in the package. In package generation mode, their name is the + generated package library or test target unless the package uses a split + layout: a package library alongside per-file libraries with disjoint + sources. +- Their `srcs` attribute uses `glob()`, a label, or a non-`.py` file. +- Their `srcs` contains `__main__.py`, `__test__.py`, or `conftest.py`, which + Gazelle handles with dedicated targets. + +For an eligible target, Gazelle updates `srcs` and the dependency attributes +`deps`, `pyi_deps`, and `pyi_srcs`. It may add `imports` when required by the +configured Python root. Other attributes, including `visibility` and `tags`, +remain unchanged. A `# keep` comment on a value prevents Gazelle from removing +that value. A `# keep` comment above the rule prevents Gazelle from changing the +rule at all. + +An eligible {bzl:obj}`py_test` always claims its sources, which keeps those +sources out of generated test and library targets. A {bzl:obj}`py_library` with +more than one declared source also claims its sources. A library with one +declared source continues to share it with the generated library, except in +`project` generation mode, where it claims the source. + +After sources are divided between preserved and generated targets, imports can +produce dependencies in both directions and therefore a Bazel dependency cycle. +If this happens, reorganize the sources or use `# keep` above the existing rule +to opt it out of preservation. + +:::{versionchanged} VERSION_NEXT_FEATURE +Eligible existing {bzl:obj}`py_library` and {bzl:obj}`py_test` targets are now +regenerated in place. +::: + ### Binaries diff --git a/gazelle/python/BUILD.bazel b/gazelle/python/BUILD.bazel index 1ffa2890e1..03d0794fd2 100644 --- a/gazelle/python/BUILD.bazel +++ b/gazelle/python/BUILD.bazel @@ -120,10 +120,20 @@ go_test( name = "default_test", srcs = [ "file_parser_test.go", + "generate_test.go", + "parser_test.go", + "resolve_test.go", "std_modules_test.go", ], embed = [":python"], deps = [ + "@bazel_gazelle//config:go_default_library", + "@bazel_gazelle//language:go_default_library", + "@bazel_gazelle//resolve:go_default_library", + "@bazel_gazelle//rule:go_default_library", + "@com_github_emirpasic_gods//sets/treeset:go_default_library", + "@com_github_emirpasic_gods//utils:go_default_library", "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", ], ) diff --git a/gazelle/python/generate.go b/gazelle/python/generate.go index 90e06a1546..79e41f1dd5 100644 --- a/gazelle/python/generate.go +++ b/gazelle/python/generate.go @@ -48,6 +48,19 @@ var ( buildFilenames = []string{"BUILD", "BUILD.bazel"} ) +// existingPythonSourceRule is a hand-written rule that Gazelle regenerates in +// place instead of replacing. +type existingPythonSourceRule struct { + name string + // srcs are the rule's srcs with the entries that no longer exist pruned. + srcs *treeset.Set + // declaredSrcCount is the number of srcs the rule lists in the BUILD file, + // before pruning. Decisions about how a rule is treated are made on this + // count so that they reflect only what the user wrote: deleting an unrelated + // file must not change how Gazelle handles the rule. + declaredSrcCount int +} + // Returns the mapped kind, or kind if no mapping is configured with the map_kind directive. func getMappedKind(c *config.Config, kind string) string { if mapped, ok := c.KindMap[kind]; ok { @@ -74,6 +87,331 @@ func matchesAnyGlob(s string, globs []string) bool { return false } +// isTargetSrc reports whether src is a label rather than a file path. +func isTargetSrc(src string) bool { + return strings.HasPrefix(src, "@") || strings.HasPrefix(src, "//") || strings.HasPrefix(src, ":") +} + +// collectExistingPythonSourceRules returns the rules of the canonical kind +// `kind` that Gazelle should regenerate in place rather than replace. knownSrcs +// holds the source files Gazelle would itself put in a generated target's srcs. +// +// A rule is only adopted if at least one of its srcs is in knownSrcs; a rule +// built entirely from sources Gazelle was told to leave alone is left alone too. +// Once adopted, srcs that exist but are absent from knownSrcs are still kept: +// python_ignore_files, gazelle:exclude and subdirectory sources are hidden from +// generation, which must not cause Gazelle to delete them from a hand-written +// target. Only srcs that no longer exist are pruned. +// +// A rule listing an entrypoint or conftest.py is never adopted: those sources +// have dedicated targets that Gazelle always generates, so adopting the rule +// would leave two targets owning the same file. +func collectExistingPythonSourceRules(args language.GenerateArgs, kind string, knownSrcs map[string]struct{}) []existingPythonSourceRule { + if args.File == nil { + return nil + } + + genFiles := make(map[string]struct{}, len(args.GenFiles)) + for _, f := range args.GenFiles { + genFiles[f] = struct{}{} + } + srcExists := func(src string) bool { + if _, ok := genFiles[src]; ok { + return true + } + _, err := os.Stat(filepath.Join(args.Dir, src)) + return err == nil + } + + var sourceRules []existingPythonSourceRule + for _, existingRule := range args.File.Rules { + if !kindMatches(args.Config, existingRule, kind) { + continue + } + + srcs := existingRule.AttrStrings("srcs") + if len(srcs) == 0 { + continue + } + + validSrcs := treeset.NewWith(godsutils.StringComparator) + skip := false + hasKnownSrc := false + for _, src := range srcs { + if isTargetSrc(src) || filepath.Ext(src) != ".py" { + skip = true + break + } + if src == pyBinaryEntrypointFilename || + src == pyTestEntrypointFilename || + src == conftestFilename { + skip = true + break + } + if _, ok := knownSrcs[src]; ok { + hasKnownSrc = true + validSrcs.Add(src) + } else if srcExists(src) { + validSrcs.Add(src) + } + } + if skip { + continue + } + if !hasKnownSrc { + continue + } + + sourceRules = append(sourceRules, existingPythonSourceRule{ + name: existingRule.Name(), + srcs: validSrcs, + declaredSrcCount: len(srcs), + }) + } + return sourceRules +} + +// addSetValuesToMap copies every value in srcs into dst. +func addSetValuesToMap(srcs *treeset.Set, dst map[string]struct{}) { + it := srcs.Iterator() + for it.Next() { + dst[it.Value().(string)] = struct{}{} + } +} + +// removeClaimedSrcs removes sources owned by rules from the generated source +// sets. A preserved rule claims a source when it becomes its sole generated +// owner rather than sharing it with another target Gazelle generates. +func removeClaimedSrcs(rules []existingPythonSourceRule, srcSets ...*treeset.Set) { + for _, sourceRule := range rules { + it := sourceRule.srcs.Iterator() + for it.Next() { + src := it.Value().(string) + for _, srcSet := range srcSets { + srcSet.Remove(src) + } + } + } +} + +// filterExistingPythonSourceRules returns the rules accepted by shouldKeep. +func filterExistingPythonSourceRules( + rules []existingPythonSourceRule, + shouldKeep func(existingPythonSourceRule) bool, +) []existingPythonSourceRule { + filtered := make([]existingPythonSourceRule, 0, len(rules)) + for _, sourceRule := range rules { + if shouldKeep(sourceRule) { + filtered = append(filtered, sourceRule) + } + } + return filtered +} + +// existingRulesShareSrcs reports whether two preserved rules list the same +// source file. +func existingRulesShareSrcs(a, b existingPythonSourceRule) bool { + it := a.srcs.Iterator() + for it.Next() { + if b.srcs.Contains(it.Value()) { + return true + } + } + return false +} + +// hasSplitPackageLibraryLayout reports whether the package already defines the +// generated package library name alongside other preserved libraries whose +// sources are disjoint from it. This is the layout where per-file libraries own +// individual modules and the package library owns the remainder. +func hasSplitPackageLibraryLayout(packageLibraryName string, rules []existingPythonSourceRule) bool { + var packageLibrary *existingPythonSourceRule + for i := range rules { + if rules[i].name == packageLibraryName { + packageLibrary = &rules[i] + break + } + } + if packageLibrary == nil || len(rules) < 2 { + return false + } + for _, other := range rules { + if other.name == packageLibraryName { + continue + } + if existingRulesShareSrcs(*packageLibrary, other) { + return false + } + } + return true +} + +// hasExplicitSourceOwnershipLayout reports whether preserved non-package +// libraries collectively own every Gazelle-managed library source without +// overlapping each other and without using the generated package library name. +// When true, Gazelle must not emit a package-level library and every preserved +// library must claim its sources, regardless of per-target source counts. +func hasExplicitSourceOwnershipLayout( + packageLibraryName string, + rules []existingPythonSourceRule, + libraryFilenames *treeset.Set, +) bool { + if libraryFilenames == nil || libraryFilenames.Empty() || len(rules) == 0 { + return false + } + for _, sourceRule := range rules { + if sourceRule.name == packageLibraryName { + return false + } + } + for i := range rules { + for j := i + 1; j < len(rules); j++ { + if existingRulesShareSrcs(rules[i], rules[j]) { + return false + } + } + } + covered := make(map[string]struct{}) + for _, sourceRule := range rules { + it := sourceRule.srcs.Iterator() + for it.Next() { + covered[it.Value().(string)] = struct{}{} + } + } + it := libraryFilenames.Iterator() + for it.Next() { + if _, ok := covered[it.Value().(string)]; !ok { + return false + } + } + return true +} + +// adoptExcludedInitOnlyPackageLibraryForSplitLayout appends the hand-written +// package library when it lists only an excluded __init__.py. That target is +// otherwise not adopted because none of its srcs are Gazelle-managed, but it +// is still the package aggregate in a split layout and must be preserved so +// Gazelle does not emit a competing package-level library. +func adoptExcludedInitOnlyPackageLibraryForSplitLayout( + args language.GenerateArgs, + kind string, + packageLibraryName string, + knownSrcs map[string]struct{}, + rules []existingPythonSourceRule, +) []existingPythonSourceRule { + if args.File == nil || len(rules) == 0 { + return rules + } + for _, sourceRule := range rules { + if sourceRule.name == packageLibraryName { + return rules + } + } + + genFiles := make(map[string]struct{}, len(args.GenFiles)) + for _, f := range args.GenFiles { + genFiles[f] = struct{}{} + } + srcExists := func(src string) bool { + if _, ok := genFiles[src]; ok { + return true + } + _, err := os.Stat(filepath.Join(args.Dir, src)) + return err == nil + } + + for _, existingRule := range args.File.Rules { + if existingRule.Name() != packageLibraryName || !kindMatches(args.Config, existingRule, kind) { + continue + } + srcs := existingRule.AttrStrings("srcs") + if len(srcs) != 1 || srcs[0] != pyLibraryEntrypointFilename { + return rules + } + if _, ok := knownSrcs[pyLibraryEntrypointFilename]; ok { + return rules + } + if !srcExists(pyLibraryEntrypointFilename) { + return rules + } + + validSrcs := treeset.NewWith(godsutils.StringComparator, pyLibraryEntrypointFilename) + candidate := existingPythonSourceRule{ + name: packageLibraryName, + srcs: validSrcs, + declaredSrcCount: 1, + } + for _, other := range rules { + if existingRulesShareSrcs(candidate, other) { + return rules + } + } + return append(rules, candidate) + } + return rules +} + +// adoptEmptyAggregatePackageLibraryForSplitLayout appends the hand-written +// package library when it omits srcs entirely and is marked with "# keep". +// Without "# keep", such targets are stale deps-only aggregates and are removed +// instead. Only applies when every other adopted library is a single-module +// target. +func adoptEmptyAggregatePackageLibraryForSplitLayout( + args language.GenerateArgs, + kind string, + packageLibraryName string, + rules []existingPythonSourceRule, +) []existingPythonSourceRule { + if args.File == nil || len(rules) == 0 { + return rules + } + for _, sourceRule := range rules { + if sourceRule.name == packageLibraryName { + return rules + } + } + for _, other := range rules { + if other.declaredSrcCount != 1 { + return rules + } + } + + for _, existingRule := range args.File.Rules { + if existingRule.Name() != packageLibraryName || !kindMatches(args.Config, existingRule, kind) { + continue + } + if len(existingRule.AttrStrings("srcs")) != 0 { + return rules + } + if !existingRule.ShouldKeep() { + return rules + } + + candidate := existingPythonSourceRule{ + name: packageLibraryName, + srcs: treeset.NewWith(godsutils.StringComparator), + declaredSrcCount: 0, + } + for _, other := range rules { + if existingRulesShareSrcs(candidate, other) { + return rules + } + } + return append(rules, candidate) + } + return rules +} + +// addTargetNamesForSrcs records the per-file target name Gazelle derives from +// each of srcs. +func addTargetNamesForSrcs(srcs *treeset.Set, dst map[string]struct{}) { + it := srcs.Iterator() + for it.Next() { + src := it.Value().(string) + dst[strings.TrimSuffix(filepath.Base(src), ".py")] = struct{}{} + } +} + // findConftestPaths returns package paths containing conftest.py, from currentPkg // up through ancestors, stopping at module root. func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string { @@ -271,27 +609,165 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes autoIncludeInit = cfg.PerFileGenerationIncludeInit() && hasInit && hasPopulatedInit } - appendPyLibrary := func(srcs *treeset.Set, pyLibraryTargetName string) { - allDeps, mainModules, annotations, err := parser.parse(srcs) - for name := range mainModules { - validFilesMap[name] = struct{}{} + // knownPySrcs is the set of source files Gazelle manages in this package, i.e. + // the ones it would put in a generated target's srcs. It is narrower than "the + // .py files that exist here": files hidden by python_ignore_files or + // gazelle:exclude, and subdirectory files in per-package mode, are absent. + // The entrypoints and conftest.py are diverted out of pyLibraryFilenames and + // pyTestFilenames by the scan above, so they are added back explicitly. + knownPySrcs := make(map[string]struct{}) + addSetValuesToMap(pyLibraryFilenames, knownPySrcs) + addSetValuesToMap(pyTestFilenames, knownPySrcs) + for _, src := range []struct { + name string + present bool + }{ + {pyBinaryEntrypointFilename, hasPyBinaryEntryPointFile}, + {pyTestEntrypointFilename, hasPyTestEntryPointFile}, + {conftestFilename, hasConftestFile}, + } { + if src.present { + knownPySrcs[src.name] = struct{}{} } + } + + // generatedTargetNames holds the names Gazelle generates in this package. An + // existing rule with one of those names is not a hand-written target to + // adopt: adopting it would put two rules with the same name into result.Gen, + // where they merge into one and silently orphan the sources of whichever rule + // lost. Names Gazelle does not emit stay available to hand-written targets. + packageLibraryName := cfg.RenderLibraryName(packageName) + existingPyLibraries := collectExistingPythonSourceRules(args, pyLibraryKind, knownPySrcs) + if !cfg.PerFileGeneration() { + existingPyLibraries = adoptExcludedInitOnlyPackageLibraryForSplitLayout( + args, + pyLibraryKind, + packageLibraryName, + knownPySrcs, + existingPyLibraries, + ) + existingPyLibraries = adoptEmptyAggregatePackageLibraryForSplitLayout( + args, + pyLibraryKind, + packageLibraryName, + existingPyLibraries, + ) + } + existingPyTests := collectExistingPythonSourceRules(args, pyTestKind, knownPySrcs) + splitPackageLibraryLayout := false + if !cfg.PerFileGeneration() { + splitPackageLibraryLayout = hasSplitPackageLibraryLayout(packageLibraryName, existingPyLibraries) + if !splitPackageLibraryLayout { + splitPackageLibraryLayout = hasExplicitSourceOwnershipLayout( + packageLibraryName, + existingPyLibraries, + pyLibraryFilenames, + ) + } + } + + generatedTargetNames := make(map[string]struct{}) + if cfg.PerFileGeneration() { + addTargetNamesForSrcs(pyLibraryFilenames, generatedTargetNames) + addTargetNamesForSrcs(pyTestFilenames, generatedTargetNames) + } else if !splitPackageLibraryLayout { + // A name is only reserved when Gazelle emits a target with it. A + // test-only package generates no package library, so a hand-written + // py_test may carry the package library name, and a package without + // tests leaves the generated test name free. + if !pyLibraryFilenames.Empty() { + generatedTargetNames[packageLibraryName] = struct{}{} + } + if !pyTestFilenames.Empty() || hasPyTestEntryPointFile || hasPyTestEntryPointTarget { + generatedTargetNames[cfg.RenderTestName(packageName)] = struct{}{} + } + } + if hasPyBinaryEntryPointFile { + generatedTargetNames[cfg.RenderBinaryName(packageName)] = struct{}{} + } + if hasConftestFile { + generatedTargetNames[conftestTargetname] = struct{}{} + } + isNotGeneratedTargetName := func(sourceRule existingPythonSourceRule) bool { + _, isGenerated := generatedTargetNames[sourceRule.name] + return !isGenerated + } + + existingPyLibraries = filterExistingPythonSourceRules( + existingPyLibraries, + isNotGeneratedTargetName, + ) + existingPyTests = filterExistingPythonSourceRules(existingPyTests, isNotGeneratedTargetName) + hasPreservedPackageLibrary := splitPackageLibraryLayout + // A library that owns a single source does not claim it: the source stays in + // the generated target as well, which is what users of the long-standing + // "extra target over one file" pattern expect. Coarse-grained generation has + // a single library for the whole tree, so there claiming is unconditional or + // the source would be owned twice. When a hand-written target already uses + // the package library name alongside per-file libraries, every other + // preserved library claims its sources so the generated package library does + // not duplicate them. A py_test always claims, because a source pulled into + // two test targets is executed twice. + claimingPyLibraries := filterExistingPythonSourceRules( + existingPyLibraries, + func(sourceRule existingPythonSourceRule) bool { + if sourceRule.declaredSrcCount > 1 || cfg.CoarseGrainedGeneration() { + return true + } + if hasPreservedPackageLibrary && sourceRule.name != packageLibraryName { + return true + } + return false + }, + ) + removeClaimedSrcs(claimingPyLibraries, pyLibraryFilenames, pyTestFilenames) + removeClaimedSrcs(existingPyTests, pyLibraryFilenames, pyTestFilenames) + + // extractedMainModules tracks the main modules that already have a generated + // py_binary target. A source file can be owned by both a preserved target and + // a generated one, in which case appendPyLibrary sees it twice and would + // otherwise emit a duplicate py_binary for it. + extractedMainModules := make(map[string]struct{}) + + // autoIncludedInit reports whether the caller added pyLibraryEntrypointFilename + // to srcs itself, rather than it being a source the user listed by hand. Only + // in the former case may it be removed again when a main module is extracted. + // + // isPreserved reports whether the target is an existing hand-written one being + // regenerated in place, as opposed to one Gazelle created. + appendPyLibrary := func( + srcs *treeset.Set, + pyLibraryTargetName string, + autoIncludedInit, isPreserved bool, + ) { + allDeps, mainModules, annotations, err := parser.parse(srcs) if err != nil { log.Fatalf("ERROR: %v\n", err) } + for name := range mainModules { + validFilesMap[name] = struct{}{} + } + srcsChanged := false if !hasPyBinaryEntryPointFile { // Creating one py_binary target per main module when __main__.py doesn't exist. mainFileNames := make([]string, 0, len(mainModules)) for name := range mainModules { + // A py_binary named after the target it would be extracted from + // cannot be generated: both rules would land in result.Gen under + // the same name and merge into one. + if isPreserved && strings.TrimSuffix(filepath.Base(name), ".py") == pyLibraryTargetName { + continue + } mainFileNames = append(mainFileNames, name) // Remove the file from srcs if we're doing per-file library generation so // that we don't also generate a py_library target for it. if cfg.PerFileGeneration() { srcs.Remove(name) + srcsChanged = true // Also remove the __init__.py that was added earlier. - if autoIncludeInit { + if autoIncludedInit { srcs.Remove(pyLibraryEntrypointFilename) } } @@ -299,6 +775,11 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes sort.Strings(mainFileNames) for _, filename := range mainFileNames { + if _, ok := extractedMainModules[filename]; ok { + continue + } + extractedMainModules[filename] = struct{}{} + pyBinaryTargetName := strings.TrimSuffix(filepath.Base(filename), ".py") if err := ensureNoCollision(args.Config, args.File, pyBinaryTargetName, pyBinaryKind); err != nil { fqTarget := label.New("", args.Rel, pyBinaryTargetName) @@ -333,20 +814,56 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // If we're doing per-file generation, srcs could be empty at this point, meaning we shouldn't make a py_library. // If there is already a package named py_library target before, we should generate an empty py_library. if srcs.Empty() { + // Leave a preserved target exactly as it was written instead. Falling + // through would build an empty rule, which Gazelle reports as removable + // and so deletes a hand-written target. + if isPreserved { + return + } if args.File == nil { return } generateEmptyLibrary := false for _, r := range args.File.Rules { - if r.Name() == pyLibraryTargetName && kindMatches(args.Config, r, pyLibraryKind) { + if r.Name() != pyLibraryTargetName || !kindMatches(args.Config, r, pyLibraryKind) { + continue + } + if r.ShouldKeep() { generateEmptyLibrary = true + break + } + // A hand-written package library that still lists srcs but owns + // only excluded or otherwise unmanaged files is not adopted and + // leaves nothing for Gazelle to generate. Do not treat it as an + // empty generated library to remove. + if len(r.AttrStrings("srcs")) > 0 { + return } + result.Empty = append(result.Empty, newTargetBuilder( + pyLibraryKind, + pyLibraryTargetName, + pythonProjectRoot, + args.Rel, + pyFileNames, + cfg.ResolveSiblingImports(), + ).build()) + return } if !generateEmptyLibrary { return } } + if srcsChanged { + // The dependencies above were derived from the srcs the target had + // before the main modules were extracted. Recompute them so the target + // does not keep dependencies contributed by a source it no longer owns. + allDeps, _, annotations, err = parser.parse(srcs) + if err != nil { + log.Fatalf("ERROR: %v\n", err) + } + } + // Add any sibling .pyi files to pyi_srcs pyiSrcs, _ := getPyiFilenames(srcs, cfg.GeneratePyiSrcs(), args.Dir) @@ -362,15 +879,30 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes collisionErrors.Add(err) } - pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). - addVisibility(visibility). + pyLibraryBuilder := newTargetBuilder( + pyLibraryKind, + pyLibraryTargetName, + pythonProjectRoot, + args.Rel, + pyFileNames, + cfg.ResolveSiblingImports(), + ). addSrcs(srcs). addPyiSrcs(pyiSrcs). addModuleDependencies(allDeps). addResolvedDependencies(annotations.includeDeps). generateImportsAttribute(). - setAnnotations(*annotations). - build() + setAnnotations(*annotations) + + // visibility is not a mergeable attribute, so rule.MergeRules copies it + // into an existing rule that does not set one and offers no '# keep' to + // prevent that. Injecting it would silently widen a hand-written target + // that relies on Bazel's default private visibility. + if !isPreserved { + pyLibraryBuilder.addVisibility(visibility) + } + + pyLibrary := pyLibraryBuilder.build() if pyLibrary.IsEmpty(py.Kinds()[pyLibrary.Kind()]) { result.Empty = append(result.Empty, pyLibrary) @@ -380,6 +912,28 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } } + for _, existingPyLibrary := range existingPyLibraries { + if existingPyLibrary.name == packageLibraryName && + existingPyLibrary.declaredSrcCount == 0 && + existingPyLibrary.srcs.Empty() { + continue + } + srcs := existingPyLibrary.srcs + if existingPyLibrary.name == packageLibraryName && + existingPyLibrary.declaredSrcCount > 0 && + !cfg.PerFileGeneration() { + mergedSrcs := treeset.NewWith(godsutils.StringComparator) + srcs.Each(func(index int, filename interface{}) { + mergedSrcs.Add(filename) + }) + pyLibraryFilenames.Each(func(index int, filename interface{}) { + mergedSrcs.Add(filename) + }) + srcs = mergedSrcs + } + appendPyLibrary(srcs, existingPyLibrary.name, false, true) + } + if cfg.PerFileGeneration() { pyLibraryFilenames.Each(func(index int, filename interface{}) { pyLibraryTargetName := strings.TrimSuffix(filepath.Base(filename.(string)), ".py") @@ -390,10 +944,10 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes if autoIncludeInit { srcs.Add(pyLibraryEntrypointFilename) } - appendPyLibrary(srcs, pyLibraryTargetName) + appendPyLibrary(srcs, pyLibraryTargetName, autoIncludeInit, false) }) - } else { - appendPyLibrary(pyLibraryFilenames, cfg.RenderLibraryName(packageName)) + } else if !hasPreservedPackageLibrary { + appendPyLibrary(pyLibraryFilenames, packageLibraryName, false, false) } if hasPyBinaryEntryPointFile { @@ -503,6 +1057,11 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes setAnnotations(*annotations). generateImportsAttribute() } + + for _, existingPyTest := range existingPyTests { + pyTestTargets = append(pyTestTargets, newPyTestTargetBuilder(existingPyTest.srcs, existingPyTest.name)) + } + if (!cfg.PerPackageGenerationRequireTestEntryPoint() || hasPyTestEntryPointFile || hasPyTestEntryPointTarget || cfg.CoarseGrainedGeneration()) && !cfg.PerFileGeneration() { // Create one py_test target per package if hasPyTestEntryPointFile { @@ -572,6 +1131,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } emptyRules := py.getRulesWithInvalidSrcs(args, validFilesMap) result.Empty = append(result.Empty, emptyRules...) + result.Empty = append(result.Empty, getStaleSourcelessPyLibraryRules(args, packageLibraryName, generatedTargetNames, result.Empty)...) if !collisionErrors.Empty() { it := collisionErrors.Iterator() for it.Next() { @@ -583,12 +1143,107 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes return result } +// getStaleSourcelessPyLibraryRules returns deps-only py_library targets with no +// srcs that Gazelle should delete. Hand-written re-export umbrellas must use +// a "# keep" suffix comment to opt out. +func getStaleSourcelessPyLibraryRules( + args language.GenerateArgs, + packageLibraryName string, + generatedTargetNames map[string]struct{}, + alreadyScheduled []*rule.Rule, +) []*rule.Rule { + if args.File == nil { + return nil + } + alreadyEmpty := make(map[string]struct{}, len(alreadyScheduled)) + for _, r := range alreadyScheduled { + alreadyEmpty[r.Name()] = struct{}{} + } + var stale []*rule.Rule + for _, existingRule := range args.File.Rules { + if !kindMatches(args.Config, existingRule, pyLibraryKind) { + continue + } + if existingRule.Name() != packageLibraryName { + continue + } + if existingRule.ShouldKeep() { + continue + } + if len(existingRule.AttrStrings("srcs")) != 0 { + continue + } + if _, isGenerated := generatedTargetNames[existingRule.Name()]; isGenerated { + continue + } + if _, dup := alreadyEmpty[existingRule.Name()]; dup { + continue + } + stale = append(stale, newTargetBuilder( + pyLibraryKind, + existingRule.Name(), + "", + "", + nil, + false, + ).build()) + } + return stale +} + +// ruleListsGazelleManagedSrc reports whether any src is one Gazelle would place +// in a generated target for this package. +func ruleListsGazelleManagedSrc(srcs []string, managed map[string]struct{}) bool { + for _, src := range srcs { + if isTargetSrc(src) || filepath.Ext(src) != ".py" { + continue + } + if _, ok := managed[src]; ok { + return true + } + } + return false +} + +// isExcludedInitOnlyPackageLibrarySrcOnDisk reports whether src is the only +// source of the hand-written package library and exists on disk but is hidden +// from Gazelle generation (for example via gazelle:exclude). +func isExcludedInitOnlyPackageLibrarySrcOnDisk( + args language.GenerateArgs, + packageLibraryName string, + existingRule *rule.Rule, + src string, +) bool { + if !kindMatches(args.Config, existingRule, pyLibraryKind) { + return false + } + if existingRule.Name() != packageLibraryName || src != pyLibraryEntrypointFilename { + return false + } + srcs := existingRule.AttrStrings("srcs") + if len(srcs) != 1 { + return false + } + _, err := os.Stat(filepath.Join(args.Dir, src)) + return err == nil +} + // getRulesWithInvalidSrcs checks existing Python rules in the BUILD file and return the rules with invalid source files. // Invalid source files are files that do not exist or not a target. func (py *Python) getRulesWithInvalidSrcs(args language.GenerateArgs, validFilesMap map[string]struct{}) (invalidRules []*rule.Rule) { if args.File == nil { return } + packageLibraryName := filepath.Base(args.Dir) + if args.Config != nil { + if raw, ok := args.Config.Exts[languageName]; ok && raw != nil { + cfg := raw.(pythonconfig.Configs)[args.Rel] + if cfg != nil { + packageLibraryName = cfg.RenderLibraryName(packageLibraryName) + } + } + } + for _, file := range args.GenFiles { validFilesMap[file] = struct{}{} } @@ -603,10 +1258,6 @@ func (py *Python) getRulesWithInvalidSrcs(args language.GenerateArgs, validFiles for _, file := range args.RegularFiles { allFilesMap[file] = struct{}{} } - - isTarget := func(src string) bool { - return strings.HasPrefix(src, "@") || strings.HasPrefix(src, "//") || strings.HasPrefix(src, ":") - } for _, existingRule := range args.File.Rules { var matchedKind string var filesMap map[string]struct{} @@ -629,7 +1280,7 @@ func (py *Python) getRulesWithInvalidSrcs(args language.GenerateArgs, validFiles } var hasValidSrcs bool for _, src := range srcs { - if isTarget(src) { + if isTargetSrc(src) { hasValidSrcs = true break } @@ -637,6 +1288,28 @@ func (py *Python) getRulesWithInvalidSrcs(args language.GenerateArgs, validFiles hasValidSrcs = true break } + if isExcludedInitOnlyPackageLibrarySrcOnDisk( + args, + packageLibraryName, + existingRule, + src, + ) { + hasValidSrcs = true + break + } + } + if !hasValidSrcs && matchedKind != pyBinaryKind && + !ruleListsGazelleManagedSrc(srcs, validFilesMap) { + for _, src := range srcs { + if isTargetSrc(src) { + hasValidSrcs = true + break + } + if _, err := os.Stat(filepath.Join(args.Dir, src)); err == nil { + hasValidSrcs = true + break + } + } } if !hasValidSrcs { invalidRules = append(invalidRules, newTargetBuilder(matchedKind, existingRule.Name(), "", "", nil, false).build()) diff --git a/gazelle/python/generate_test.go b/gazelle/python/generate_test.go new file mode 100644 index 0000000000..af20ef8b9f --- /dev/null +++ b/gazelle/python/generate_test.go @@ -0,0 +1,332 @@ +package python + +import ( + "os" + "path/filepath" + "testing" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/language" + "github.com/bazelbuild/bazel-gazelle/rule" + "github.com/emirpasic/gods/sets/treeset" + godsutils "github.com/emirpasic/gods/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/bazel-contrib/rules_python/gazelle/pythonconfig" +) + +func newTestBuildFile(rules ...*rule.Rule) *rule.File { + return &rule.File{ + Path: "BUILD.bazel", + Rules: rules, + } +} + +func newPyLibraryRule(name string, srcs []string) *rule.Rule { + r := rule.NewRule("py_library", name) + r.SetAttr("srcs", srcs) + return r +} + +func TestCollectExistingPythonSourceRulesSkipsExcludedOnly(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "excluded_only.py"), []byte(""), 0o600)) + + args := language.GenerateArgs{ + Dir: dir, + File: newTestBuildFile(newPyLibraryRule("excluded_only", []string{"excluded_only.py"})), + Config: &config.Config{}, + } + knownSrcs := map[string]struct{}{"foo.py": {}} + + rules := collectExistingPythonSourceRules(args, pyLibraryKind, knownSrcs) + assert.Empty(t, rules) +} + +func TestCollectExistingPythonSourceRulesKeepsExcludedAlongsideKnown(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo.py"), []byte(""), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "excluded_only.py"), []byte(""), 0o600)) + + args := language.GenerateArgs{ + Dir: dir, + File: newTestBuildFile(newPyLibraryRule("custom", []string{ + "foo.py", + "excluded_only.py", + })), + Config: &config.Config{}, + } + knownSrcs := map[string]struct{}{"foo.py": {}} + + rules := collectExistingPythonSourceRules(args, pyLibraryKind, knownSrcs) + require.Len(t, rules, 1) + assert.Equal(t, "custom", rules[0].name) + assert.Equal(t, 2, rules[0].srcs.Size()) +} + +func TestAdoptExcludedInitOnlyPackageLibraryForSplitLayout(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "__init__.py"), []byte(""), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo.py"), []byte(""), 0o600)) + + packageLibraryName := "pkg" + args := language.GenerateArgs{ + Dir: dir, + File: newTestBuildFile( + newPyLibraryRule("foo", []string{"foo.py"}), + newPyLibraryRule(packageLibraryName, []string{pyLibraryEntrypointFilename}), + ), + Config: &config.Config{}, + } + knownSrcs := map[string]struct{}{"foo.py": {}} + + adopted := adoptExcludedInitOnlyPackageLibraryForSplitLayout( + args, + pyLibraryKind, + packageLibraryName, + knownSrcs, + collectExistingPythonSourceRules(args, pyLibraryKind, knownSrcs), + ) + require.Len(t, adopted, 2) + assert.True(t, hasSplitPackageLibraryLayout(packageLibraryName, adopted)) + + packageRule := adopted[1] + assert.Equal(t, packageLibraryName, packageRule.name) + assert.Equal(t, 1, packageRule.srcs.Size()) + assert.True(t, packageRule.srcs.Contains(pyLibraryEntrypointFilename)) +} + +func TestAdoptExcludedInitOnlyPackageLibraryIgnoredWithoutOtherLibraries(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "__init__.py"), []byte(""), 0o600)) + + packageLibraryName := "pkg" + args := language.GenerateArgs{ + Dir: dir, + File: newTestBuildFile(newPyLibraryRule(packageLibraryName, []string{pyLibraryEntrypointFilename})), + Config: &config.Config{}, + } + + adopted := adoptExcludedInitOnlyPackageLibraryForSplitLayout( + args, + pyLibraryKind, + packageLibraryName, + map[string]struct{}{}, + nil, + ) + assert.Nil(t, adopted) +} + +func TestGetRulesWithInvalidSrcsKeepsExcludedSourcesOnDisk(t *testing.T) { + t.Parallel() + + dir := filepath.Join(t.TempDir(), "pkg") + require.NoError(t, os.MkdirAll(dir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "__init__.py"), []byte(""), 0o600)) + + buildFile := newTestBuildFile(newPyLibraryRule("pkg", []string{pyLibraryEntrypointFilename})) + pkgConfig := pythonconfig.New(dir, "") + args := language.GenerateArgs{ + Dir: dir, + Rel: "pkg", + File: buildFile, + Config: &config.Config{ + Exts: map[string]interface{}{ + "py": pythonconfig.Configs{"pkg": pkgConfig}, + }, + }, + RegularFiles: []string{"foo.py"}, + } + + py := &Python{} + invalid := py.getRulesWithInvalidSrcs(args, map[string]struct{}{}) + assert.Empty(t, invalid) +} + +func TestHasSplitPackageLibraryLayout(t *testing.T) { + t.Parallel() + + packageLibraryName := "pkg" + fooSrcs := treeset.NewWith(godsutils.StringComparator, "foo.py") + pkgSrcs := treeset.NewWith(godsutils.StringComparator, "aux.py") + + rules := []existingPythonSourceRule{ + {name: "foo", srcs: fooSrcs, declaredSrcCount: 1}, + {name: packageLibraryName, srcs: pkgSrcs, declaredSrcCount: 1}, + } + assert.True(t, hasSplitPackageLibraryLayout(packageLibraryName, rules)) + + overlapPkg := treeset.NewWith(godsutils.StringComparator, "foo.py", "aux.py") + overlapRules := []existingPythonSourceRule{ + {name: "foo", srcs: fooSrcs, declaredSrcCount: 1}, + {name: packageLibraryName, srcs: overlapPkg, declaredSrcCount: 2}, + } + assert.False(t, hasSplitPackageLibraryLayout(packageLibraryName, overlapRules)) +} + +func TestEmptyAggregateFixtureSplitLayout(t *testing.T) { + t.Parallel() + + packageLibraryName := "package_mode_respect_existing_split_package_library_empty_aggregate" + dir := filepath.Join(t.TempDir(), packageLibraryName) + require.NoError(t, os.MkdirAll(dir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo.py"), []byte(""), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "bar.py"), []byte(""), 0o600)) + + pkgRule := rule.NewRule("py_library", packageLibraryName) + pkgRule.SetAttr("deps", []string{":foo"}) + args := language.GenerateArgs{ + Dir: dir, + File: newTestBuildFile( + newPyLibraryRule("foo", []string{"foo.py"}), + newPyLibraryRule("bar", []string{"bar.py"}), + pkgRule, + ), + Config: &config.Config{}, + } + knownSrcs := map[string]struct{}{"foo.py": {}, "bar.py": {}} + + rules := collectExistingPythonSourceRules(args, pyLibraryKind, knownSrcs) + rules = adoptEmptyAggregatePackageLibraryForSplitLayout(args, pyLibraryKind, packageLibraryName, rules) + require.Len(t, rules, 2) + assert.False(t, hasSplitPackageLibraryLayout(packageLibraryName, rules)) +} + +func TestAdoptEmptyAggregatePackageLibraryForSplitLayout(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo.py"), []byte(""), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "bar.py"), []byte(""), 0o600)) + + packageLibraryName := "pkg" + args := language.GenerateArgs{ + Dir: dir, + File: newTestBuildFile( + rule.NewRule("py_library", packageLibraryName), + newPyLibraryRule("foo", []string{"foo.py"}), + newPyLibraryRule("bar", []string{"bar.py"}), + ), + Config: &config.Config{}, + } + knownSrcs := map[string]struct{}{"foo.py": {}, "bar.py": {}} + + adopted := adoptEmptyAggregatePackageLibraryForSplitLayout( + args, + pyLibraryKind, + packageLibraryName, + collectExistingPythonSourceRules(args, pyLibraryKind, knownSrcs), + ) + require.Len(t, adopted, 2) + assert.False(t, hasSplitPackageLibraryLayout(packageLibraryName, adopted)) +} + +func TestAdoptEmptyAggregatePackageLibraryIgnoredWithoutPerFileLibraries(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + packageLibraryName := "pkg" + args := language.GenerateArgs{ + Dir: dir, + File: newTestBuildFile(rule.NewRule("py_library", packageLibraryName)), + Config: &config.Config{}, + } + + adopted := adoptEmptyAggregatePackageLibraryForSplitLayout( + args, + pyLibraryKind, + packageLibraryName, + nil, + ) + assert.Nil(t, adopted) +} + +func TestHasExplicitSourceOwnershipLayout(t *testing.T) { + t.Parallel() + + packageLibraryName := "pkg" + fooSrcs := treeset.NewWith(godsutils.StringComparator, "foo.py") + barSrcs := treeset.NewWith(godsutils.StringComparator, "bar.py") + libraryFilenames := treeset.NewWith(godsutils.StringComparator, "foo.py", "bar.py") + + rules := []existingPythonSourceRule{ + {name: "foo", srcs: fooSrcs, declaredSrcCount: 1}, + {name: "bar", srcs: barSrcs, declaredSrcCount: 1}, + } + assert.True(t, hasExplicitSourceOwnershipLayout(packageLibraryName, rules, libraryFilenames)) + + withPackageLib := append(rules, existingPythonSourceRule{ + name: packageLibraryName, + srcs: treeset.NewWith(godsutils.StringComparator), + declaredSrcCount: 0, + }) + assert.False(t, hasExplicitSourceOwnershipLayout(packageLibraryName, withPackageLib, libraryFilenames)) + + multiSrc := []existingPythonSourceRule{ + {name: "custom", srcs: libraryFilenames, declaredSrcCount: 2}, + } + assert.True(t, hasExplicitSourceOwnershipLayout(packageLibraryName, multiSrc, libraryFilenames)) + + authSrcs := treeset.NewWith(godsutils.StringComparator, "auth.py", "oauth2.py") + mixedFilenames := treeset.NewWith(godsutils.StringComparator, "foo.py", "auth.py", "oauth2.py") + mixed := []existingPythonSourceRule{ + {name: "foo", srcs: fooSrcs, declaredSrcCount: 1}, + {name: "auth", srcs: authSrcs, declaredSrcCount: 2}, + } + assert.True(t, hasExplicitSourceOwnershipLayout(packageLibraryName, mixed, mixedFilenames)) + + onlySrcs := treeset.NewWith(godsutils.StringComparator, "only.py") + singleTarget := []existingPythonSourceRule{ + {name: "only", srcs: onlySrcs, declaredSrcCount: 1}, + } + singleFilenames := treeset.NewWith(godsutils.StringComparator, "only.py") + assert.True(t, hasExplicitSourceOwnershipLayout(packageLibraryName, singleTarget, singleFilenames)) + + overlap := []existingPythonSourceRule{ + {name: "a", srcs: fooSrcs, declaredSrcCount: 1}, + {name: "b", srcs: libraryFilenames, declaredSrcCount: 2}, + } + assert.False(t, hasExplicitSourceOwnershipLayout(packageLibraryName, overlap, libraryFilenames)) + + partial := []existingPythonSourceRule{ + {name: "foo", srcs: fooSrcs, declaredSrcCount: 1}, + } + assert.False(t, hasExplicitSourceOwnershipLayout(packageLibraryName, partial, libraryFilenames)) +} + +func TestAdoptEmptyAggregatePackageLibraryIgnoredWithMultiSrcLibrary(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo.py"), []byte(""), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "bar.py"), []byte(""), 0o600)) + + packageLibraryName := "pkg" + args := language.GenerateArgs{ + Dir: dir, + File: newTestBuildFile( + rule.NewRule("py_library", packageLibraryName), + newPyLibraryRule("custom", []string{"foo.py", "bar.py"}), + ), + Config: &config.Config{}, + } + knownSrcs := map[string]struct{}{"foo.py": {}, "bar.py": {}} + + adopted := adoptEmptyAggregatePackageLibraryForSplitLayout( + args, + pyLibraryKind, + packageLibraryName, + collectExistingPythonSourceRules(args, pyLibraryKind, knownSrcs), + ) + require.Len(t, adopted, 1) + assert.Equal(t, "custom", adopted[0].name) +} diff --git a/gazelle/python/parser.go b/gazelle/python/parser.go index 3d0dbe7a5f..bead1848b1 100644 --- a/gazelle/python/parser.go +++ b/gazelle/python/parser.go @@ -92,6 +92,8 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, map[strin mainModules := make(map[string]*treeset.Set, len(chRes)) allAnnotations := new(annotations) allAnnotations.ignore = make(map[string]struct{}) + var includesPytestConftest bool + var excludesPytestConftest bool for res := range chRes { if res.HasMain { mainModules[res.FileName] = treeset.NewWith(moduleComparator) @@ -125,9 +127,24 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, map[strin allAnnotations.ignore[k] = v } allAnnotations.includeDeps = append(allAnnotations.includeDeps, annotations.includeDeps...) - allAnnotations.includePytestConftest = annotations.includePytestConftest + if annotations.includePytestConftest != nil { + if *annotations.includePytestConftest { + includesPytestConftest = true + } else { + excludesPytestConftest = true + } + } } + if includesPytestConftest && excludesPytestConftest { + return nil, nil, nil, fmt.Errorf( + "conflicting values for the %q annotation across Python source files", + annotationKindIncludePytestConftest, + ) + } + if includesPytestConftest || excludesPytestConftest { + allAnnotations.includePytestConftest = &includesPytestConftest + } allAnnotations.includeDeps = removeDupesFromStringTreeSetSlice(allAnnotations.includeDeps) return modules, mainModules, allAnnotations, nil diff --git a/gazelle/python/parser_test.go b/gazelle/python/parser_test.go new file mode 100644 index 0000000000..9aff397fd2 --- /dev/null +++ b/gazelle/python/parser_test.go @@ -0,0 +1,92 @@ +package python + +import ( + "os" + "path/filepath" + "testing" + + "github.com/emirpasic/gods/sets/treeset" + godsutils "github.com/emirpasic/gods/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseIncludePytestConftestAnnotations(t *testing.T) { + t.Parallel() + + boolPointer := func(value bool) *bool { + return &value + } + tests := []struct { + name string + contents []string + expected *bool + expectErr string + }{ + { + name: "all unset", + contents: []string{"", ""}, + }, + { + name: "false and unset", + contents: []string{"# gazelle:include_pytest_conftest false", ""}, + expected: boolPointer(false), + }, + { + name: "true and unset", + contents: []string{"", "# gazelle:include_pytest_conftest true"}, + expected: boolPointer(true), + }, + { + name: "matching false values", + contents: []string{ + "# gazelle:include_pytest_conftest false", + "# gazelle:include_pytest_conftest false", + "", + }, + expected: boolPointer(false), + }, + { + name: "matching true values", + contents: []string{ + "# gazelle:include_pytest_conftest true", + "", + "# gazelle:include_pytest_conftest true", + }, + expected: boolPointer(true), + }, + { + name: "conflicting values", + contents: []string{ + "# gazelle:include_pytest_conftest false", + "", + "# gazelle:include_pytest_conftest true", + }, + expectErr: "conflicting values for the \"include_pytest_conftest\" annotation " + + "across Python source files", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + repoRoot := t.TempDir() + filenames := treeset.NewWith(godsutils.StringComparator) + for index, contents := range test.contents { + filename := string(rune('a'+index)) + "_test.py" + require.NoError(t, os.WriteFile(filepath.Join(repoRoot, filename), []byte(contents), 0o600)) + filenames.Add(filename) + } + + parser := newPython3Parser(repoRoot, "", func(string) bool { return false }) + _, _, annotations, err := parser.parse(filenames) + if test.expectErr != "" { + assert.EqualError(t, err, test.expectErr) + return + } + + require.NoError(t, err) + assert.Equal(t, test.expected, annotations.includePytestConftest) + }) + } +} diff --git a/gazelle/python/resolve_test.go b/gazelle/python/resolve_test.go new file mode 100644 index 0000000000..5a8a090571 --- /dev/null +++ b/gazelle/python/resolve_test.go @@ -0,0 +1,69 @@ +package python + +import ( + "testing" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/resolve" + "github.com/bazelbuild/bazel-gazelle/rule" + "github.com/stretchr/testify/assert" + + "github.com/bazel-contrib/rules_python/gazelle/pythonconfig" +) + +func TestImportsPerFileGenerationInitPackageTargetDoesNotIndexSiblingModules(t *testing.T) { + t.Parallel() + + fileModeCfg := &pythonconfig.Config{} + fileModeCfg.SetPerFileGeneration(true) + c := &config.Config{ + Exts: map[string]interface{}{ + languageName: pythonconfig.Configs{ + "util/events": fileModeCfg, + }, + }, + } + f := &rule.File{Pkg: "util/events"} + + resolver := &Resolver{} + eventsRule := rule.NewRule("py_library", "events") + eventsRule.SetAttr("srcs", []string{"__init__.py"}) + datadogRule := rule.NewRule("py_library", "datadog") + datadogRule.SetAttr("srcs", []string{"datadog.py"}) + + eventsImports := resolver.Imports(c, eventsRule, f) + datadogImports := resolver.Imports(c, datadogRule, f) + + assert.Equal(t, []string{"util.events"}, importImps(eventsImports)) + assert.Equal(t, []string{"util.events.datadog"}, importImps(datadogImports)) +} + +func TestImportsMergedPackageLibraryIndexesUnclaimedModules(t *testing.T) { + t.Parallel() + + packageModeCfg := &pythonconfig.Config{} + packageModeCfg.SetPerFileGeneration(false) + c := &config.Config{ + Exts: map[string]interface{}{ + languageName: pythonconfig.Configs{ + "pkg": packageModeCfg, + }, + }, + } + f := &rule.File{Pkg: "pkg"} + + resolver := &Resolver{} + pkgRule := rule.NewRule("py_library", "pkg") + pkgRule.SetAttr("srcs", []string{"__init__.py", "datadog.py"}) + + imports := resolver.Imports(c, pkgRule, f) + assert.Equal(t, []string{"pkg", "pkg.datadog"}, importImps(imports)) +} + +func importImps(specs []resolve.ImportSpec) []string { + out := make([]string, len(specs)) + for i, spec := range specs { + out[i] = spec.Imp + } + return out +} diff --git a/gazelle/python/target.go b/gazelle/python/target.go index c7009a6a84..1cbf8eaea9 100644 --- a/gazelle/python/target.go +++ b/gazelle/python/target.go @@ -16,6 +16,7 @@ package python import ( "path/filepath" + "strings" "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/rule" @@ -134,6 +135,10 @@ func (t *targetBuilder) addResolvedDependencies(deps []string) *targetBuilder { // addVisibility adds visibility labels to the target. func (t *targetBuilder) addVisibility(visibility []string) *targetBuilder { for _, item := range visibility { + item = strings.TrimSpace(item) + if item == "" { + continue + } t.visibility.Add(item) } return t diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/BUILD.in b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/BUILD.in new file mode 100644 index 0000000000..786a959d7b --- /dev/null +++ b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode package +# gazelle:python_generation_mode_per_package_require_test_entry_point false diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/BUILD.out b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/BUILD.out new file mode 100644 index 0000000000..786a959d7b --- /dev/null +++ b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/BUILD.out @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode package +# gazelle:python_generation_mode_per_package_require_test_entry_point false diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/README.md b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/README.md new file mode 100644 index 0000000000..97317ffca7 --- /dev/null +++ b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/README.md @@ -0,0 +1,4 @@ +# Conflicting `include_pytest_conftest` annotations + +This test case asserts that Gazelle fails when source files in the same +`py_test` set `include_pytest_conftest` to conflicting values. diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/WORKSPACE b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/conftest.py b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/conftest.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/conftest.py @@ -0,0 +1 @@ + diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/false_test.py b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/false_test.py new file mode 100644 index 0000000000..ba71a2818b --- /dev/null +++ b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/false_test.py @@ -0,0 +1 @@ +# gazelle:include_pytest_conftest false diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/test.yaml b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/test.yaml new file mode 100644 index 0000000000..780d2148d7 --- /dev/null +++ b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/test.yaml @@ -0,0 +1,5 @@ +--- +expect: + exit_code: 1 + stderr: | + gazelle: ERROR: conflicting values for the "include_pytest_conftest" annotation across Python source files diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/true_test.py b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/true_test.py new file mode 100644 index 0000000000..b2d10359da --- /dev/null +++ b/gazelle/python/testdata/annotation_include_pytest_conftest_conflict/true_test.py @@ -0,0 +1 @@ +# gazelle:include_pytest_conftest true diff --git a/gazelle/python/testdata/dont_rename_target/BUILD.out b/gazelle/python/testdata/dont_rename_target/BUILD.out index 62772e30b5..e9bc0e6e29 100644 --- a/gazelle/python/testdata/dont_rename_target/BUILD.out +++ b/gazelle/python/testdata/dont_rename_target/BUILD.out @@ -3,5 +3,4 @@ load("@rules_python//python:defs.bzl", "py_library") py_library( name = "my_custom_target", srcs = ["__init__.py"], - visibility = ["//:__subpackages__"], ) diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/README.md b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/README.md new file mode 100644 index 0000000000..634c610452 --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/README.md @@ -0,0 +1,9 @@ +# File mode with inherited config, init package library, and per-file targets + +Parent `util/BUILD` sets `gazelle:python_generation_mode file`. The child +`util/events` package has no local mode directive, a hand-written package +library named after the directory (`events`) that owns only `__init__.py`, and a +separate per-file library for `datadog.py`. + +Gazelle must not merge unclaimed per-file sources into the package library in +file mode; doing so duplicates import specs with the per-file targets. diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/WORKSPACE b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/test.yaml b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/test.yaml @@ -0,0 +1,15 @@ +# Copyright 2023 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/BUILD.in b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/BUILD.in new file mode 100644 index 0000000000..7afa751466 --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode file +# gazelle:exclude __init__.py diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/BUILD.out b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/BUILD.out new file mode 100644 index 0000000000..7afa751466 --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/BUILD.out @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode file +# gazelle:exclude __init__.py diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/BUILD.in b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/BUILD.in new file mode 100644 index 0000000000..6c19ed1893 --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/BUILD.in @@ -0,0 +1,13 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "events", + srcs = ["__init__.py"], + tags = ["keep_events"], +) + +py_library( + name = "datadog", + srcs = ["datadog.py"], + tags = ["keep_datadog"], +) diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/BUILD.out b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/BUILD.out new file mode 100644 index 0000000000..975896dc65 --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/BUILD.out @@ -0,0 +1,22 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "events", + srcs = ["__init__.py"], + tags = ["keep_events"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "datadog", + srcs = ["datadog.py"], + tags = ["keep_datadog"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "user", + srcs = ["user.py"], + visibility = ["//:__subpackages__"], + deps = [":datadog"], +) diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/__init__.py b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/__init__.py new file mode 100644 index 0000000000..ba1055549d --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/__init__.py @@ -0,0 +1 @@ +"""Events package.""" diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/datadog.py b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/datadog.py new file mode 100644 index 0000000000..729c944fe0 --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/datadog.py @@ -0,0 +1 @@ +"""Datadog helpers for events.""" diff --git a/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/user.py b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/user.py new file mode 100644 index 0000000000..0bc684bb58 --- /dev/null +++ b/gazelle/python/testdata/file_mode_inherited_init_package_and_per_file/util/events/user.py @@ -0,0 +1 @@ +from util.events import datadog diff --git a/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/BUILD.in b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/BUILD.in new file mode 100644 index 0000000000..ec9c96ffc6 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/BUILD.in @@ -0,0 +1,16 @@ +# gazelle:python_generation_mode file + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "widget", + srcs = ["widget.py"], +) + +py_library( + name = "file_mode_remove_stale_sourceless_umbrella", + deps = [ + ":widget", + "//file_mode_remove_stale_sourceless_umbrella/nested", + ], +) diff --git a/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/BUILD.out b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/BUILD.out new file mode 100644 index 0000000000..094d7aa4d9 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/BUILD.out @@ -0,0 +1,9 @@ +# gazelle:python_generation_mode file + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "widget", + srcs = ["widget.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/WORKSPACE b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/nested/BUILD.in b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/nested/BUILD.in new file mode 100644 index 0000000000..280765e424 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/nested/BUILD.in @@ -0,0 +1,8 @@ +# gazelle:python_generation_mode file + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "helper", + srcs = ["helper.py"], +) diff --git a/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/nested/BUILD.out b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/nested/BUILD.out new file mode 100644 index 0000000000..1d838d1da2 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/nested/BUILD.out @@ -0,0 +1,9 @@ +# gazelle:python_generation_mode file + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "helper", + srcs = ["helper.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/nested/helper.py b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/nested/helper.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/test.yaml b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/widget.py b/gazelle/python/testdata/file_mode_remove_stale_sourceless_umbrella/widget.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/BUILD.in b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/BUILD.in new file mode 100644 index 0000000000..3488cfa713 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/BUILD.in @@ -0,0 +1,19 @@ +# gazelle:python_generation_mode file + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "widget", + srcs = ["widget.py"], +) + +py_library( + name = "file_mode_remove_stale_umbrella_stale_dep", + deps = [":widget"], +) + +py_library( + name = "consumer", + srcs = ["consumer.py"], + deps = [":file_mode_remove_stale_umbrella_stale_dep"], +) diff --git a/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/BUILD.out b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/BUILD.out new file mode 100644 index 0000000000..84d62b9a19 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/BUILD.out @@ -0,0 +1,16 @@ +# gazelle:python_generation_mode file + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "widget", + srcs = ["widget.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "consumer", + srcs = ["consumer.py"], + visibility = ["//:__subpackages__"], + deps = [":widget"], +) diff --git a/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/WORKSPACE b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/consumer.py b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/consumer.py new file mode 100644 index 0000000000..4e1d7f6f0a --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/consumer.py @@ -0,0 +1,5 @@ +import widget + + +def use_widget() -> None: + widget.do_thing() diff --git a/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/test.yaml b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/widget.py b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/widget.py new file mode 100644 index 0000000000..93d83bfed4 --- /dev/null +++ b/gazelle/python/testdata/file_mode_remove_stale_umbrella_stale_dep/widget.py @@ -0,0 +1,2 @@ +def do_thing() -> None: + return None diff --git a/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/BUILD.in new file mode 100644 index 0000000000..02e4320ee3 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/BUILD.in @@ -0,0 +1,15 @@ +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "config", + srcs = ["config.py"], + tags = ["keep_config"], +) + +py_library( + name = "email_handlers", + srcs = ["email_handlers.py"], + tags = ["keep_email_handlers"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/BUILD.out new file mode 100644 index 0000000000..02e4320ee3 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/BUILD.out @@ -0,0 +1,15 @@ +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "config", + srcs = ["config.py"], + tags = ["keep_config"], +) + +py_library( + name = "email_handlers", + srcs = ["email_handlers.py"], + tags = ["keep_email_handlers"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/README.md b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/README.md new file mode 100644 index 0000000000..d780e180b9 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/README.md @@ -0,0 +1,5 @@ +# Package mode with only per-file libraries and no package aggregate + +This test verifies that Gazelle does not emit a package-level `py_library` +when every module is already owned by preserved targets with disjoint sources, +and that those targets claim their sources so nothing is duplicated. diff --git a/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/config.py b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/config.py new file mode 100644 index 0000000000..da7ff7c7be --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/config.py @@ -0,0 +1 @@ +"""Config module.""" diff --git a/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/email_handlers.py b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/email_handlers.py new file mode 100644 index 0000000000..36e1bddd71 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/email_handlers.py @@ -0,0 +1 @@ +"""Email handlers module.""" diff --git a/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_all_per_file_no_aggregate/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/BUILD.in new file mode 100644 index 0000000000..3ce356c497 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/BUILD.in @@ -0,0 +1,12 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# "conftest" is the name Gazelle generates for conftest.py, so this target is +# not adopted. +py_library( + name = "conftest", + srcs = [ + "bar.py", + "baz.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/BUILD.out new file mode 100644 index 0000000000..2a59062431 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/BUILD.out @@ -0,0 +1,22 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# "conftest" is the name Gazelle generates for conftest.py, so this target is +# not adopted. +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + tags = ["keep_me"], + visibility = ["//:__subpackages__"], + deps = [":package_mode_respect_existing_conftest_name_collision"], +) + +py_library( + name = "package_mode_respect_existing_conftest_name_collision", + srcs = [ + "bar.py", + "baz.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/README.md b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/README.md new file mode 100644 index 0000000000..7ceabe97d3 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/README.md @@ -0,0 +1,9 @@ +# Package mode with an existing target named after the generated conftest + +This test verifies that the generated-name check covers target names Gazelle +derives from something other than the generation mode. + +`conftest` is the name of the target Gazelle generates for `conftest.py`, so the +existing `conftest` is not adopted. Were it adopted, it would claim `bar.py` and +`baz.py` and the generated `conftest` would then merge over it, dropping both +sources from the build. diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/bar.py b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/bar.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/bar.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/baz.py b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/baz.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/baz.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/conftest.py b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/conftest.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/conftest.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/foo.py b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_conftest_name_collision/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/BUILD.in new file mode 100644 index 0000000000..18ae0770f5 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/BUILD.in @@ -0,0 +1,13 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# Gazelle should leave this target alone: it lists sources that always get their +# own generated targets. +py_library( + name = "custom", + srcs = [ + "__main__.py", + "bar.py", + "conftest.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/BUILD.out new file mode 100644 index 0000000000..4dfca9dc91 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/BUILD.out @@ -0,0 +1,38 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +# Gazelle should leave this target alone: it lists sources that always get their +# own generated targets. +py_library( + name = "custom", + srcs = [ + "__main__.py", + "bar.py", + "conftest.py", + ], + tags = ["keep_me"], +) + +py_library( + name = "package_mode_respect_existing_entrypoint_srcs", + srcs = [ + "bar.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) + +py_binary( + name = "package_mode_respect_existing_entrypoint_srcs_bin", + srcs = ["__main__.py"], + main = "__main__.py", + visibility = ["//:__subpackages__"], + deps = [":package_mode_respect_existing_entrypoint_srcs"], +) + +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + visibility = ["//:__subpackages__"], + deps = [":package_mode_respect_existing_entrypoint_srcs"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/README.md b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/README.md new file mode 100644 index 0000000000..ef86083c99 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/README.md @@ -0,0 +1,9 @@ +# Package mode with an existing target owning entrypoint sources + +This test verifies that a rule listing an entrypoint or `conftest.py` is left +untouched rather than preserved. + +`__main__.py` and `conftest.py` always get their own generated targets, and +Gazelle has no way to hand them over to another target. Regenerating `custom` +in place would therefore leave two targets owning each of those sources, so +`custom` keeps all three of its sources and gains no generated attributes. diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/__main__.py b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/__main__.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/__main__.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/bar.py b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/bar.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/bar.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/conftest.py b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/conftest.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/conftest.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/foo.py b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_entrypoint_srcs/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/BUILD.in new file mode 100644 index 0000000000..51aab3e64f --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/BUILD.in @@ -0,0 +1,17 @@ +# gazelle:exclude excluded_only.py +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +# Entirely excluded sources: Gazelle must leave this target unmanaged. +py_library( + name = "excluded_only", + srcs = ["excluded_only.py"], + tags = ["keep_excluded"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/BUILD.out new file mode 100644 index 0000000000..51aab3e64f --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/BUILD.out @@ -0,0 +1,17 @@ +# gazelle:exclude excluded_only.py +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +# Entirely excluded sources: Gazelle must leave this target unmanaged. +py_library( + name = "excluded_only", + srcs = ["excluded_only.py"], + tags = ["keep_excluded"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/README.md b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/README.md new file mode 100644 index 0000000000..c9a8fbb81e --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/README.md @@ -0,0 +1,5 @@ +# Package mode with an excluded-only hand-written target + +This test verifies that a `py_library` listing only `gazelle:exclude` sources +stays unmanaged while Gazelle still adopts other targets that own +Gazelle-managed modules. diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/excluded_only.py b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/excluded_only.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/excluded_only.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/foo.py b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_only_target/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/README.md b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/README.md new file mode 100644 index 0000000000..41dd140ce0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/README.md @@ -0,0 +1,6 @@ +# Package mode with excluded subdirs and a package-named library + +Mirrors monolith layouts where `benchling/`, `inductive/`, and `tunelab/` +subdirectories are excluded from Gazelle but a hand-written `py_library` named +after the package aggregates their sources, `data`, `deps`, and custom attrs. +Gazelle must leave that target unmanaged. diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/BUILD.in new file mode 100644 index 0000000000..95719be77a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/BUILD.in @@ -0,0 +1,22 @@ +# benchling/, inductive/, and tunelab/ are organizational only; keep all srcs in this target. +# gazelle:exclude benchling +# gazelle:exclude inductive +# gazelle:exclude tunelab + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "models", + srcs = [ + "benchling/alphafold.py", + "inductive/logd.py", + "tunelab/ablab.py", + ], + data = [ + "inductive/logd.yaml", + "tunelab/ablab.yaml", + ], + tags = ["benchling_monolith"], + visibility = ["//visibility:public"], + deps = ["//other:dep"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/BUILD.out new file mode 100644 index 0000000000..95719be77a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/BUILD.out @@ -0,0 +1,22 @@ +# benchling/, inductive/, and tunelab/ are organizational only; keep all srcs in this target. +# gazelle:exclude benchling +# gazelle:exclude inductive +# gazelle:exclude tunelab + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "models", + srcs = [ + "benchling/alphafold.py", + "inductive/logd.py", + "tunelab/ablab.py", + ], + data = [ + "inductive/logd.yaml", + "tunelab/ablab.yaml", + ], + tags = ["benchling_monolith"], + visibility = ["//visibility:public"], + deps = ["//other:dep"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/benchling/alphafold.py b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/benchling/alphafold.py new file mode 100644 index 0000000000..c3746cc944 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/benchling/alphafold.py @@ -0,0 +1 @@ +"""Excluded benchling model stub for Gazelle regression coverage.""" diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/inductive/logd.py b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/inductive/logd.py new file mode 100644 index 0000000000..d44a17a5f1 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/inductive/logd.py @@ -0,0 +1 @@ +"""Excluded inductive model stub for Gazelle regression coverage.""" diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/inductive/logd.yaml b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/inductive/logd.yaml new file mode 100644 index 0000000000..34be8ef15b --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/inductive/logd.yaml @@ -0,0 +1 @@ +name: logd diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/tunelab/ablab.py b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/tunelab/ablab.py new file mode 100644 index 0000000000..8b6ab958bf --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/tunelab/ablab.py @@ -0,0 +1 @@ +"""Excluded tunelab model stub for Gazelle regression coverage.""" diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/tunelab/ablab.yaml b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/tunelab/ablab.yaml new file mode 100644 index 0000000000..491d84f249 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/models/tunelab/ablab.yaml @@ -0,0 +1 @@ +name: ablab diff --git a/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_excluded_subdirs_named_package/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/BUILD.in new file mode 100644 index 0000000000..1b4fcb3aeb --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/BUILD.in @@ -0,0 +1,18 @@ +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +py_library( + name = "auth", + srcs = [ + "auth.py", + "oauth2.py", + ], + tags = ["keep_auth"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/BUILD.out new file mode 100644 index 0000000000..1b4fcb3aeb --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/BUILD.out @@ -0,0 +1,18 @@ +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +py_library( + name = "auth", + srcs = [ + "auth.py", + "oauth2.py", + ], + tags = ["keep_auth"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/README.md b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/README.md new file mode 100644 index 0000000000..7890d121a0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/README.md @@ -0,0 +1,5 @@ +# Package mode with explicit source ownership (mixed single- and multi-source) + +This test verifies that Gazelle does not emit a package-level `py_library` +when preserved libraries collectively own every module without overlap, including +a multi-source target alongside single-source targets. diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/auth.py b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/auth.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/foo.py b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/foo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/oauth2.py b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/oauth2.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_mixed_srcs/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/BUILD.in new file mode 100644 index 0000000000..a8e9dba292 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/BUILD.in @@ -0,0 +1,9 @@ +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "only", + srcs = ["only.py"], + tags = ["keep_only"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/BUILD.out new file mode 100644 index 0000000000..a8e9dba292 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/BUILD.out @@ -0,0 +1,9 @@ +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "only", + srcs = ["only.py"], + tags = ["keep_only"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/README.md b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/README.md new file mode 100644 index 0000000000..92d2b7329e --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/README.md @@ -0,0 +1,5 @@ +# Package mode with a single preserved library owning the only module + +This test verifies that one preserved `py_library` covering the package's only +Gazelle-managed source is enough to suppress the generated package aggregate and +claim that source. diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/only.py b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/only.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_explicit_ownership_single_target/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/BUILD.in new file mode 100644 index 0000000000..5b4869909e --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/BUILD.in @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# Gazelle should preserve this custom source group and prune missing sources. +# Unclaimed sources should still go into the generated package target. +py_library( + name = "custom", + srcs = ["bar.py", "baz.py", "removed.py"], + visibility = ["//visibility:private"], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/BUILD.out new file mode 100644 index 0000000000..5dbd988d50 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/BUILD.out @@ -0,0 +1,30 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +# Gazelle should preserve this custom source group and prune missing sources. +# Unclaimed sources should still go into the generated package target. +py_library( + name = "custom", + srcs = [ + "bar.py", + "baz.py", + ], + tags = ["keep_me"], + visibility = ["//visibility:private"], + deps = [":package_mode_respect_existing_multiple_srcs"], +) + +py_library( + name = "package_mode_respect_existing_multiple_srcs", + srcs = [ + "__init__.py", + "foo.py", + "qux.py", + ], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "bar_test", + srcs = ["bar_test.py"], + deps = [":custom"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/README.md b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/README.md new file mode 100644 index 0000000000..79d828073d --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/README.md @@ -0,0 +1,8 @@ +# Package Mode With Existing Target Spanning Multiple Files + +This test verifies that default package generation preserves a non-standard +`py_library` that already owns multiple sources. + +Gazelle should prune sources that no longer exist, keep the target's +non-generated attributes, add generated dependencies, and put unclaimed sources +in the generated package target. diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/__init__.py b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/bar.py b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/bar.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/bar.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/bar_test.py b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/bar_test.py new file mode 100644 index 0000000000..b6b8723822 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/bar_test.py @@ -0,0 +1 @@ +import bar diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/baz.py b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/baz.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/baz.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/foo.py b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/qux.py b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/qux.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/qux.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_multiple_srcs/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_package_named_test/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/BUILD.in new file mode 100644 index 0000000000..81c75fe65b --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/BUILD.in @@ -0,0 +1,14 @@ +# gazelle:python_generation_mode project + +load("@rules_python//python:defs.bzl", "py_test") + +# Test-only package: no package library is generated, so this hand-written +# target may carry the package library name. +py_test( + name = "package_mode_respect_existing_package_named_test", + srcs = [ + "bar_test.py", + "foo_test.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_package_named_test/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/BUILD.out new file mode 100644 index 0000000000..81c75fe65b --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/BUILD.out @@ -0,0 +1,14 @@ +# gazelle:python_generation_mode project + +load("@rules_python//python:defs.bzl", "py_test") + +# Test-only package: no package library is generated, so this hand-written +# target may carry the package library name. +py_test( + name = "package_mode_respect_existing_package_named_test", + srcs = [ + "bar_test.py", + "foo_test.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_package_named_test/README.md b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/README.md new file mode 100644 index 0000000000..7f1387b40b --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/README.md @@ -0,0 +1,6 @@ +# Package Mode With An Existing Test Named After The Package + +This test verifies that a hand-written `py_test` using the package library name +is preserved in a package that has no library sources. Gazelle generates no +package library there, so the name is free, and the target's sources must not +also appear in a generated `py_test`. diff --git a/gazelle/python/testdata/package_mode_respect_existing_package_named_test/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_package_named_test/bar_test.py b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/bar_test.py new file mode 100644 index 0000000000..0debd4f2ee --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/bar_test.py @@ -0,0 +1,2 @@ +def test_bar(): + assert True diff --git a/gazelle/python/testdata/package_mode_respect_existing_package_named_test/foo_test.py b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/foo_test.py new file mode 100644 index 0000000000..30e2c41ab9 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/foo_test.py @@ -0,0 +1,2 @@ +def test_foo(): + assert True diff --git a/gazelle/python/testdata/package_mode_respect_existing_package_named_test/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_package_named_test/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/BUILD.in new file mode 100644 index 0000000000..d097d1c0a1 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/BUILD.in @@ -0,0 +1,12 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# Two sources are declared, so this target claims bar.py even though removed.py +# no longer exists. +py_library( + name = "custom", + srcs = [ + "bar.py", + "removed.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/BUILD.out new file mode 100644 index 0000000000..96dd47c8fb --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/BUILD.out @@ -0,0 +1,16 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# Two sources are declared, so this target claims bar.py even though removed.py +# no longer exists. +py_library( + name = "custom", + srcs = ["bar.py"], + tags = ["keep_me"], + deps = [":package_mode_respect_existing_pruned_src_count"], +) + +py_library( + name = "package_mode_respect_existing_pruned_src_count", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/README.md b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/README.md new file mode 100644 index 0000000000..e06ecdd77e --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/README.md @@ -0,0 +1,10 @@ +# Package mode with a preserved target whose declared sources are pruned + +This test verifies that whether a preserved library claims its sources depends +only on what the BUILD file declares. + +A library owning a single source does not claim it, but the count is taken from +the srcs the user wrote rather than from the srcs that survive pruning. `custom` +declares two sources, so it claims `bar.py` even though `removed.py` no longer +exists, and `bar.py` stays out of the generated package library. Deleting an +unrelated file must not flip a target between claiming and not claiming. diff --git a/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/bar.py b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/bar.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/bar.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/foo.py b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_pruned_src_count/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/BUILD.in new file mode 100644 index 0000000000..11866a140a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/BUILD.in @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# Gazelle should preserve this custom target. Because it has a single source, +# cli.py stays in the generated package target too, but only one py_binary +# should be generated for it. +py_library( + name = "custom", + srcs = ["cli.py"], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/BUILD.out new file mode 100644 index 0000000000..c97cc86df0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/BUILD.out @@ -0,0 +1,28 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +# Gazelle should preserve this custom target. Because it has a single source, +# cli.py stays in the generated package target too, but only one py_binary +# should be generated for it. +py_library( + name = "custom", + srcs = ["cli.py"], + tags = ["keep_me"], + deps = [":package_mode_respect_existing_single_src_main_module"], +) + +py_binary( + name = "cli", + srcs = ["cli.py"], + visibility = ["//:__subpackages__"], + deps = [":package_mode_respect_existing_single_src_main_module"], +) + +py_library( + name = "package_mode_respect_existing_single_src_main_module", + srcs = [ + "__init__.py", + "cli.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/README.md b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/README.md new file mode 100644 index 0000000000..d3d69e3675 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/README.md @@ -0,0 +1,9 @@ +# Package Mode With Preserved Single-Source Target Containing a Main Module + +This test verifies that a main module owned by a preserved target yields exactly +one `py_binary`. + +A preserved target with a single source does not claim it, so the source is also +part of the generated package target. Gazelle must still extract the main module +only once instead of emitting a duplicate `py_binary` for each target that owns +the source. diff --git a/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/__init__.py b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/cli.py b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/cli.py new file mode 100644 index 0000000000..07cb4bb282 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/cli.py @@ -0,0 +1,4 @@ +import foo + +if __name__ == "__main__": + print(foo) diff --git a/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/foo.py b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_single_src_main_module/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/BUILD.in new file mode 100644 index 0000000000..6726d93651 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/BUILD.in @@ -0,0 +1,29 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +py_library( + name = "bar", + srcs = ["bar.py"], + tags = ["keep_bar"], +) + +py_library( + name = "baz", + srcs = ["baz.py"], + tags = ["keep_baz"], +) + +# Uses the package library name Gazelle generates in package mode. +py_library( + name = "package_mode_respect_existing_split_package_library", + srcs = [ + "aux.py", + "qux.py", + ], + tags = ["keep_pkg"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/BUILD.out new file mode 100644 index 0000000000..9e458620de --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/BUILD.out @@ -0,0 +1,30 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +py_library( + name = "bar", + srcs = ["bar.py"], + tags = ["keep_bar"], +) + +py_library( + name = "baz", + srcs = ["baz.py"], + tags = ["keep_baz"], +) + +# Uses the package library name Gazelle generates in package mode. +py_library( + name = "package_mode_respect_existing_split_package_library", + srcs = [ + "__init__.py", + "aux.py", + "qux.py", + ], + tags = ["keep_pkg"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/README.md b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/README.md new file mode 100644 index 0000000000..0a6fb2db27 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/README.md @@ -0,0 +1,6 @@ +# Package mode with a split package library and per-file libraries + +This test verifies that a hand-written target using the package library name +is preserved and regenerated in place alongside per-file `py_library` targets. +Sources owned by the per-file targets must not also appear in the generated +package library. diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/__init__.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/aux.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/aux.py new file mode 100644 index 0000000000..22a23c4c1c --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/aux.py @@ -0,0 +1 @@ +v = 5 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/bar.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/bar.py new file mode 100644 index 0000000000..47643d4d30 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/bar.py @@ -0,0 +1 @@ +y = 2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/baz.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/baz.py new file mode 100644 index 0000000000..b67ca6a5d8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/baz.py @@ -0,0 +1 @@ +z = 3 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/foo.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/foo.py new file mode 100644 index 0000000000..7d4290a117 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/foo.py @@ -0,0 +1 @@ +x = 1 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/qux.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/qux.py new file mode 100644 index 0000000000..6e912b5abc --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/qux.py @@ -0,0 +1 @@ +w = 4 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/BUILD.in new file mode 100644 index 0000000000..2491f1169a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/BUILD.in @@ -0,0 +1,21 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +py_library( + name = "bar", + srcs = ["bar.py"], + tags = ["keep_bar"], +) + +# Package library name Gazelle generates in package mode; deps-only aggregate. +py_library( + name = "package_mode_respect_existing_split_package_library_empty_aggregate", + deps = [":foo"], + tags = ["keep_pkg"], + visibility = ["//visibility:public"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/BUILD.out new file mode 100644 index 0000000000..9e1e0719fb --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/BUILD.out @@ -0,0 +1,13 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +py_library( + name = "bar", + srcs = ["bar.py"], + tags = ["keep_bar"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/bar.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/bar.py new file mode 100644 index 0000000000..d45ce085dc --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/bar.py @@ -0,0 +1 @@ +"""bar module.""" diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/foo.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/foo.py new file mode 100644 index 0000000000..5a47c3032b --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/foo.py @@ -0,0 +1 @@ +"""foo module.""" diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/BUILD.in new file mode 100644 index 0000000000..c54a656f9c --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/BUILD.in @@ -0,0 +1,21 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +py_library( + name = "bar", + srcs = ["bar.py"], + tags = ["keep_bar"], +) + +# Package library name Gazelle generates in package mode; deps-only aggregate. +py_library( + name = "package_mode_respect_existing_split_package_library_empty_aggregate_keep", + deps = [":foo"], + tags = ["keep_pkg"], + visibility = ["//visibility:public"], +) # keep diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/BUILD.out new file mode 100644 index 0000000000..887215c132 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/BUILD.out @@ -0,0 +1,21 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +py_library( + name = "bar", + srcs = ["bar.py"], + tags = ["keep_bar"], +) + +# Package library name Gazelle generates in package mode; deps-only aggregate. +py_library( + name = "package_mode_respect_existing_split_package_library_empty_aggregate_keep", + tags = ["keep_pkg"], + visibility = ["//visibility:public"], + deps = [":foo"], +) # keep diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/bar.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/foo.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/foo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_empty_aggregate_keep/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/BUILD.in new file mode 100644 index 0000000000..226d6f1b38 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/BUILD.in @@ -0,0 +1,17 @@ +# gazelle:exclude **/__init__.py +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +# Package library name Gazelle generates in package mode; only excluded __init__.py. +py_library( + name = "package_mode_respect_existing_split_package_library_excluded_init", + srcs = ["__init__.py"], + tags = ["keep_pkg"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/BUILD.out new file mode 100644 index 0000000000..226d6f1b38 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/BUILD.out @@ -0,0 +1,17 @@ +# gazelle:exclude **/__init__.py +# gazelle:python_generation_mode package + +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["foo.py"], + tags = ["keep_foo"], +) + +# Package library name Gazelle generates in package mode; only excluded __init__.py. +py_library( + name = "package_mode_respect_existing_split_package_library_excluded_init", + srcs = ["__init__.py"], + tags = ["keep_pkg"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/README.md b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/README.md new file mode 100644 index 0000000000..962dea1174 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/README.md @@ -0,0 +1,7 @@ +# Package mode with excluded `__init__.py` and a split package library + +This test verifies that a hand-written package library owning only an excluded +`__init__.py` is still preserved alongside per-file `py_library` targets. The +parent `gazelle:exclude **/__init__.py` pattern matches Benchling monolith +packages where init-only package libraries must not cause Gazelle to regenerate +a competing package-level target over the remaining modules. diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/__init__.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/foo.py b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/foo.py new file mode 100644 index 0000000000..cf68624419 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/foo.py @@ -0,0 +1,2 @@ +def foo(): + return "foo" diff --git a/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_split_package_library_excluded_init/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/BUILD.in b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/BUILD.in new file mode 100644 index 0000000000..88b41a2764 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/BUILD.in @@ -0,0 +1,17 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_ignore_files ignored.py +# gazelle:exclude excluded.py + +# Gazelle should keep excluded.py and ignored.py because both exist on disk, +# and prune only removed.py. +py_library( + name = "custom", + srcs = [ + "excluded.py", + "foo.py", + "ignored.py", + "removed.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/BUILD.out b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/BUILD.out new file mode 100644 index 0000000000..c64647c907 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/BUILD.out @@ -0,0 +1,23 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_ignore_files ignored.py +# gazelle:exclude excluded.py + +# Gazelle should keep excluded.py and ignored.py because both exist on disk, +# and prune only removed.py. +py_library( + name = "custom", + srcs = [ + "excluded.py", + "foo.py", + "ignored.py", + ], + tags = ["keep_me"], +) + +py_library( + name = "package_mode_respect_existing_unmanaged_srcs", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], + deps = [":custom"], +) diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/README.md b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/README.md new file mode 100644 index 0000000000..64277b0991 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/README.md @@ -0,0 +1,8 @@ +# Package Mode With Preserved Target Owning Unmanaged Sources + +This test verifies that Gazelle only prunes sources that do not exist on disk. + +`ignored.py` and `excluded.py` exist but are hidden from generation by +`python_ignore_files` and `gazelle:exclude`. Those directives suppress +generation; they must not cause Gazelle to delete the sources from a +hand-written target. Only `removed.py`, which does not exist, is pruned. diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/WORKSPACE b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/bar.py b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/bar.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/bar.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/excluded.py b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/excluded.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/excluded.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/foo.py b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/ignored.py b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/ignored.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/ignored.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/test.yaml b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/package_mode_respect_existing_unmanaged_srcs/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/per_file_default_visibility_trailing_comma/BUILD.in b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/BUILD.in new file mode 100644 index 0000000000..a1519f3ac8 --- /dev/null +++ b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode file +# gazelle:python_default_visibility //benchling:__subpackages__,//scripts:__subpackages__,//tests:__subpackages__, diff --git a/gazelle/python/testdata/per_file_default_visibility_trailing_comma/BUILD.out b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/BUILD.out new file mode 100644 index 0000000000..e74acea2aa --- /dev/null +++ b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/BUILD.out @@ -0,0 +1,14 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file +# gazelle:python_default_visibility //benchling:__subpackages__,//scripts:__subpackages__,//tests:__subpackages__, + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = [ + "//benchling:__subpackages__", + "//scripts:__subpackages__", + "//tests:__subpackages__", + ], +) diff --git a/gazelle/python/testdata/per_file_default_visibility_trailing_comma/README.md b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/README.md new file mode 100644 index 0000000000..a1c03df0f5 --- /dev/null +++ b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/README.md @@ -0,0 +1,7 @@ +# Default visibility with a trailing comma in file generation mode + +This test verifies that a `python_default_visibility` directive whose value +ends with a comma does not emit an empty visibility label on generated +per-file targets. That empty label is what Bazel lint rejects as an invalid +`""` label when a new per-file target is emitted after splitting a preserved +multi-source library. diff --git a/gazelle/python/testdata/per_file_default_visibility_trailing_comma/WORKSPACE b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file_default_visibility_trailing_comma/foo.py b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/foo.py new file mode 100644 index 0000000000..5fca550345 --- /dev/null +++ b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/foo.py @@ -0,0 +1 @@ +"""Module for trailing-comma visibility regression.""" diff --git a/gazelle/python/testdata/per_file_default_visibility_trailing_comma/test.yaml b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/per_file_default_visibility_trailing_comma/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/per_file_respect_existing_all_main_modules/BUILD.in b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/BUILD.in new file mode 100644 index 0000000000..5b194e8ec8 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/BUILD.in @@ -0,0 +1,14 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +# Every source is a main module, so this target must be left alone rather than +# emptied and deleted. +py_library( + name = "custom", + srcs = [ + "a.py", + "b.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_all_main_modules/BUILD.out b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/BUILD.out new file mode 100644 index 0000000000..2590d4e158 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/BUILD.out @@ -0,0 +1,34 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +# gazelle:python_generation_mode file + +# Every source is a main module, so this target must be left alone rather than +# emptied and deleted. +py_library( + name = "custom", + srcs = [ + "a.py", + "b.py", + ], + tags = ["keep_me"], +) + +py_binary( + name = "a", + srcs = ["a.py"], + visibility = ["//:__subpackages__"], + deps = [":foo"], +) + +py_binary( + name = "b", + srcs = ["b.py"], + visibility = ["//:__subpackages__"], + deps = [":foo"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_all_main_modules/README.md b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/README.md new file mode 100644 index 0000000000..24f56aa828 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/README.md @@ -0,0 +1,10 @@ +# Per-file generation with a preserved target of only main modules + +This test verifies that a preserved target is never deleted by having its +sources extracted out from under it. + +In per-file generation a main module is removed from the library's srcs so that +only the `py_binary` owns it. Here every source is a main module, so the target +would be left with empty srcs, and an empty rule is one Gazelle reports as +removable. Rather than delete a hand-written target, Gazelle leaves it exactly +as written and still generates the two `py_binary` targets. diff --git a/gazelle/python/testdata/per_file_respect_existing_all_main_modules/WORKSPACE b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file_respect_existing_all_main_modules/a.py b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/a.py new file mode 100644 index 0000000000..07cb4bb282 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/a.py @@ -0,0 +1,4 @@ +import foo + +if __name__ == "__main__": + print(foo) diff --git a/gazelle/python/testdata/per_file_respect_existing_all_main_modules/b.py b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/b.py new file mode 100644 index 0000000000..07cb4bb282 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/b.py @@ -0,0 +1,4 @@ +import foo + +if __name__ == "__main__": + print(foo) diff --git a/gazelle/python/testdata/per_file_respect_existing_all_main_modules/foo.py b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/per_file_respect_existing_all_main_modules/test.yaml b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_all_main_modules/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/BUILD.in b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/BUILD.in new file mode 100644 index 0000000000..002acd3b9c --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/BUILD.in @@ -0,0 +1,13 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +# "qux" is the name Gazelle generates for qux.py, so this target is not adopted. +py_library( + name = "qux", + srcs = [ + "bar.py", + "baz.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/BUILD.out b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/BUILD.out new file mode 100644 index 0000000000..3d7e45756b --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/BUILD.out @@ -0,0 +1,31 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +# "qux" is the name Gazelle generates for qux.py, so this target is not adopted. +py_library( + name = "qux", + srcs = ["qux.py"], + tags = ["keep_me"], + visibility = ["//:__subpackages__"], + deps = [":bar"], +) + +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], + deps = [":foo"], +) + +py_library( + name = "baz", + srcs = ["baz.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/README.md b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/README.md new file mode 100644 index 0000000000..b3e994f86d --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/README.md @@ -0,0 +1,15 @@ +# Per-file generation with an existing target named after a generated one + +This test verifies that Gazelle refuses to adopt an existing target whose name +is one it generates itself. + +`qux` is the per-file target name for `qux.py`, so the existing `qux` is not a +hand-written target to regenerate in place. Adopting it would let it claim +`bar.py` and `baz.py`, suppressing the per-file targets for them, and the +generated `qux` would then merge over it and drop both sources -- leaving no +target that owns them. + +Instead the existing rule is left out of preservation and `bar.py` and `baz.py` +get their own per-file targets. Gazelle still merges the generated `qux` over +the existing rule of that name, which is its long-standing behavior for any +rule whose name matches a generated target. diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/WORKSPACE b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/bar.py b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/bar.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/bar.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/baz.py b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/baz.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/baz.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/foo.py b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/qux.py b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/qux.py new file mode 100644 index 0000000000..b6b8723822 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/qux.py @@ -0,0 +1 @@ +import bar diff --git a/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/test.yaml b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_generated_name_collision/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/BUILD.in b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/BUILD.in new file mode 100644 index 0000000000..55883023d1 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/BUILD.in @@ -0,0 +1,12 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +# Gazelle should preserve this custom source group and prune missing sources. +# Unclaimed sources should still get generated per-file targets. +py_library( + name = "custom", + srcs = ["bar.py", "baz.py", "removed.py"], + visibility = ["//visibility:private"], + tags = ["cant_touch_this"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/BUILD.out b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/BUILD.out new file mode 100644 index 0000000000..a0e9ce97c1 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/BUILD.out @@ -0,0 +1,33 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +# gazelle:python_generation_mode file + +# Gazelle should preserve this custom source group and prune missing sources. +# Unclaimed sources should still get generated per-file targets. +py_library( + name = "custom", + srcs = [ + "bar.py", + "baz.py", + ], + tags = ["cant_touch_this"], + visibility = ["//visibility:private"], + deps = [":foo"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "bar_test", + srcs = ["bar_test.py"], + deps = [":custom"], +) + +py_test( + name = "foo_test", + srcs = ["foo_test.py"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/README.md b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/README.md new file mode 100644 index 0000000000..1998e70a6e --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/README.md @@ -0,0 +1,8 @@ +# Per-File Generation With Existing Target Spanning Multiple Files + +This test verifies that per-file generation preserves a non-standard +`py_library` that already owns multiple sources. + +Gazelle should prune sources that no longer exist, keep the target's +non-generated attributes, add generated dependencies, and create per-file +targets only for unclaimed files. diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/WORKSPACE b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/__init__.py b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/bar.py b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/bar.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/bar.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/bar_test.py b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/bar_test.py new file mode 100644 index 0000000000..b6b8723822 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/bar_test.py @@ -0,0 +1 @@ +import bar diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/baz.py b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/baz.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/baz.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/foo.py b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/foo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/foo_test.py b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/foo_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/test.yaml b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_multiple_srcs/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/BUILD.in b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/BUILD.in new file mode 100644 index 0000000000..5ce4362590 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/BUILD.in @@ -0,0 +1,15 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file +# gazelle:python_generation_mode_per_file_include_init true + +# Gazelle should extract cli.py into a py_binary but keep __init__.py, which was +# written by hand rather than added by python_generation_mode_per_file_include_init. +py_library( + name = "custom", + srcs = [ + "__init__.py", + "cli.py", + ], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/BUILD.out b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/BUILD.out new file mode 100644 index 0000000000..978dd2894f --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/BUILD.out @@ -0,0 +1,31 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +# gazelle:python_generation_mode file +# gazelle:python_generation_mode_per_file_include_init true + +# Gazelle should extract cli.py into a py_binary but keep __init__.py, which was +# written by hand rather than added by python_generation_mode_per_file_include_init. +py_library( + name = "custom", + srcs = ["__init__.py"], + tags = ["keep_me"], +) + +py_binary( + name = "cli", + srcs = [ + "__init__.py", + "cli.py", + ], + visibility = ["//:__subpackages__"], + deps = [":foo"], +) + +py_library( + name = "foo", + srcs = [ + "__init__.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/README.md b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/README.md new file mode 100644 index 0000000000..c65040c1b4 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/README.md @@ -0,0 +1,13 @@ +# Per-File Generation With Preserved Target Owning `__init__.py` and a Main Module + +This test verifies that extracting a main module from a preserved target does not +also drop a hand-written `__init__.py` source. + +With `python_generation_mode_per_file_include_init`, Gazelle adds `__init__.py` +to the per-file targets it generates. It must not remove `__init__.py` from a +preserved target that listed it explicitly, which would empty the target's srcs +and delete it. + +The preserved target gets no dependency on `:foo`: `cli.py` imports `foo`, but +dependencies are recomputed after the main module is extracted, so an import +from a source the target no longer owns does not survive in its `deps`. diff --git a/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/WORKSPACE b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/__init__.py b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/__init__.py new file mode 100644 index 0000000000..769f462dfd --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/__init__.py @@ -0,0 +1 @@ +BAR = "baz" diff --git a/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/cli.py b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/cli.py new file mode 100644 index 0000000000..07cb4bb282 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/cli.py @@ -0,0 +1,4 @@ +import foo + +if __name__ == "__main__": + print(foo) diff --git a/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/foo.py b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/test.yaml b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_init_and_main_module/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/per_file_respect_existing_with_main_module/BUILD.in b/gazelle/python/testdata/per_file_respect_existing_with_main_module/BUILD.in new file mode 100644 index 0000000000..1433adebec --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_main_module/BUILD.in @@ -0,0 +1,12 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +# Gazelle should extract bar.py into a py_binary because it has a main module. +# The preserved library should keep only its remaining valid sources. +py_library( + name = "custom", + srcs = ["bar.py", "baz.py"], + visibility = ["//visibility:private"], + tags = ["keep_me"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_with_main_module/BUILD.out b/gazelle/python/testdata/per_file_respect_existing_with_main_module/BUILD.out new file mode 100644 index 0000000000..5cdb2621d1 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_main_module/BUILD.out @@ -0,0 +1,26 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +# gazelle:python_generation_mode file + +# Gazelle should extract bar.py into a py_binary because it has a main module. +# The preserved library should keep only its remaining valid sources. +py_library( + name = "custom", + srcs = ["baz.py"], + tags = ["keep_me"], + visibility = ["//visibility:private"], + deps = [":foo"], +) + +py_binary( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], + deps = [":foo"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/per_file_respect_existing_with_main_module/README.md b/gazelle/python/testdata/per_file_respect_existing_with_main_module/README.md new file mode 100644 index 0000000000..0f3df6ae4e --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_main_module/README.md @@ -0,0 +1,8 @@ +# Per-File Generation With Preserved Target Containing a Main Module + +This test verifies that per-file generation still extracts a `py_binary` when a +preserved target contains a source with `if __name__ == "__main__":`. + +Gazelle should remove the main-module source from the preserved `py_library`, +keep the remaining sources and non-generated attributes, and generate the +matching `py_binary`. diff --git a/gazelle/python/testdata/per_file_respect_existing_with_main_module/WORKSPACE b/gazelle/python/testdata/per_file_respect_existing_with_main_module/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_main_module/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file_respect_existing_with_main_module/bar.py b/gazelle/python/testdata/per_file_respect_existing_with_main_module/bar.py new file mode 100644 index 0000000000..6b2c4bbce6 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_main_module/bar.py @@ -0,0 +1,4 @@ +import foo + +if __name__ == "__main__": + pass diff --git a/gazelle/python/testdata/per_file_respect_existing_with_main_module/baz.py b/gazelle/python/testdata/per_file_respect_existing_with_main_module/baz.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_main_module/baz.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/per_file_respect_existing_with_main_module/foo.py b/gazelle/python/testdata/per_file_respect_existing_with_main_module/foo.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_main_module/foo.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/per_file_respect_existing_with_main_module/test.yaml b/gazelle/python/testdata/per_file_respect_existing_with_main_module/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/per_file_respect_existing_with_main_module/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/BUILD.in b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/BUILD.in new file mode 100644 index 0000000000..6fb7544b59 --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/BUILD.in @@ -0,0 +1,27 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode project + +# In project mode a preserved target claims its sources even when it has only +# one, so __init__.py is left out of the generated project library below. +# Regenerating this target yields identical content, so it appears unchanged. +py_library( + name = "__init__", + srcs = ["__init__.py"], + visibility = ["//visibility:private"], +) + +# Gazelle should preserve this custom source group and prune missing sources. +py_library( + name = "custom", + srcs = ["bar.py", "baz.py", "removed.py"], + visibility = ["//visibility:private"], + tags = ["cant_touch_this"], +) + + +# Gazelle should preserve this custom test target and add generated deps. +py_test( + name = "foo_test", + srcs = ["foo_test.py"], +) diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/BUILD.out b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/BUILD.out new file mode 100644 index 0000000000..f220eba0a1 --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/BUILD.out @@ -0,0 +1,43 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +# gazelle:python_generation_mode project + +# In project mode a preserved target claims its sources even when it has only +# one, so __init__.py is left out of the generated project library below. +# Regenerating this target yields identical content, so it appears unchanged. +py_library( + name = "__init__", + srcs = ["__init__.py"], + visibility = ["//visibility:private"], +) + +# Gazelle should preserve this custom source group and prune missing sources. +py_library( + name = "custom", + srcs = [ + "bar.py", + "baz.py", + ], + tags = ["cant_touch_this"], + visibility = ["//visibility:private"], + deps = [":project_generation_mode_respect_existing_multiple_srcs"], +) + +# Gazelle should preserve this custom test target and add generated deps. +py_test( + name = "foo_test", + srcs = ["foo_test.py"], + deps = [":project_generation_mode_respect_existing_multiple_srcs"], +) + +py_library( + name = "project_generation_mode_respect_existing_multiple_srcs", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "project_generation_mode_respect_existing_multiple_srcs_test", + srcs = ["bar_test.py"], + deps = [":custom"], +) diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/README.md b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/README.md new file mode 100644 index 0000000000..d0269ba85e --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/README.md @@ -0,0 +1,14 @@ +# Project Generation With Existing Target Spanning Multiple Files + +This test verifies that project generation preserves existing non-standard +`py_library` and `py_test` targets while still generating project-wide targets +for unclaimed sources. + +Gazelle should prune sources that no longer exist, keep non-generated +attributes, and add generated dependencies. + +Unlike the other generation modes, project mode has a single generated library +for the whole tree, so every preserved target claims its sources even when it +has only one. That is why `__init__.py` is absent from the generated project +library, and why the existing `__init__` target appears unchanged: regenerating +it produces identical content. diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/WORKSPACE b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/__init__.py b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/bar.py b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/bar.py new file mode 100644 index 0000000000..6b58ff30a8 --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/bar.py @@ -0,0 +1 @@ +# For test purposes only. diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/bar_test.py b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/bar_test.py new file mode 100644 index 0000000000..b6b8723822 --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/bar_test.py @@ -0,0 +1 @@ +import bar diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/baz.py b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/baz.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/baz.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/foo.py b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/foo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/foo_test.py b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/foo_test.py new file mode 100644 index 0000000000..ddf557475a --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/foo_test.py @@ -0,0 +1 @@ +import foo diff --git a/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/test.yaml b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/project_generation_mode_respect_existing_multiple_srcs/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/simple_binary_with_library/BUILD.in b/gazelle/python/testdata/simple_binary_with_library/BUILD.in index b60e84f17e..1b3869119f 100644 --- a/gazelle/python/testdata/simple_binary_with_library/BUILD.in +++ b/gazelle/python/testdata/simple_binary_with_library/BUILD.in @@ -9,7 +9,7 @@ py_library( ], ) -# This target should be kept unmodified by Gazelle. +# Gazelle should keep this custom target unmodified, with bar.py in both libraries. py_library( name = "custom", srcs = [ diff --git a/gazelle/python/testdata/simple_binary_with_library/BUILD.out b/gazelle/python/testdata/simple_binary_with_library/BUILD.out index eddc15cacd..e97dc02b4f 100644 --- a/gazelle/python/testdata/simple_binary_with_library/BUILD.out +++ b/gazelle/python/testdata/simple_binary_with_library/BUILD.out @@ -10,7 +10,7 @@ py_library( visibility = ["//:__subpackages__"], ) -# This target should be kept unmodified by Gazelle. +# Gazelle should keep this custom target unmodified, with bar.py in both libraries. py_library( name = "custom", srcs = [ diff --git a/gazelle/python/testdata/simple_binary_with_library/README.md b/gazelle/python/testdata/simple_binary_with_library/README.md index cfc81a3581..c6d8af7f85 100644 --- a/gazelle/python/testdata/simple_binary_with_library/README.md +++ b/gazelle/python/testdata/simple_binary_with_library/README.md @@ -2,3 +2,9 @@ This test case asserts that a simple `py_binary` is generated as expected referencing a `py_library`. + +The existing custom `py_library` shares `bar.py` with the generated package +library. Gazelle should preserve that shared source in both targets and leave +the custom target otherwise untouched: it gets no `visibility`, because +injecting one would widen a hand-written target that relies on the default +private visibility. diff --git a/gazelle/pythonconfig/BUILD.bazel b/gazelle/pythonconfig/BUILD.bazel index a82a7f19ca..c1db45f40f 100644 --- a/gazelle/pythonconfig/BUILD.bazel +++ b/gazelle/pythonconfig/BUILD.bazel @@ -17,8 +17,12 @@ go_library( go_test( name = "pythonconfig_test", - srcs = ["pythonconfig_test.go"], + srcs = [ + "pythonconfig_test.go", + "pythonconfig_visibility_test.go", + ], embed = [":pythonconfig"], + deps = ["@com_github_stretchr_testify//assert"], ) filegroup( diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go index c88d59abcf..18c5213cf6 100644 --- a/gazelle/pythonconfig/pythonconfig.go +++ b/gazelle/pythonconfig/pythonconfig.go @@ -537,8 +537,30 @@ func (c *Config) RenderProtoName(protoName string) string { return strings.ReplaceAll(c.protoNamingConvention, protoNameNamingConventionSubstitution, strings.TrimSuffix(protoName, "_proto")) } +// nonEmptyVisibilityLabels returns labels with empty and whitespace-only entries +// removed. Comma-separated python_default_visibility directives may include a +// trailing comma, which strings.Split turns into an empty label. +func nonEmptyVisibilityLabels(labels []string) []string { + if len(labels) == 0 { + return labels + } + filtered := make([]string, 0, len(labels)) + for _, label := range labels { + label = strings.TrimSpace(label) + if label == "" { + continue + } + filtered = append(filtered, label) + } + return filtered +} + // AppendVisibility adds additional items to the target's visibility. func (c *Config) AppendVisibility(visibility string) { + visibility = strings.TrimSpace(visibility) + if visibility == "" { + return + } c.visibility = append(c.visibility, visibility) } @@ -549,7 +571,7 @@ func (c *Config) Visibility() []string { // SetDefaultVisibility sets the default visibility of the target. func (c *Config) SetDefaultVisibility(visibility []string) { - c.defaultVisibility = visibility + c.defaultVisibility = nonEmptyVisibilityLabels(visibility) } // DefaultVisibilty returns the target's default visibility. diff --git a/gazelle/pythonconfig/pythonconfig_visibility_test.go b/gazelle/pythonconfig/pythonconfig_visibility_test.go new file mode 100644 index 0000000000..7e74aabae1 --- /dev/null +++ b/gazelle/pythonconfig/pythonconfig_visibility_test.go @@ -0,0 +1,24 @@ +package pythonconfig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNonEmptyVisibilityLabels(t *testing.T) { + assert.Equal(t, []string{"//a:b", "//c:d"}, nonEmptyVisibilityLabels([]string{ + "//a:b", + "", + " //c:d ", + "", + })) + assert.Nil(t, nonEmptyVisibilityLabels(nil)) + assert.Empty(t, nonEmptyVisibilityLabels([]string{"", " "})) +} + +func TestSetDefaultVisibilityDropsEmptyLabels(t *testing.T) { + cfg := New("", "") + cfg.SetDefaultVisibility([]string{"//benchling:__subpackages__", ""}) + assert.Equal(t, []string{"//benchling:__subpackages__"}, cfg.Visibility()) +} diff --git a/news/gazelle-pytest-conftest.fixed.md b/news/gazelle-pytest-conftest.fixed.md new file mode 100644 index 0000000000..532e13012f --- /dev/null +++ b/news/gazelle-pytest-conftest.fixed.md @@ -0,0 +1,2 @@ +(gazelle) Made `include_pytest_conftest` annotations deterministic for +multi-source tests and report conflicting explicit values. diff --git a/news/preserve_existing_python_targets.changed.md b/news/preserve_existing_python_targets.changed.md new file mode 100644 index 0000000000..61efa61372 --- /dev/null +++ b/news/preserve_existing_python_targets.changed.md @@ -0,0 +1,8 @@ +(gazelle) Existing hand-written {obj}`py_library` and {obj}`py_test` targets +that own sources Gazelle manages can now be regenerated in place. Gazelle +manages their `srcs` and dependency attributes, and usually keeps their sources +out of generated targets; a single-source library remains shared outside +project mode. Dependencies not derived from imports are removed unless marked +with `# keep`. Add `# keep` above the rule to opt out. Rules named after a +generated target, and rules listing `__main__.py`, `__test__.py`, or +`conftest.py`, are excluded from this preservation behavior.