| // 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" | |
| "path/filepath" | |
| "strconv" | |
| "strings" | |
| "github.com/bazelbuild/bazel-gazelle/config" | |
| "github.com/bazelbuild/bazel-gazelle/rule" | |
| "github.com/bmatcuk/doublestar/v4" | |
| "github.com/bazel-contrib/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.ProtoNamingConvention, | |
| pythonconfig.DefaultVisibilty, | |
| pythonconfig.Visibility, | |
| pythonconfig.TestFilePattern, | |
| pythonconfig.LabelConvention, | |
| pythonconfig.LabelNormalization, | |
| pythonconfig.GeneratePyiDeps, | |
| pythonconfig.GeneratePyiSrcs, | |
| pythonconfig.ExperimentalAllowRelativeImports, | |
| pythonconfig.GenerateProto, | |
| pythonconfig.PythonResolveSiblingImports, | |
| pythonconfig.PythonIncludeAncestorConftest, | |
| } | |
| } | |
| // 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.ProtoNamingConvention: | |
| config.SetProtoNamingConvention(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) | |
| } | |
| case pythonconfig.ExperimentalAllowRelativeImports: | |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | |
| if err != nil { | |
| log.Printf("invalid value for gazelle:%s in %q: %q", | |
| pythonconfig.ExperimentalAllowRelativeImports, rel, d.Value) | |
| } | |
| config.SetExperimentalAllowRelativeImports(v) | |
| case pythonconfig.GeneratePyiDeps: | |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| config.SetGeneratePyiDeps(v) | |
| case pythonconfig.GeneratePyiSrcs: | |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| config.SetGeneratePyiSrcs(v) | |
| case pythonconfig.GenerateProto: | |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| config.SetGenerateProto(v) | |
| case pythonconfig.PythonResolveSiblingImports: | |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| config.SetResolveSiblingImports(v) | |
| case pythonconfig.PythonIncludeAncestorConftest: | |
| v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| config.SetIncludeAncestorConftest(v) | |
| } | |
| } | |
| gazelleManifestPath := filepath.Join(c.RepoRoot, rel, gazelleManifestFilename) | |
| config.SetGazelleManifestPath(gazelleManifestPath) | |
| } |