• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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