| // 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" | |
| "regexp" | |
| "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" | |
| // GenerationModePerFileIncludeInit represents the directive that augments | |
| // the "per_file" GenerationMode by including the package's __init__.py file. | |
| // This is a boolean directive. | |
| GenerationModePerFileIncludeInit = "python_generation_mode_per_file_include_init" | |
| // 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" | |
| // DefaultVisibilty represents the directive that controls what visibility | |
| // labels are added to generated python targets. | |
| DefaultVisibilty = "python_default_visibility" | |
| // Visibility represents the directive that controls what additional | |
| // visibility labels are added to generated targets. It mimics the behavior | |
| // of the `go_visibility` directive. | |
| Visibility = "python_visibility" | |
| // TestFilePattern represents the directive that controls which python | |
| // files are mapped to `py_test` targets. | |
| TestFilePattern = "python_test_file_pattern" | |
| // LabelConvention represents the directive that defines the format of the | |
| // labels to third-party dependencies. | |
| LabelConvention = "python_label_convention" | |
| // LabelNormalization represents the directive that controls how distribution | |
| // names of labels to third-party dependencies are normalized. Supported values | |
| // are 'none', 'pep503' and 'snake_case' (default). See LabelNormalizationType. | |
| LabelNormalization = "python_label_normalization" | |
| ) | |
| // 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" | |
| GenerationModeFile GenerationModeType = "file" | |
| ) | |
| const ( | |
| packageNameNamingConventionSubstitution = "$package_name$" | |
| distributionNameLabelConventionSubstitution = "$distribution_name$" | |
| ) | |
| const ( | |
| // The default visibility label, including a format placeholder for `python_root`. | |
| DefaultVisibilityFmtString = "//%s:__subpackages__" | |
| // The default globs used to determine pt_test targets. | |
| DefaultTestFilePatternString = "*_test.py,test_*.py" | |
| // The default convention of label of third-party dependencies. | |
| DefaultLabelConvention = "$distribution_name$" | |
| // The default normalization applied to distribution names of third-party dependency labels. | |
| DefaultLabelNormalizationType = SnakeCaseLabelNormalizationType | |
| ) | |
| // defaultIgnoreFiles is the list of default values used in the | |
| // python_ignore_files option. | |
| var defaultIgnoreFiles = map[string]struct{}{ | |
| "setup.py": {}, | |
| } | |
| // 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 := path.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 | |
| perFileGeneration bool | |
| perFileGenerationIncludeInit bool | |
| libraryNamingConvention string | |
| binaryNamingConvention string | |
| testNamingConvention string | |
| defaultVisibility []string | |
| visibility []string | |
| testFilePattern []string | |
| labelConvention string | |
| labelNormalization LabelNormalizationType | |
| } | |
| type LabelNormalizationType int | |
| const ( | |
| NoLabelNormalizationType LabelNormalizationType = iota | |
| Pep503LabelNormalizationType | |
| SnakeCaseLabelNormalizationType | |
| ) | |
| // 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, | |
| perFileGeneration: false, | |
| perFileGenerationIncludeInit: false, | |
| libraryNamingConvention: packageNameNamingConventionSubstitution, | |
| binaryNamingConvention: fmt.Sprintf("%s_bin", packageNameNamingConventionSubstitution), | |
| testNamingConvention: fmt.Sprintf("%s_test", packageNameNamingConventionSubstitution), | |
| defaultVisibility: []string{fmt.Sprintf(DefaultVisibilityFmtString, "")}, | |
| visibility: []string{}, | |
| testFilePattern: strings.Split(DefaultTestFilePatternString, ","), | |
| labelConvention: DefaultLabelConvention, | |
| labelNormalization: DefaultLabelNormalizationType, | |
| } | |
| } | |
| // 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, | |
| perFileGeneration: c.perFileGeneration, | |
| perFileGenerationIncludeInit: c.perFileGenerationIncludeInit, | |
| libraryNamingConvention: c.libraryNamingConvention, | |
| binaryNamingConvention: c.binaryNamingConvention, | |
| testNamingConvention: c.testNamingConvention, | |
| defaultVisibility: c.defaultVisibility, | |
| visibility: c.visibility, | |
| testFilePattern: c.testFilePattern, | |
| labelConvention: c.labelConvention, | |
| labelNormalization: c.labelNormalization, | |
| } | |
| } | |
| // 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 | |
| } | |
| lbl := currentCfg.FormatThirdPartyDependency(distributionRepositoryName, distributionName) | |
| 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 | |
| } | |
| // SetPerFileGneration sets whether a separate py_library target should be | |
| // generated for each file. | |
| func (c *Config) SetPerFileGeneration(perFile bool) { | |
| c.perFileGeneration = perFile | |
| } | |
| // PerFileGeneration returns whether a separate py_library target should be | |
| // generated for each file. | |
| func (c *Config) PerFileGeneration() bool { | |
| return c.perFileGeneration | |
| } | |
| // SetPerFileGenerationIncludeInit sets whether py_library targets should | |
| // include __init__.py files when PerFileGeneration() is true. | |
| func (c *Config) SetPerFileGenerationIncludeInit(includeInit bool) { | |
| c.perFileGenerationIncludeInit = includeInit | |
| } | |
| // PerFileGenerationIncludeInit returns whether py_library targets should | |
| // include __init__.py files when PerFileGeneration() is true. | |
| func (c *Config) PerFileGenerationIncludeInit() bool { | |
| return c.perFileGenerationIncludeInit | |
| } | |
| // 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) | |
| } | |
| // AppendVisibility adds additional items to the target's visibility. | |
| func (c *Config) AppendVisibility(visibility string) { | |
| c.visibility = append(c.visibility, visibility) | |
| } | |
| // Visibility returns the target's visibility. | |
| func (c *Config) Visibility() []string { | |
| return append(c.defaultVisibility, c.visibility...) | |
| } | |
| // SetDefaultVisibility sets the default visibility of the target. | |
| func (c *Config) SetDefaultVisibility(visibility []string) { | |
| c.defaultVisibility = visibility | |
| } | |
| // DefaultVisibilty returns the target's default visibility. | |
| func (c *Config) DefaultVisibilty() []string { | |
| return c.defaultVisibility | |
| } | |
| // SetTestFilePattern sets the file patterns that should be mapped to 'py_test' rules. | |
| func (c *Config) SetTestFilePattern(patterns []string) { | |
| c.testFilePattern = patterns | |
| } | |
| // TestFilePattern returns the patterns that should be mapped to 'py_test' rules. | |
| func (c *Config) TestFilePattern() []string { | |
| return c.testFilePattern | |
| } | |
| // SetLabelConvention sets the label convention used for third-party dependencies. | |
| func (c *Config) SetLabelConvention(convention string) { | |
| c.labelConvention = convention | |
| } | |
| // LabelConvention returns the label convention used for third-party dependencies. | |
| func (c *Config) LabelConvention() string { | |
| return c.labelConvention | |
| } | |
| // SetLabelConvention sets the label normalization applied to distribution names of third-party dependencies. | |
| func (c *Config) SetLabelNormalization(normalizationType LabelNormalizationType) { | |
| c.labelNormalization = normalizationType | |
| } | |
| // LabelConvention returns the label normalization applied to distribution names of third-party dependencies. | |
| func (c *Config) LabelNormalization() LabelNormalizationType { | |
| return c.labelNormalization | |
| } | |
| // FormatThirdPartyDependency returns a label to a third-party dependency performing all formating and normalization. | |
| func (c *Config) FormatThirdPartyDependency(repositoryName string, distributionName string) label.Label { | |
| conventionalDistributionName := strings.ReplaceAll(c.labelConvention, distributionNameLabelConventionSubstitution, distributionName) | |
| var normConventionalDistributionName string | |
| switch norm := c.LabelNormalization(); norm { | |
| case SnakeCaseLabelNormalizationType: | |
| // See /python/private/normalize_name.bzl | |
| normConventionalDistributionName = strings.ToLower(conventionalDistributionName) | |
| normConventionalDistributionName = regexp.MustCompile(`[-_.]+`).ReplaceAllString(normConventionalDistributionName, "_") | |
| normConventionalDistributionName = strings.Trim(normConventionalDistributionName, "_") | |
| case Pep503LabelNormalizationType: | |
| // See https://packaging.python.org/en/latest/specifications/name-normalization/#name-format | |
| normConventionalDistributionName = strings.ToLower(conventionalDistributionName) // ... "should be lowercased" | |
| normConventionalDistributionName = regexp.MustCompile(`[-_.]+`).ReplaceAllString(normConventionalDistributionName, "-") // ... "all runs of the characters ., -, or _ replaced with a single -" | |
| normConventionalDistributionName = strings.Trim(normConventionalDistributionName, "-") // ... "must start and end with a letter or number" | |
| default: | |
| fallthrough | |
| case NoLabelNormalizationType: | |
| normConventionalDistributionName = conventionalDistributionName | |
| } | |
| return label.New(repositoryName, normConventionalDistributionName, normConventionalDistributionName) | |
| } |