diff options
Diffstat (limited to 'gazelle/pythonconfig/pythonconfig.go')
-rw-r--r-- | gazelle/pythonconfig/pythonconfig.go | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go new file mode 100644 index 0000000..c7cd7c1 --- /dev/null +++ b/gazelle/pythonconfig/pythonconfig.go @@ -0,0 +1,361 @@ +// 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 pythonconfig + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/emirpasic/gods/lists/singlylinkedlist" + + "github.com/bazelbuild/bazel-gazelle/label" + "github.com/bazelbuild/rules_python/gazelle/manifest" +) + +// Directives +const ( + // PythonExtensionDirective represents the directive that controls whether + // this Python extension is enabled or not. Sub-packages inherit this value. + // Can be either "enabled" or "disabled". Defaults to "enabled". + PythonExtensionDirective = "python_extension" + // PythonRootDirective represents the directive that sets a Bazel package as + // a Python root. This is used on monorepos with multiple Python projects + // that don't share the top-level of the workspace as the root. + PythonRootDirective = "python_root" + // PythonManifestFileNameDirective represents the directive that overrides + // the default gazelle_python.yaml manifest file name. + PythonManifestFileNameDirective = "python_manifest_file_name" + // IgnoreFilesDirective represents the directive that controls the ignored + // files from the generated targets. + IgnoreFilesDirective = "python_ignore_files" + // IgnoreDependenciesDirective represents the directive that controls the + // ignored dependencies from the generated targets. + IgnoreDependenciesDirective = "python_ignore_dependencies" + // ValidateImportStatementsDirective represents the directive that controls + // whether the Python import statements should be validated. + ValidateImportStatementsDirective = "python_validate_import_statements" + // GenerationMode represents the directive that controls the target generation + // mode. See below for the GenerationModeType constants. + GenerationMode = "python_generation_mode" + // LibraryNamingConvention represents the directive that controls the + // py_library naming convention. It interpolates $package_name$ with the + // Bazel package name. E.g. if the Bazel package name is `foo`, setting this + // to `$package_name$_my_lib` would render to `foo_my_lib`. + LibraryNamingConvention = "python_library_naming_convention" + // BinaryNamingConvention represents the directive that controls the + // py_binary naming convention. See python_library_naming_convention for + // more info on the package name interpolation. + BinaryNamingConvention = "python_binary_naming_convention" + // TestNamingConvention represents the directive that controls the py_test + // naming convention. See python_library_naming_convention for more info on + // the package name interpolation. + TestNamingConvention = "python_test_naming_convention" +) + +// GenerationModeType represents one of the generation modes for the Python +// extension. +type GenerationModeType string + +// Generation modes +const ( + // GenerationModePackage defines the mode in which targets will be generated + // for each __init__.py, or when an existing BUILD or BUILD.bazel file already + // determines a Bazel package. + GenerationModePackage GenerationModeType = "package" + // GenerationModeProject defines the mode in which a coarse-grained target will + // be generated englobing sub-directories containing Python files. + GenerationModeProject GenerationModeType = "project" +) + +const ( + packageNameNamingConventionSubstitution = "$package_name$" +) + +// defaultIgnoreFiles is the list of default values used in the +// python_ignore_files option. +var defaultIgnoreFiles = map[string]struct{}{ + "setup.py": {}, +} + +func SanitizeDistribution(distributionName string) string { + sanitizedDistribution := strings.ToLower(distributionName) + sanitizedDistribution = strings.ReplaceAll(sanitizedDistribution, "-", "_") + sanitizedDistribution = strings.ReplaceAll(sanitizedDistribution, ".", "_") + + return sanitizedDistribution +} + +// Configs is an extension of map[string]*Config. It provides finding methods +// on top of the mapping. +type Configs map[string]*Config + +// ParentForPackage returns the parent Config for the given Bazel package. +func (c *Configs) ParentForPackage(pkg string) *Config { + dir := filepath.Dir(pkg) + if dir == "." { + dir = "" + } + parent := (map[string]*Config)(*c)[dir] + return parent +} + +// Config represents a config extension for a specific Bazel package. +type Config struct { + parent *Config + + extensionEnabled bool + repoRoot string + pythonProjectRoot string + gazelleManifest *manifest.Manifest + + excludedPatterns *singlylinkedlist.List + ignoreFiles map[string]struct{} + ignoreDependencies map[string]struct{} + validateImportStatements bool + coarseGrainedGeneration bool + libraryNamingConvention string + binaryNamingConvention string + testNamingConvention string +} + +// New creates a new Config. +func New( + repoRoot string, + pythonProjectRoot string, +) *Config { + return &Config{ + extensionEnabled: true, + repoRoot: repoRoot, + pythonProjectRoot: pythonProjectRoot, + excludedPatterns: singlylinkedlist.New(), + ignoreFiles: make(map[string]struct{}), + ignoreDependencies: make(map[string]struct{}), + validateImportStatements: true, + coarseGrainedGeneration: false, + libraryNamingConvention: packageNameNamingConventionSubstitution, + binaryNamingConvention: fmt.Sprintf("%s_bin", packageNameNamingConventionSubstitution), + testNamingConvention: fmt.Sprintf("%s_test", packageNameNamingConventionSubstitution), + } +} + +// Parent returns the parent config. +func (c *Config) Parent() *Config { + return c.parent +} + +// NewChild creates a new child Config. It inherits desired values from the +// current Config and sets itself as the parent to the child. +func (c *Config) NewChild() *Config { + return &Config{ + parent: c, + extensionEnabled: c.extensionEnabled, + repoRoot: c.repoRoot, + pythonProjectRoot: c.pythonProjectRoot, + excludedPatterns: c.excludedPatterns, + ignoreFiles: make(map[string]struct{}), + ignoreDependencies: make(map[string]struct{}), + validateImportStatements: c.validateImportStatements, + coarseGrainedGeneration: c.coarseGrainedGeneration, + libraryNamingConvention: c.libraryNamingConvention, + binaryNamingConvention: c.binaryNamingConvention, + testNamingConvention: c.testNamingConvention, + } +} + +// AddExcludedPattern adds a glob pattern parsed from the standard +// gazelle:exclude directive. +func (c *Config) AddExcludedPattern(pattern string) { + c.excludedPatterns.Add(pattern) +} + +// ExcludedPatterns returns the excluded patterns list. +func (c *Config) ExcludedPatterns() *singlylinkedlist.List { + return c.excludedPatterns +} + +// SetExtensionEnabled sets whether the extension is enabled or not. +func (c *Config) SetExtensionEnabled(enabled bool) { + c.extensionEnabled = enabled +} + +// ExtensionEnabled returns whether the extension is enabled or not. +func (c *Config) ExtensionEnabled() bool { + return c.extensionEnabled +} + +// SetPythonProjectRoot sets the Python project root. +func (c *Config) SetPythonProjectRoot(pythonProjectRoot string) { + c.pythonProjectRoot = pythonProjectRoot +} + +// PythonProjectRoot returns the Python project root. +func (c *Config) PythonProjectRoot() string { + return c.pythonProjectRoot +} + +// SetGazelleManifest sets the Gazelle manifest parsed from the +// gazelle_python.yaml file. +func (c *Config) SetGazelleManifest(gazelleManifest *manifest.Manifest) { + c.gazelleManifest = gazelleManifest +} + +// FindThirdPartyDependency scans the gazelle manifests for the current config +// and the parent configs up to the root finding if it can resolve the module +// name. +func (c *Config) FindThirdPartyDependency(modName string) (string, bool) { + for currentCfg := c; currentCfg != nil; currentCfg = currentCfg.parent { + if currentCfg.gazelleManifest != nil { + gazelleManifest := currentCfg.gazelleManifest + if distributionName, ok := gazelleManifest.ModulesMapping[modName]; ok { + var distributionRepositoryName string + if gazelleManifest.PipDepsRepositoryName != "" { + distributionRepositoryName = gazelleManifest.PipDepsRepositoryName + } else if gazelleManifest.PipRepository != nil { + distributionRepositoryName = gazelleManifest.PipRepository.Name + } + sanitizedDistribution := SanitizeDistribution(distributionName) + + if gazelleManifest.PipRepository != nil && gazelleManifest.PipRepository.UsePipRepositoryAliases { + // @<repository_name>//<distribution_name> + lbl := label.New(distributionRepositoryName, sanitizedDistribution, sanitizedDistribution) + return lbl.String(), true + } + + // @<repository_name>_<distribution_name>//:pkg + distributionRepositoryName = distributionRepositoryName + "_" + sanitizedDistribution + lbl := label.New(distributionRepositoryName, "", "pkg") + return lbl.String(), true + } + } + } + return "", false +} + +// AddIgnoreFile adds a file to the list of ignored files for a given package. +// Adding an ignored file to a package also makes it ignored on a subpackage. +func (c *Config) AddIgnoreFile(file string) { + c.ignoreFiles[strings.TrimSpace(file)] = struct{}{} +} + +// IgnoresFile checks if a file is ignored in the given package or in one of the +// parent packages up to the workspace root. +func (c *Config) IgnoresFile(file string) bool { + trimmedFile := strings.TrimSpace(file) + + if _, ignores := defaultIgnoreFiles[trimmedFile]; ignores { + return true + } + + if _, ignores := c.ignoreFiles[trimmedFile]; ignores { + return true + } + + parent := c.parent + for parent != nil { + if _, ignores := parent.ignoreFiles[trimmedFile]; ignores { + return true + } + parent = parent.parent + } + + return false +} + +// AddIgnoreDependency adds a dependency to the list of ignored dependencies for +// a given package. Adding an ignored dependency to a package also makes it +// ignored on a subpackage. +func (c *Config) AddIgnoreDependency(dep string) { + c.ignoreDependencies[strings.TrimSpace(dep)] = struct{}{} +} + +// IgnoresDependency checks if a dependency is ignored in the given package or +// in one of the parent packages up to the workspace root. +func (c *Config) IgnoresDependency(dep string) bool { + trimmedDep := strings.TrimSpace(dep) + + if _, ignores := c.ignoreDependencies[trimmedDep]; ignores { + return true + } + + parent := c.parent + for parent != nil { + if _, ignores := parent.ignoreDependencies[trimmedDep]; ignores { + return true + } + parent = parent.parent + } + + return false +} + +// SetValidateImportStatements sets whether Python import statements should be +// validated or not. It throws an error if this is set multiple times, i.e. if +// the directive is specified multiple times in the Bazel workspace. +func (c *Config) SetValidateImportStatements(validate bool) { + c.validateImportStatements = validate +} + +// ValidateImportStatements returns whether the Python import statements should +// be validated or not. If this option was not explicitly specified by the user, +// it defaults to true. +func (c *Config) ValidateImportStatements() bool { + return c.validateImportStatements +} + +// SetCoarseGrainedGeneration sets whether coarse-grained targets should be +// generated or not. +func (c *Config) SetCoarseGrainedGeneration(coarseGrained bool) { + c.coarseGrainedGeneration = coarseGrained +} + +// CoarseGrainedGeneration returns whether coarse-grained targets should be +// generated or not. +func (c *Config) CoarseGrainedGeneration() bool { + return c.coarseGrainedGeneration +} + +// SetLibraryNamingConvention sets the py_library target naming convention. +func (c *Config) SetLibraryNamingConvention(libraryNamingConvention string) { + c.libraryNamingConvention = libraryNamingConvention +} + +// RenderLibraryName returns the py_library target name by performing all +// substitutions. +func (c *Config) RenderLibraryName(packageName string) string { + return strings.ReplaceAll(c.libraryNamingConvention, packageNameNamingConventionSubstitution, packageName) +} + +// SetBinaryNamingConvention sets the py_binary target naming convention. +func (c *Config) SetBinaryNamingConvention(binaryNamingConvention string) { + c.binaryNamingConvention = binaryNamingConvention +} + +// RenderBinaryName returns the py_binary target name by performing all +// substitutions. +func (c *Config) RenderBinaryName(packageName string) string { + return strings.ReplaceAll(c.binaryNamingConvention, packageNameNamingConventionSubstitution, packageName) +} + +// SetTestNamingConvention sets the py_test target naming convention. +func (c *Config) SetTestNamingConvention(testNamingConvention string) { + c.testNamingConvention = testNamingConvention +} + +// RenderTestName returns the py_test target name by performing all +// substitutions. +func (c *Config) RenderTestName(packageName string) string { + return strings.ReplaceAll(c.testNamingConvention, packageNameNamingConventionSubstitution, packageName) +} |