1// Copyright 2023 The Bazel Authors. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package pythonconfig 16 17import ( 18 "fmt" 19 "path" 20 "regexp" 21 "strings" 22 23 "github.com/emirpasic/gods/lists/singlylinkedlist" 24 25 "github.com/bazelbuild/bazel-gazelle/label" 26 "github.com/bazelbuild/rules_python/gazelle/manifest" 27) 28 29// Directives 30const ( 31 // PythonExtensionDirective represents the directive that controls whether 32 // this Python extension is enabled or not. Sub-packages inherit this value. 33 // Can be either "enabled" or "disabled". Defaults to "enabled". 34 PythonExtensionDirective = "python_extension" 35 // PythonRootDirective represents the directive that sets a Bazel package as 36 // a Python root. This is used on monorepos with multiple Python projects 37 // that don't share the top-level of the workspace as the root. 38 PythonRootDirective = "python_root" 39 // PythonManifestFileNameDirective represents the directive that overrides 40 // the default gazelle_python.yaml manifest file name. 41 PythonManifestFileNameDirective = "python_manifest_file_name" 42 // IgnoreFilesDirective represents the directive that controls the ignored 43 // files from the generated targets. 44 IgnoreFilesDirective = "python_ignore_files" 45 // IgnoreDependenciesDirective represents the directive that controls the 46 // ignored dependencies from the generated targets. 47 IgnoreDependenciesDirective = "python_ignore_dependencies" 48 // ValidateImportStatementsDirective represents the directive that controls 49 // whether the Python import statements should be validated. 50 ValidateImportStatementsDirective = "python_validate_import_statements" 51 // GenerationMode represents the directive that controls the target generation 52 // mode. See below for the GenerationModeType constants. 53 GenerationMode = "python_generation_mode" 54 // GenerationModePerFileIncludeInit represents the directive that augments 55 // the "per_file" GenerationMode by including the package's __init__.py file. 56 // This is a boolean directive. 57 GenerationModePerFileIncludeInit = "python_generation_mode_per_file_include_init" 58 // GenerationModePerPackageRequireTestEntryPoint represents the directive that 59 // requires a test entry point to generate test targets in "package" GenerationMode. 60 // This is a boolean directive. 61 GenerationModePerPackageRequireTestEntryPoint = "python_generation_mode_per_package_require_test_entry_point" 62 // LibraryNamingConvention represents the directive that controls the 63 // py_library naming convention. It interpolates $package_name$ with the 64 // Bazel package name. E.g. if the Bazel package name is `foo`, setting this 65 // to `$package_name$_my_lib` would render to `foo_my_lib`. 66 LibraryNamingConvention = "python_library_naming_convention" 67 // BinaryNamingConvention represents the directive that controls the 68 // py_binary naming convention. See python_library_naming_convention for 69 // more info on the package name interpolation. 70 BinaryNamingConvention = "python_binary_naming_convention" 71 // TestNamingConvention represents the directive that controls the py_test 72 // naming convention. See python_library_naming_convention for more info on 73 // the package name interpolation. 74 TestNamingConvention = "python_test_naming_convention" 75 // DefaultVisibilty represents the directive that controls what visibility 76 // labels are added to generated python targets. 77 DefaultVisibilty = "python_default_visibility" 78 // Visibility represents the directive that controls what additional 79 // visibility labels are added to generated targets. It mimics the behavior 80 // of the `go_visibility` directive. 81 Visibility = "python_visibility" 82 // TestFilePattern represents the directive that controls which python 83 // files are mapped to `py_test` targets. 84 TestFilePattern = "python_test_file_pattern" 85 // LabelConvention represents the directive that defines the format of the 86 // labels to third-party dependencies. 87 LabelConvention = "python_label_convention" 88 // LabelNormalization represents the directive that controls how distribution 89 // names of labels to third-party dependencies are normalized. Supported values 90 // are 'none', 'pep503' and 'snake_case' (default). See LabelNormalizationType. 91 LabelNormalization = "python_label_normalization" 92) 93 94// GenerationModeType represents one of the generation modes for the Python 95// extension. 96type GenerationModeType string 97 98// Generation modes 99const ( 100 // GenerationModePackage defines the mode in which targets will be generated 101 // for each __init__.py, or when an existing BUILD or BUILD.bazel file already 102 // determines a Bazel package. 103 GenerationModePackage GenerationModeType = "package" 104 // GenerationModeProject defines the mode in which a coarse-grained target will 105 // be generated englobing sub-directories containing Python files. 106 GenerationModeProject GenerationModeType = "project" 107 GenerationModeFile GenerationModeType = "file" 108) 109 110const ( 111 packageNameNamingConventionSubstitution = "$package_name$" 112 distributionNameLabelConventionSubstitution = "$distribution_name$" 113) 114 115const ( 116 // The default visibility label, including a format placeholder for `python_root`. 117 DefaultVisibilityFmtString = "//%s:__subpackages__" 118 // The default globs used to determine pt_test targets. 119 DefaultTestFilePatternString = "*_test.py,test_*.py" 120 // The default convention of label of third-party dependencies. 121 DefaultLabelConvention = "$distribution_name$" 122 // The default normalization applied to distribution names of third-party dependency labels. 123 DefaultLabelNormalizationType = SnakeCaseLabelNormalizationType 124) 125 126// defaultIgnoreFiles is the list of default values used in the 127// python_ignore_files option. 128var defaultIgnoreFiles = map[string]struct{}{ 129 "setup.py": {}, 130} 131 132// Configs is an extension of map[string]*Config. It provides finding methods 133// on top of the mapping. 134type Configs map[string]*Config 135 136// ParentForPackage returns the parent Config for the given Bazel package. 137func (c *Configs) ParentForPackage(pkg string) *Config { 138 dir := path.Dir(pkg) 139 if dir == "." { 140 dir = "" 141 } 142 parent := (map[string]*Config)(*c)[dir] 143 return parent 144} 145 146// Config represents a config extension for a specific Bazel package. 147type Config struct { 148 parent *Config 149 150 extensionEnabled bool 151 repoRoot string 152 pythonProjectRoot string 153 gazelleManifest *manifest.Manifest 154 155 excludedPatterns *singlylinkedlist.List 156 ignoreFiles map[string]struct{} 157 ignoreDependencies map[string]struct{} 158 validateImportStatements bool 159 coarseGrainedGeneration bool 160 perFileGeneration bool 161 perFileGenerationIncludeInit bool 162 perPackageGenerationRequireTestEntryPoint bool 163 libraryNamingConvention string 164 binaryNamingConvention string 165 testNamingConvention string 166 defaultVisibility []string 167 visibility []string 168 testFilePattern []string 169 labelConvention string 170 labelNormalization LabelNormalizationType 171} 172 173type LabelNormalizationType int 174 175const ( 176 NoLabelNormalizationType LabelNormalizationType = iota 177 Pep503LabelNormalizationType 178 SnakeCaseLabelNormalizationType 179) 180 181// New creates a new Config. 182func New( 183 repoRoot string, 184 pythonProjectRoot string, 185) *Config { 186 return &Config{ 187 extensionEnabled: true, 188 repoRoot: repoRoot, 189 pythonProjectRoot: pythonProjectRoot, 190 excludedPatterns: singlylinkedlist.New(), 191 ignoreFiles: make(map[string]struct{}), 192 ignoreDependencies: make(map[string]struct{}), 193 validateImportStatements: true, 194 coarseGrainedGeneration: false, 195 perFileGeneration: false, 196 perFileGenerationIncludeInit: false, 197 perPackageGenerationRequireTestEntryPoint: true, 198 libraryNamingConvention: packageNameNamingConventionSubstitution, 199 binaryNamingConvention: fmt.Sprintf("%s_bin", packageNameNamingConventionSubstitution), 200 testNamingConvention: fmt.Sprintf("%s_test", packageNameNamingConventionSubstitution), 201 defaultVisibility: []string{fmt.Sprintf(DefaultVisibilityFmtString, "")}, 202 visibility: []string{}, 203 testFilePattern: strings.Split(DefaultTestFilePatternString, ","), 204 labelConvention: DefaultLabelConvention, 205 labelNormalization: DefaultLabelNormalizationType, 206 } 207} 208 209// Parent returns the parent config. 210func (c *Config) Parent() *Config { 211 return c.parent 212} 213 214// NewChild creates a new child Config. It inherits desired values from the 215// current Config and sets itself as the parent to the child. 216func (c *Config) NewChild() *Config { 217 return &Config{ 218 parent: c, 219 extensionEnabled: c.extensionEnabled, 220 repoRoot: c.repoRoot, 221 pythonProjectRoot: c.pythonProjectRoot, 222 excludedPatterns: c.excludedPatterns, 223 ignoreFiles: make(map[string]struct{}), 224 ignoreDependencies: make(map[string]struct{}), 225 validateImportStatements: c.validateImportStatements, 226 coarseGrainedGeneration: c.coarseGrainedGeneration, 227 perFileGeneration: c.perFileGeneration, 228 perFileGenerationIncludeInit: c.perFileGenerationIncludeInit, 229 perPackageGenerationRequireTestEntryPoint: c.perPackageGenerationRequireTestEntryPoint, 230 libraryNamingConvention: c.libraryNamingConvention, 231 binaryNamingConvention: c.binaryNamingConvention, 232 testNamingConvention: c.testNamingConvention, 233 defaultVisibility: c.defaultVisibility, 234 visibility: c.visibility, 235 testFilePattern: c.testFilePattern, 236 labelConvention: c.labelConvention, 237 labelNormalization: c.labelNormalization, 238 } 239} 240 241// AddExcludedPattern adds a glob pattern parsed from the standard 242// gazelle:exclude directive. 243func (c *Config) AddExcludedPattern(pattern string) { 244 c.excludedPatterns.Add(pattern) 245} 246 247// ExcludedPatterns returns the excluded patterns list. 248func (c *Config) ExcludedPatterns() *singlylinkedlist.List { 249 return c.excludedPatterns 250} 251 252// SetExtensionEnabled sets whether the extension is enabled or not. 253func (c *Config) SetExtensionEnabled(enabled bool) { 254 c.extensionEnabled = enabled 255} 256 257// ExtensionEnabled returns whether the extension is enabled or not. 258func (c *Config) ExtensionEnabled() bool { 259 return c.extensionEnabled 260} 261 262// SetPythonProjectRoot sets the Python project root. 263func (c *Config) SetPythonProjectRoot(pythonProjectRoot string) { 264 c.pythonProjectRoot = pythonProjectRoot 265} 266 267// PythonProjectRoot returns the Python project root. 268func (c *Config) PythonProjectRoot() string { 269 return c.pythonProjectRoot 270} 271 272// SetGazelleManifest sets the Gazelle manifest parsed from the 273// gazelle_python.yaml file. 274func (c *Config) SetGazelleManifest(gazelleManifest *manifest.Manifest) { 275 c.gazelleManifest = gazelleManifest 276} 277 278// FindThirdPartyDependency scans the gazelle manifests for the current config 279// and the parent configs up to the root finding if it can resolve the module 280// name. 281func (c *Config) FindThirdPartyDependency(modName string) (string, bool) { 282 for currentCfg := c; currentCfg != nil; currentCfg = currentCfg.parent { 283 if currentCfg.gazelleManifest != nil { 284 gazelleManifest := currentCfg.gazelleManifest 285 if distributionName, ok := gazelleManifest.ModulesMapping[modName]; ok { 286 var distributionRepositoryName string 287 if gazelleManifest.PipDepsRepositoryName != "" { 288 distributionRepositoryName = gazelleManifest.PipDepsRepositoryName 289 } else if gazelleManifest.PipRepository != nil { 290 distributionRepositoryName = gazelleManifest.PipRepository.Name 291 } 292 293 lbl := currentCfg.FormatThirdPartyDependency(distributionRepositoryName, distributionName) 294 return lbl.String(), true 295 } 296 } 297 } 298 return "", false 299} 300 301// AddIgnoreFile adds a file to the list of ignored files for a given package. 302// Adding an ignored file to a package also makes it ignored on a subpackage. 303func (c *Config) AddIgnoreFile(file string) { 304 c.ignoreFiles[strings.TrimSpace(file)] = struct{}{} 305} 306 307// IgnoresFile checks if a file is ignored in the given package or in one of the 308// parent packages up to the workspace root. 309func (c *Config) IgnoresFile(file string) bool { 310 trimmedFile := strings.TrimSpace(file) 311 312 if _, ignores := defaultIgnoreFiles[trimmedFile]; ignores { 313 return true 314 } 315 316 if _, ignores := c.ignoreFiles[trimmedFile]; ignores { 317 return true 318 } 319 320 parent := c.parent 321 for parent != nil { 322 if _, ignores := parent.ignoreFiles[trimmedFile]; ignores { 323 return true 324 } 325 parent = parent.parent 326 } 327 328 return false 329} 330 331// AddIgnoreDependency adds a dependency to the list of ignored dependencies for 332// a given package. Adding an ignored dependency to a package also makes it 333// ignored on a subpackage. 334func (c *Config) AddIgnoreDependency(dep string) { 335 c.ignoreDependencies[strings.TrimSpace(dep)] = struct{}{} 336} 337 338// IgnoresDependency checks if a dependency is ignored in the given package or 339// in one of the parent packages up to the workspace root. 340func (c *Config) IgnoresDependency(dep string) bool { 341 trimmedDep := strings.TrimSpace(dep) 342 343 if _, ignores := c.ignoreDependencies[trimmedDep]; ignores { 344 return true 345 } 346 347 parent := c.parent 348 for parent != nil { 349 if _, ignores := parent.ignoreDependencies[trimmedDep]; ignores { 350 return true 351 } 352 parent = parent.parent 353 } 354 355 return false 356} 357 358// SetValidateImportStatements sets whether Python import statements should be 359// validated or not. It throws an error if this is set multiple times, i.e. if 360// the directive is specified multiple times in the Bazel workspace. 361func (c *Config) SetValidateImportStatements(validate bool) { 362 c.validateImportStatements = validate 363} 364 365// ValidateImportStatements returns whether the Python import statements should 366// be validated or not. If this option was not explicitly specified by the user, 367// it defaults to true. 368func (c *Config) ValidateImportStatements() bool { 369 return c.validateImportStatements 370} 371 372// SetCoarseGrainedGeneration sets whether coarse-grained targets should be 373// generated or not. 374func (c *Config) SetCoarseGrainedGeneration(coarseGrained bool) { 375 c.coarseGrainedGeneration = coarseGrained 376} 377 378// CoarseGrainedGeneration returns whether coarse-grained targets should be 379// generated or not. 380func (c *Config) CoarseGrainedGeneration() bool { 381 return c.coarseGrainedGeneration 382} 383 384// SetPerFileGneration sets whether a separate py_library target should be 385// generated for each file. 386func (c *Config) SetPerFileGeneration(perFile bool) { 387 c.perFileGeneration = perFile 388} 389 390// PerFileGeneration returns whether a separate py_library target should be 391// generated for each file. 392func (c *Config) PerFileGeneration() bool { 393 return c.perFileGeneration 394} 395 396// SetPerFileGenerationIncludeInit sets whether py_library targets should 397// include __init__.py files when PerFileGeneration() is true. 398func (c *Config) SetPerFileGenerationIncludeInit(includeInit bool) { 399 c.perFileGenerationIncludeInit = includeInit 400} 401 402// PerFileGenerationIncludeInit returns whether py_library targets should 403// include __init__.py files when PerFileGeneration() is true. 404func (c *Config) PerFileGenerationIncludeInit() bool { 405 return c.perFileGenerationIncludeInit 406} 407 408func (c *Config) SetPerPackageGenerationRequireTestEntryPoint(perPackageGenerationRequireTestEntryPoint bool) { 409 c.perPackageGenerationRequireTestEntryPoint = perPackageGenerationRequireTestEntryPoint 410} 411 412func (c *Config) PerPackageGenerationRequireTestEntryPoint() bool { 413 return c.perPackageGenerationRequireTestEntryPoint 414} 415 416// SetLibraryNamingConvention sets the py_library target naming convention. 417func (c *Config) SetLibraryNamingConvention(libraryNamingConvention string) { 418 c.libraryNamingConvention = libraryNamingConvention 419} 420 421// RenderLibraryName returns the py_library target name by performing all 422// substitutions. 423func (c *Config) RenderLibraryName(packageName string) string { 424 return strings.ReplaceAll(c.libraryNamingConvention, packageNameNamingConventionSubstitution, packageName) 425} 426 427// SetBinaryNamingConvention sets the py_binary target naming convention. 428func (c *Config) SetBinaryNamingConvention(binaryNamingConvention string) { 429 c.binaryNamingConvention = binaryNamingConvention 430} 431 432// RenderBinaryName returns the py_binary target name by performing all 433// substitutions. 434func (c *Config) RenderBinaryName(packageName string) string { 435 return strings.ReplaceAll(c.binaryNamingConvention, packageNameNamingConventionSubstitution, packageName) 436} 437 438// SetTestNamingConvention sets the py_test target naming convention. 439func (c *Config) SetTestNamingConvention(testNamingConvention string) { 440 c.testNamingConvention = testNamingConvention 441} 442 443// RenderTestName returns the py_test target name by performing all 444// substitutions. 445func (c *Config) RenderTestName(packageName string) string { 446 return strings.ReplaceAll(c.testNamingConvention, packageNameNamingConventionSubstitution, packageName) 447} 448 449// AppendVisibility adds additional items to the target's visibility. 450func (c *Config) AppendVisibility(visibility string) { 451 c.visibility = append(c.visibility, visibility) 452} 453 454// Visibility returns the target's visibility. 455func (c *Config) Visibility() []string { 456 return append(c.defaultVisibility, c.visibility...) 457} 458 459// SetDefaultVisibility sets the default visibility of the target. 460func (c *Config) SetDefaultVisibility(visibility []string) { 461 c.defaultVisibility = visibility 462} 463 464// DefaultVisibilty returns the target's default visibility. 465func (c *Config) DefaultVisibilty() []string { 466 return c.defaultVisibility 467} 468 469// SetTestFilePattern sets the file patterns that should be mapped to 'py_test' rules. 470func (c *Config) SetTestFilePattern(patterns []string) { 471 c.testFilePattern = patterns 472} 473 474// TestFilePattern returns the patterns that should be mapped to 'py_test' rules. 475func (c *Config) TestFilePattern() []string { 476 return c.testFilePattern 477} 478 479// SetLabelConvention sets the label convention used for third-party dependencies. 480func (c *Config) SetLabelConvention(convention string) { 481 c.labelConvention = convention 482} 483 484// LabelConvention returns the label convention used for third-party dependencies. 485func (c *Config) LabelConvention() string { 486 return c.labelConvention 487} 488 489// SetLabelConvention sets the label normalization applied to distribution names of third-party dependencies. 490func (c *Config) SetLabelNormalization(normalizationType LabelNormalizationType) { 491 c.labelNormalization = normalizationType 492} 493 494// LabelConvention returns the label normalization applied to distribution names of third-party dependencies. 495func (c *Config) LabelNormalization() LabelNormalizationType { 496 return c.labelNormalization 497} 498 499// FormatThirdPartyDependency returns a label to a third-party dependency performing all formating and normalization. 500func (c *Config) FormatThirdPartyDependency(repositoryName string, distributionName string) label.Label { 501 conventionalDistributionName := strings.ReplaceAll(c.labelConvention, distributionNameLabelConventionSubstitution, distributionName) 502 503 var normConventionalDistributionName string 504 switch norm := c.LabelNormalization(); norm { 505 case SnakeCaseLabelNormalizationType: 506 // See /python/private/normalize_name.bzl 507 normConventionalDistributionName = strings.ToLower(conventionalDistributionName) 508 normConventionalDistributionName = regexp.MustCompile(`[-_.]+`).ReplaceAllString(normConventionalDistributionName, "_") 509 normConventionalDistributionName = strings.Trim(normConventionalDistributionName, "_") 510 case Pep503LabelNormalizationType: 511 // See https://packaging.python.org/en/latest/specifications/name-normalization/#name-format 512 normConventionalDistributionName = strings.ToLower(conventionalDistributionName) // ... "should be lowercased" 513 normConventionalDistributionName = regexp.MustCompile(`[-_.]+`).ReplaceAllString(normConventionalDistributionName, "-") // ... "all runs of the characters ., -, or _ replaced with a single -" 514 normConventionalDistributionName = strings.Trim(normConventionalDistributionName, "-") // ... "must start and end with a letter or number" 515 default: 516 fallthrough 517 case NoLabelNormalizationType: 518 normConventionalDistributionName = conventionalDistributionName 519 } 520 521 return label.New(repositoryName, normConventionalDistributionName, normConventionalDistributionName) 522} 523