| // 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. |
| |
| package python |
| |
| import ( |
| "flag" |
| "fmt" |
| "log" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "github.com/bazelbuild/bazel-gazelle/config" |
| "github.com/bazelbuild/bazel-gazelle/rule" |
| "github.com/bmatcuk/doublestar/v4" |
| |
| "github.com/bazelbuild/rules_python/gazelle/manifest" |
| "github.com/bazelbuild/rules_python/gazelle/pythonconfig" |
| ) |
| |
| // Configurer satisfies the config.Configurer interface. It's the |
| // language-specific configuration extension. |
| type Configurer struct{} |
| |
| // RegisterFlags registers command-line flags used by the extension. This |
| // method is called once with the root configuration when Gazelle |
| // starts. RegisterFlags may set an initial values in Config.Exts. When flags |
| // are set, they should modify these values. |
| func (py *Configurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {} |
| |
| // CheckFlags validates the configuration after command line flags are parsed. |
| // This is called once with the root configuration when Gazelle starts. |
| // CheckFlags may set default values in flags or make implied changes. |
| func (py *Configurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error { |
| return nil |
| } |
| |
| // KnownDirectives returns a list of directive keys that this Configurer can |
| // interpret. Gazelle prints errors for directives that are not recoginized by |
| // any Configurer. |
| func (py *Configurer) KnownDirectives() []string { |
| return []string{ |
| pythonconfig.PythonExtensionDirective, |
| pythonconfig.PythonRootDirective, |
| pythonconfig.PythonManifestFileNameDirective, |
| pythonconfig.IgnoreFilesDirective, |
| pythonconfig.IgnoreDependenciesDirective, |
| pythonconfig.ValidateImportStatementsDirective, |
| pythonconfig.GenerationMode, |
| pythonconfig.GenerationModePerFileIncludeInit, |
| pythonconfig.GenerationModePerPackageRequireTestEntryPoint, |
| pythonconfig.LibraryNamingConvention, |
| pythonconfig.BinaryNamingConvention, |
| pythonconfig.TestNamingConvention, |
| pythonconfig.DefaultVisibilty, |
| pythonconfig.Visibility, |
| pythonconfig.TestFilePattern, |
| pythonconfig.LabelConvention, |
| pythonconfig.LabelNormalization, |
| } |
| } |
| |
| // Configure modifies the configuration using directives and other information |
| // extracted from a build file. Configure is called in each directory. |
| // |
| // c is the configuration for the current directory. It starts out as a copy |
| // of the configuration for the parent directory. |
| // |
| // rel is the slash-separated relative path from the repository root to |
| // the current directory. It is "" for the root directory itself. |
| // |
| // f is the build file for the current directory or nil if there is no |
| // existing build file. |
| func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { |
| // Create the root config. |
| if _, exists := c.Exts[languageName]; !exists { |
| rootConfig := pythonconfig.New(c.RepoRoot, "") |
| c.Exts[languageName] = pythonconfig.Configs{"": rootConfig} |
| } |
| |
| configs := c.Exts[languageName].(pythonconfig.Configs) |
| |
| config, exists := configs[rel] |
| if !exists { |
| parent := configs.ParentForPackage(rel) |
| config = parent.NewChild() |
| configs[rel] = config |
| } |
| |
| if f == nil { |
| return |
| } |
| |
| gazelleManifestFilename := "gazelle_python.yaml" |
| |
| for _, d := range f.Directives { |
| switch d.Key { |
| case "exclude": |
| // We record the exclude directive for coarse-grained packages |
| // since we do manual tree traversal in this mode. |
| config.AddExcludedPattern(filepath.Join(rel, strings.TrimSpace(d.Value))) |
| case pythonconfig.PythonExtensionDirective: |
| switch d.Value { |
| case "enabled": |
| config.SetExtensionEnabled(true) |
| case "disabled": |
| config.SetExtensionEnabled(false) |
| default: |
| err := fmt.Errorf("invalid value for directive %q: %s: possible values are enabled/disabled", |
| pythonconfig.PythonExtensionDirective, d.Value) |
| log.Fatal(err) |
| } |
| case pythonconfig.PythonRootDirective: |
| config.SetPythonProjectRoot(rel) |
| config.SetDefaultVisibility([]string{fmt.Sprintf(pythonconfig.DefaultVisibilityFmtString, rel)}) |
| case pythonconfig.PythonManifestFileNameDirective: |
| gazelleManifestFilename = strings.TrimSpace(d.Value) |
| case pythonconfig.IgnoreFilesDirective: |
| for _, ignoreFile := range strings.Split(d.Value, ",") { |
| config.AddIgnoreFile(ignoreFile) |
| } |
| case pythonconfig.IgnoreDependenciesDirective: |
| for _, ignoreDependency := range strings.Split(d.Value, ",") { |
| config.AddIgnoreDependency(ignoreDependency) |
| } |
| case pythonconfig.ValidateImportStatementsDirective: |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) |
| if err != nil { |
| log.Fatal(err) |
| } |
| config.SetValidateImportStatements(v) |
| case pythonconfig.GenerationMode: |
| switch pythonconfig.GenerationModeType(strings.TrimSpace(d.Value)) { |
| case pythonconfig.GenerationModePackage: |
| config.SetCoarseGrainedGeneration(false) |
| config.SetPerFileGeneration(false) |
| case pythonconfig.GenerationModeFile: |
| config.SetCoarseGrainedGeneration(false) |
| config.SetPerFileGeneration(true) |
| case pythonconfig.GenerationModeProject: |
| config.SetCoarseGrainedGeneration(true) |
| config.SetPerFileGeneration(false) |
| default: |
| err := fmt.Errorf("invalid value for directive %q: %s", |
| pythonconfig.GenerationMode, d.Value) |
| log.Fatal(err) |
| } |
| case pythonconfig.GenerationModePerFileIncludeInit: |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) |
| if err != nil { |
| log.Fatal(err) |
| } |
| config.SetPerFileGenerationIncludeInit(v) |
| case pythonconfig.GenerationModePerPackageRequireTestEntryPoint: |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) |
| if err != nil { |
| log.Printf("invalid value for gazelle:%s in %q: %q", |
| pythonconfig.GenerationModePerPackageRequireTestEntryPoint, rel, d.Value) |
| } else { |
| config.SetPerPackageGenerationRequireTestEntryPoint(v) |
| } |
| case pythonconfig.LibraryNamingConvention: |
| config.SetLibraryNamingConvention(strings.TrimSpace(d.Value)) |
| case pythonconfig.BinaryNamingConvention: |
| config.SetBinaryNamingConvention(strings.TrimSpace(d.Value)) |
| case pythonconfig.TestNamingConvention: |
| config.SetTestNamingConvention(strings.TrimSpace(d.Value)) |
| case pythonconfig.DefaultVisibilty: |
| switch directiveArg := strings.TrimSpace(d.Value); directiveArg { |
| case "NONE": |
| config.SetDefaultVisibility([]string{}) |
| case "DEFAULT": |
| pythonProjectRoot := config.PythonProjectRoot() |
| defaultVisibility := fmt.Sprintf(pythonconfig.DefaultVisibilityFmtString, pythonProjectRoot) |
| config.SetDefaultVisibility([]string{defaultVisibility}) |
| default: |
| // Handle injecting the python root. Assume that the user used the |
| // exact string "$python_root$". |
| labels := strings.ReplaceAll(directiveArg, "$python_root$", config.PythonProjectRoot()) |
| config.SetDefaultVisibility(strings.Split(labels, ",")) |
| } |
| case pythonconfig.Visibility: |
| labels := strings.ReplaceAll(strings.TrimSpace(d.Value), "$python_root$", config.PythonProjectRoot()) |
| config.AppendVisibility(labels) |
| case pythonconfig.TestFilePattern: |
| value := strings.TrimSpace(d.Value) |
| if value == "" { |
| log.Fatal("directive 'python_test_file_pattern' requires a value") |
| } |
| globStrings := strings.Split(value, ",") |
| for _, g := range globStrings { |
| if !doublestar.ValidatePattern(g) { |
| log.Fatalf("invalid glob pattern '%s'", g) |
| } |
| } |
| config.SetTestFilePattern(globStrings) |
| case pythonconfig.LabelConvention: |
| value := strings.TrimSpace(d.Value) |
| if value == "" { |
| log.Fatalf("directive '%s' requires a value", pythonconfig.LabelConvention) |
| } |
| config.SetLabelConvention(value) |
| case pythonconfig.LabelNormalization: |
| switch directiveArg := strings.ToLower(strings.TrimSpace(d.Value)); directiveArg { |
| case "pep503": |
| config.SetLabelNormalization(pythonconfig.Pep503LabelNormalizationType) |
| case "none": |
| config.SetLabelNormalization(pythonconfig.NoLabelNormalizationType) |
| case "snake_case": |
| config.SetLabelNormalization(pythonconfig.SnakeCaseLabelNormalizationType) |
| default: |
| config.SetLabelNormalization(pythonconfig.DefaultLabelNormalizationType) |
| } |
| } |
| } |
| |
| gazelleManifestPath := filepath.Join(c.RepoRoot, rel, gazelleManifestFilename) |
| gazelleManifest, err := py.loadGazelleManifest(gazelleManifestPath) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if gazelleManifest != nil { |
| config.SetGazelleManifest(gazelleManifest) |
| } |
| } |
| |
| func (py *Configurer) loadGazelleManifest(gazelleManifestPath string) (*manifest.Manifest, error) { |
| if _, err := os.Stat(gazelleManifestPath); err != nil { |
| if os.IsNotExist(err) { |
| return nil, nil |
| } |
| return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) |
| } |
| manifestFile := new(manifest.File) |
| if err := manifestFile.Decode(gazelleManifestPath); err != nil { |
| return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) |
| } |
| return manifestFile.Manifest, nil |
| } |