xref: /aosp_15_r20/external/bazelbuild-rules_python/gazelle/pythonconfig/pythonconfig.go (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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