blob: 166b5750461d16ba28376153b4be57cbc9bd835a [file] [log] [blame]
// 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"
// GenerationModePerPackageRequireTestEntryPoint represents the directive that
// requires a test entry point to generate test targets in "package" GenerationMode.
// This is a boolean directive.
GenerationModePerPackageRequireTestEntryPoint = "python_generation_mode_per_package_require_test_entry_point"
// 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
perPackageGenerationRequireTestEntryPoint 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,
perPackageGenerationRequireTestEntryPoint: true,
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,
perPackageGenerationRequireTestEntryPoint: c.perPackageGenerationRequireTestEntryPoint,
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
for {
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
}
i := strings.LastIndex(modName, ".")
if i == -1 {
break
}
modName = modName[:i]
}
}
}
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
}
func (c *Config) SetPerPackageGenerationRequireTestEntryPoint(perPackageGenerationRequireTestEntryPoint bool) {
c.perPackageGenerationRequireTestEntryPoint = perPackageGenerationRequireTestEntryPoint
}
func (c *Config) PerPackageGenerationRequireTestEntryPoint() bool {
return c.perPackageGenerationRequireTestEntryPoint
}
// 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)
}