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/filepath" 20 "strings" 21 22 "github.com/emirpasic/gods/lists/singlylinkedlist" 23 24 "github.com/bazelbuild/bazel-gazelle/label" 25 "github.com/bazelbuild/rules_python/gazelle/manifest" 26) 27 28// Directives 29const ( 30 // PythonExtensionDirective represents the directive that controls whether 31 // this Python extension is enabled or not. Sub-packages inherit this value. 32 // Can be either "enabled" or "disabled". Defaults to "enabled". 33 PythonExtensionDirective = "python_extension" 34 // PythonRootDirective represents the directive that sets a Bazel package as 35 // a Python root. This is used on monorepos with multiple Python projects 36 // that don't share the top-level of the workspace as the root. 37 PythonRootDirective = "python_root" 38 // PythonManifestFileNameDirective represents the directive that overrides 39 // the default gazelle_python.yaml manifest file name. 40 PythonManifestFileNameDirective = "python_manifest_file_name" 41 // IgnoreFilesDirective represents the directive that controls the ignored 42 // files from the generated targets. 43 IgnoreFilesDirective = "python_ignore_files" 44 // IgnoreDependenciesDirective represents the directive that controls the 45 // ignored dependencies from the generated targets. 46 IgnoreDependenciesDirective = "python_ignore_dependencies" 47 // ValidateImportStatementsDirective represents the directive that controls 48 // whether the Python import statements should be validated. 49 ValidateImportStatementsDirective = "python_validate_import_statements" 50 // GenerationMode represents the directive that controls the target generation 51 // mode. See below for the GenerationModeType constants. 52 GenerationMode = "python_generation_mode" 53 // LibraryNamingConvention represents the directive that controls the 54 // py_library naming convention. It interpolates $package_name$ with the 55 // Bazel package name. E.g. if the Bazel package name is `foo`, setting this 56 // to `$package_name$_my_lib` would render to `foo_my_lib`. 57 LibraryNamingConvention = "python_library_naming_convention" 58 // BinaryNamingConvention represents the directive that controls the 59 // py_binary naming convention. See python_library_naming_convention for 60 // more info on the package name interpolation. 61 BinaryNamingConvention = "python_binary_naming_convention" 62 // TestNamingConvention represents the directive that controls the py_test 63 // naming convention. See python_library_naming_convention for more info on 64 // the package name interpolation. 65 TestNamingConvention = "python_test_naming_convention" 66) 67 68// GenerationModeType represents one of the generation modes for the Python 69// extension. 70type GenerationModeType string 71 72// Generation modes 73const ( 74 // GenerationModePackage defines the mode in which targets will be generated 75 // for each __init__.py, or when an existing BUILD or BUILD.bazel file already 76 // determines a Bazel package. 77 GenerationModePackage GenerationModeType = "package" 78 // GenerationModeProject defines the mode in which a coarse-grained target will 79 // be generated englobing sub-directories containing Python files. 80 GenerationModeProject GenerationModeType = "project" 81) 82 83const ( 84 packageNameNamingConventionSubstitution = "$package_name$" 85) 86 87// defaultIgnoreFiles is the list of default values used in the 88// python_ignore_files option. 89var defaultIgnoreFiles = map[string]struct{}{ 90 "setup.py": {}, 91} 92 93func SanitizeDistribution(distributionName string) string { 94 sanitizedDistribution := strings.ToLower(distributionName) 95 sanitizedDistribution = strings.ReplaceAll(sanitizedDistribution, "-", "_") 96 sanitizedDistribution = strings.ReplaceAll(sanitizedDistribution, ".", "_") 97 98 return sanitizedDistribution 99} 100 101// Configs is an extension of map[string]*Config. It provides finding methods 102// on top of the mapping. 103type Configs map[string]*Config 104 105// ParentForPackage returns the parent Config for the given Bazel package. 106func (c *Configs) ParentForPackage(pkg string) *Config { 107 dir := filepath.Dir(pkg) 108 if dir == "." { 109 dir = "" 110 } 111 parent := (map[string]*Config)(*c)[dir] 112 return parent 113} 114 115// Config represents a config extension for a specific Bazel package. 116type Config struct { 117 parent *Config 118 119 extensionEnabled bool 120 repoRoot string 121 pythonProjectRoot string 122 gazelleManifest *manifest.Manifest 123 124 excludedPatterns *singlylinkedlist.List 125 ignoreFiles map[string]struct{} 126 ignoreDependencies map[string]struct{} 127 validateImportStatements bool 128 coarseGrainedGeneration bool 129 libraryNamingConvention string 130 binaryNamingConvention string 131 testNamingConvention string 132} 133 134// New creates a new Config. 135func New( 136 repoRoot string, 137 pythonProjectRoot string, 138) *Config { 139 return &Config{ 140 extensionEnabled: true, 141 repoRoot: repoRoot, 142 pythonProjectRoot: pythonProjectRoot, 143 excludedPatterns: singlylinkedlist.New(), 144 ignoreFiles: make(map[string]struct{}), 145 ignoreDependencies: make(map[string]struct{}), 146 validateImportStatements: true, 147 coarseGrainedGeneration: false, 148 libraryNamingConvention: packageNameNamingConventionSubstitution, 149 binaryNamingConvention: fmt.Sprintf("%s_bin", packageNameNamingConventionSubstitution), 150 testNamingConvention: fmt.Sprintf("%s_test", packageNameNamingConventionSubstitution), 151 } 152} 153 154// Parent returns the parent config. 155func (c *Config) Parent() *Config { 156 return c.parent 157} 158 159// NewChild creates a new child Config. It inherits desired values from the 160// current Config and sets itself as the parent to the child. 161func (c *Config) NewChild() *Config { 162 return &Config{ 163 parent: c, 164 extensionEnabled: c.extensionEnabled, 165 repoRoot: c.repoRoot, 166 pythonProjectRoot: c.pythonProjectRoot, 167 excludedPatterns: c.excludedPatterns, 168 ignoreFiles: make(map[string]struct{}), 169 ignoreDependencies: make(map[string]struct{}), 170 validateImportStatements: c.validateImportStatements, 171 coarseGrainedGeneration: c.coarseGrainedGeneration, 172 libraryNamingConvention: c.libraryNamingConvention, 173 binaryNamingConvention: c.binaryNamingConvention, 174 testNamingConvention: c.testNamingConvention, 175 } 176} 177 178// AddExcludedPattern adds a glob pattern parsed from the standard 179// gazelle:exclude directive. 180func (c *Config) AddExcludedPattern(pattern string) { 181 c.excludedPatterns.Add(pattern) 182} 183 184// ExcludedPatterns returns the excluded patterns list. 185func (c *Config) ExcludedPatterns() *singlylinkedlist.List { 186 return c.excludedPatterns 187} 188 189// SetExtensionEnabled sets whether the extension is enabled or not. 190func (c *Config) SetExtensionEnabled(enabled bool) { 191 c.extensionEnabled = enabled 192} 193 194// ExtensionEnabled returns whether the extension is enabled or not. 195func (c *Config) ExtensionEnabled() bool { 196 return c.extensionEnabled 197} 198 199// SetPythonProjectRoot sets the Python project root. 200func (c *Config) SetPythonProjectRoot(pythonProjectRoot string) { 201 c.pythonProjectRoot = pythonProjectRoot 202} 203 204// PythonProjectRoot returns the Python project root. 205func (c *Config) PythonProjectRoot() string { 206 return c.pythonProjectRoot 207} 208 209// SetGazelleManifest sets the Gazelle manifest parsed from the 210// gazelle_python.yaml file. 211func (c *Config) SetGazelleManifest(gazelleManifest *manifest.Manifest) { 212 c.gazelleManifest = gazelleManifest 213} 214 215// FindThirdPartyDependency scans the gazelle manifests for the current config 216// and the parent configs up to the root finding if it can resolve the module 217// name. 218func (c *Config) FindThirdPartyDependency(modName string) (string, bool) { 219 for currentCfg := c; currentCfg != nil; currentCfg = currentCfg.parent { 220 if currentCfg.gazelleManifest != nil { 221 gazelleManifest := currentCfg.gazelleManifest 222 if distributionName, ok := gazelleManifest.ModulesMapping[modName]; ok { 223 var distributionRepositoryName string 224 if gazelleManifest.PipDepsRepositoryName != "" { 225 distributionRepositoryName = gazelleManifest.PipDepsRepositoryName 226 } else if gazelleManifest.PipRepository != nil { 227 distributionRepositoryName = gazelleManifest.PipRepository.Name 228 } 229 sanitizedDistribution := SanitizeDistribution(distributionName) 230 231 if gazelleManifest.PipRepository != nil && gazelleManifest.PipRepository.UsePipRepositoryAliases { 232 // @<repository_name>//<distribution_name> 233 lbl := label.New(distributionRepositoryName, sanitizedDistribution, sanitizedDistribution) 234 return lbl.String(), true 235 } 236 237 // @<repository_name>_<distribution_name>//:pkg 238 distributionRepositoryName = distributionRepositoryName + "_" + sanitizedDistribution 239 lbl := label.New(distributionRepositoryName, "", "pkg") 240 return lbl.String(), true 241 } 242 } 243 } 244 return "", false 245} 246 247// AddIgnoreFile adds a file to the list of ignored files for a given package. 248// Adding an ignored file to a package also makes it ignored on a subpackage. 249func (c *Config) AddIgnoreFile(file string) { 250 c.ignoreFiles[strings.TrimSpace(file)] = struct{}{} 251} 252 253// IgnoresFile checks if a file is ignored in the given package or in one of the 254// parent packages up to the workspace root. 255func (c *Config) IgnoresFile(file string) bool { 256 trimmedFile := strings.TrimSpace(file) 257 258 if _, ignores := defaultIgnoreFiles[trimmedFile]; ignores { 259 return true 260 } 261 262 if _, ignores := c.ignoreFiles[trimmedFile]; ignores { 263 return true 264 } 265 266 parent := c.parent 267 for parent != nil { 268 if _, ignores := parent.ignoreFiles[trimmedFile]; ignores { 269 return true 270 } 271 parent = parent.parent 272 } 273 274 return false 275} 276 277// AddIgnoreDependency adds a dependency to the list of ignored dependencies for 278// a given package. Adding an ignored dependency to a package also makes it 279// ignored on a subpackage. 280func (c *Config) AddIgnoreDependency(dep string) { 281 c.ignoreDependencies[strings.TrimSpace(dep)] = struct{}{} 282} 283 284// IgnoresDependency checks if a dependency is ignored in the given package or 285// in one of the parent packages up to the workspace root. 286func (c *Config) IgnoresDependency(dep string) bool { 287 trimmedDep := strings.TrimSpace(dep) 288 289 if _, ignores := c.ignoreDependencies[trimmedDep]; ignores { 290 return true 291 } 292 293 parent := c.parent 294 for parent != nil { 295 if _, ignores := parent.ignoreDependencies[trimmedDep]; ignores { 296 return true 297 } 298 parent = parent.parent 299 } 300 301 return false 302} 303 304// SetValidateImportStatements sets whether Python import statements should be 305// validated or not. It throws an error if this is set multiple times, i.e. if 306// the directive is specified multiple times in the Bazel workspace. 307func (c *Config) SetValidateImportStatements(validate bool) { 308 c.validateImportStatements = validate 309} 310 311// ValidateImportStatements returns whether the Python import statements should 312// be validated or not. If this option was not explicitly specified by the user, 313// it defaults to true. 314func (c *Config) ValidateImportStatements() bool { 315 return c.validateImportStatements 316} 317 318// SetCoarseGrainedGeneration sets whether coarse-grained targets should be 319// generated or not. 320func (c *Config) SetCoarseGrainedGeneration(coarseGrained bool) { 321 c.coarseGrainedGeneration = coarseGrained 322} 323 324// CoarseGrainedGeneration returns whether coarse-grained targets should be 325// generated or not. 326func (c *Config) CoarseGrainedGeneration() bool { 327 return c.coarseGrainedGeneration 328} 329 330// SetLibraryNamingConvention sets the py_library target naming convention. 331func (c *Config) SetLibraryNamingConvention(libraryNamingConvention string) { 332 c.libraryNamingConvention = libraryNamingConvention 333} 334 335// RenderLibraryName returns the py_library target name by performing all 336// substitutions. 337func (c *Config) RenderLibraryName(packageName string) string { 338 return strings.ReplaceAll(c.libraryNamingConvention, packageNameNamingConventionSubstitution, packageName) 339} 340 341// SetBinaryNamingConvention sets the py_binary target naming convention. 342func (c *Config) SetBinaryNamingConvention(binaryNamingConvention string) { 343 c.binaryNamingConvention = binaryNamingConvention 344} 345 346// RenderBinaryName returns the py_binary target name by performing all 347// substitutions. 348func (c *Config) RenderBinaryName(packageName string) string { 349 return strings.ReplaceAll(c.binaryNamingConvention, packageNameNamingConventionSubstitution, packageName) 350} 351 352// SetTestNamingConvention sets the py_test target naming convention. 353func (c *Config) SetTestNamingConvention(testNamingConvention string) { 354 c.testNamingConvention = testNamingConvention 355} 356 357// RenderTestName returns the py_test target name by performing all 358// substitutions. 359func (c *Config) RenderTestName(packageName string) string { 360 return strings.ReplaceAll(c.testNamingConvention, packageNameNamingConventionSubstitution, packageName) 361} 362