Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions gazelle/parse.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,18 @@ def parse_import_statements(content, filepath):
"name": subnode.name,
"lineno": node.lineno,
"filepath": filepath,
"from": ""
}
modules.append(module)
elif isinstance(node, ast.ImportFrom) and node.level == 0:
module = {
"name": node.module,
"lineno": node.lineno,
"filepath": filepath,
}
modules.append(module)
for subnode in node.names:
module = {
"name": f"{node.module}.{subnode.name}",
"lineno": node.lineno,
"filepath": filepath,
"from": node.module
}
modules.append(module)
return modules


Expand All@@ -47,9 +50,10 @@ def parse(repo_root, rel_package_path, filename):
abs_filepath = os.path.join(repo_root, rel_filepath)
with open(abs_filepath, "r") as file:
content = file.read()
# From simple benchmarks, 2 workers gave the best performance here.
# From simple benchmarks, 2 workers gave the best performance here.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
modules_future = executor.submit(parse_import_statements, content, rel_filepath)
modules_future = executor.submit(parse_import_statements, content,
rel_filepath)
comments_future = executor.submit(parse_comments, content)
modules = modules_future.result()
comments = comments_future.result()
Expand All@@ -69,11 +73,12 @@ def main(stdin, stdout):
filenames = parse_request["filenames"]
outputs = list()
if len(filenames) == 1:
outputs.append(parse(repo_root, rel_package_path, filenames[0]))
outputs.append(parse(repo_root, rel_package_path,
filenames[0]))
else:
futures = [
executor.submit(parse, repo_root, rel_package_path, filename)
for filename in filenames
executor.submit(parse, repo_root, rel_package_path,
filename) for filename in filenames
if filename != ""
]
for future in concurrent.futures.as_completed(futures):
Expand Down
7 changes: 5 additions & 2 deletions gazelle/parser.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,13 +133,13 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) {
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}

// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) {
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}

Expand DownExpand Up@@ -170,6 +170,9 @@ type module struct {
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}

// moduleComparator compares modules by name.
Expand Down
187 changes: 107 additions & 80 deletions gazelle/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,99 +140,126 @@ func (py *Resolver) Resolve(
it := modules.Iterator()
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
hasFatalError := false
MODULE_LOOP:
MODULES_LOOP:
for it.Next() {
mod := it.Value().(module)
imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
moduleParts := strings.Split(mod.Name, ".")
possibleModules := []string{mod.Name}
for len(moduleParts) > 1 {
// Iterate back through the possible imports until
// a match is found.
// For example, "from foo.bar import baz" where bar is a variable, we should try
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
// in the directory `foo/bar`.
// Or, the import could be variable `bar` in file `foo/bar.py`.
// The import could also be from a standard module, e.g. `six.moves`, where
// the dependency is actually `six`.
moduleParts = moduleParts[:len(moduleParts)-1]
possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
}
errs := []error{}
POSSIBLE_MODULE_LOOP:
for _, moduleName := range possibleModules {
imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
}
} else {
if dep, ok := cfg.FindThirdPartyDependency(mod.Name); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, dep)
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
continue MODULES_LOOP
}
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(mod); err != nil {
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
} else if isStd {
continue MODULE_LOOP
if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
}
if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
mod.Name, mod.LineNumber, mod.Filepath,
)
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
hasFatalError = true
continue MODULE_LOOP
continue MODULES_LOOP
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
log.Println("Error checking if standard module: ", err)
hasFatalError = true
continue POSSIBLE_MODULE_LOOP
} else if isStd {
continue MODULES_LOOP
} else if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
moduleName, mod.LineNumber, mod.Filepath,
)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
}
}
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULE_LOOP
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULES_LOOP
}
filteredMatches = append(filteredMatches, match)
}
filteredMatches = append(filteredMatches, match)
}
if len(filteredMatches) == 0 {
continue
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
if len(filteredMatches) == 0 {
continue POSSIBLE_MODULE_LOOP
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
}
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
filteredMatches = sameRootMatches
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
filteredMatches = sameRootMatches
}
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
continue MODULES_LOOP
}
}
} // End possible modules loop.
if len(errs) > 0 {
// If, after trying all possible modules, we still haven't found anything, error out.
joinedErrs := ""
for _, err := range errs {
joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
}
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
hasFatalError = true
}
}
if hasFatalError {
Expand Down
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
7 changes: 7 additions & 0 deletions gazelle/testdata/from_imports/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
# From Imports

This test case simulates imports of the form:

```python
from foo import bar
```
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/WORKSPACE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@

8 changes: 8 additions & 0 deletions gazelle/testdata/from_imports/foo/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
foo = "foo"
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
bar = "bar"
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/baz.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
baz = "baz"
5 changes: 5 additions & 0 deletions gazelle/testdata/from_imports/gazelle_python.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
manifest:
modules_mapping:
boto3: rootboto3
boto4: rootboto4
pip_deps_repository_name: root_pip_deps
Empty file.
9 changes: 9 additions & 0 deletions gazelle/testdata/from_imports/import_from_init_py/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_init_py",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = ["//foo/bar"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# bar is a variable inside foo/bar/__init__.py
from foo.bar import bar
Empty file.
12 changes: 12 additions & 0 deletions gazelle/testdata/from_imports/import_from_multiple/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_multiple",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = [
"//foo/bar",
"//foo/bar:baz",
],
)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions gazelle/parse.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,18 @@ def parse_import_statements(content, filepath):
"name": subnode.name,
"lineno": node.lineno,
"filepath": filepath,
"from": ""
}
modules.append(module)
elif isinstance(node, ast.ImportFrom) and node.level == 0:
module = {
"name": node.module,
"lineno": node.lineno,
"filepath": filepath,
}
modules.append(module)
for subnode in node.names:
module = {
"name": f"{node.module}.{subnode.name}",
"lineno": node.lineno,
"filepath": filepath,
"from": node.module
}
modules.append(module)
return modules


Expand All@@ -47,9 +50,10 @@ def parse(repo_root, rel_package_path, filename):
abs_filepath = os.path.join(repo_root, rel_filepath)
with open(abs_filepath, "r") as file:
content = file.read()
# From simple benchmarks, 2 workers gave the best performance here.
# From simple benchmarks, 2 workers gave the best performance here.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
modules_future = executor.submit(parse_import_statements, content, rel_filepath)
modules_future = executor.submit(parse_import_statements, content,
rel_filepath)
comments_future = executor.submit(parse_comments, content)
modules = modules_future.result()
comments = comments_future.result()
Expand All@@ -69,11 +73,12 @@ def main(stdin, stdout):
filenames = parse_request["filenames"]
outputs = list()
if len(filenames) == 1:
outputs.append(parse(repo_root, rel_package_path, filenames[0]))
outputs.append(parse(repo_root, rel_package_path,
filenames[0]))
else:
futures = [
executor.submit(parse, repo_root, rel_package_path, filename)
for filename in filenames
executor.submit(parse, repo_root, rel_package_path,
filename) for filename in filenames
if filename != ""
]
for future in concurrent.futures.as_completed(futures):
Expand Down
7 changes: 5 additions & 2 deletions gazelle/parser.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,13 +133,13 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) {
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}

// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) {
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}

Expand DownExpand Up@@ -170,6 +170,9 @@ type module struct {
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}

// moduleComparator compares modules by name.
Expand Down
187 changes: 107 additions & 80 deletions gazelle/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,99 +140,126 @@ func (py *Resolver) Resolve(
it := modules.Iterator()
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
hasFatalError := false
MODULE_LOOP:
MODULES_LOOP:
for it.Next() {
mod := it.Value().(module)
imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
moduleParts := strings.Split(mod.Name, ".")
possibleModules := []string{mod.Name}
for len(moduleParts) > 1 {
// Iterate back through the possible imports until
// a match is found.
// For example, "from foo.bar import baz" where bar is a variable, we should try
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
// in the directory `foo/bar`.
// Or, the import could be variable `bar` in file `foo/bar.py`.
// The import could also be from a standard module, e.g. `six.moves`, where
// the dependency is actually `six`.
moduleParts = moduleParts[:len(moduleParts)-1]
possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
}
errs := []error{}
POSSIBLE_MODULE_LOOP:
for _, moduleName := range possibleModules {
imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
}
} else {
if dep, ok := cfg.FindThirdPartyDependency(mod.Name); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, dep)
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
continue MODULES_LOOP
}
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(mod); err != nil {
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
} else if isStd {
continue MODULE_LOOP
if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
}
if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
mod.Name, mod.LineNumber, mod.Filepath,
)
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
hasFatalError = true
continue MODULE_LOOP
continue MODULES_LOOP
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
log.Println("Error checking if standard module: ", err)
hasFatalError = true
continue POSSIBLE_MODULE_LOOP
} else if isStd {
continue MODULES_LOOP
} else if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
moduleName, mod.LineNumber, mod.Filepath,
)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
}
}
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULE_LOOP
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULES_LOOP
}
filteredMatches = append(filteredMatches, match)
}
filteredMatches = append(filteredMatches, match)
}
if len(filteredMatches) == 0 {
continue
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
if len(filteredMatches) == 0 {
continue POSSIBLE_MODULE_LOOP
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
}
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
filteredMatches = sameRootMatches
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
filteredMatches = sameRootMatches
}
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
continue MODULES_LOOP
}
}
} // End possible modules loop.
if len(errs) > 0 {
// If, after trying all possible modules, we still haven't found anything, error out.
joinedErrs := ""
for _, err := range errs {
joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
}
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
hasFatalError = true
}
}
if hasFatalError {
Expand Down
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
7 changes: 7 additions & 0 deletions gazelle/testdata/from_imports/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
# From Imports

This test case simulates imports of the form:

```python
from foo import bar
```
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/WORKSPACE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@

8 changes: 8 additions & 0 deletions gazelle/testdata/from_imports/foo/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
foo = "foo"
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
bar = "bar"
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/baz.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
baz = "baz"
5 changes: 5 additions & 0 deletions gazelle/testdata/from_imports/gazelle_python.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
manifest:
modules_mapping:
boto3: rootboto3
boto4: rootboto4
pip_deps_repository_name: root_pip_deps
Empty file.
9 changes: 9 additions & 0 deletions gazelle/testdata/from_imports/import_from_init_py/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_init_py",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = ["//foo/bar"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# bar is a variable inside foo/bar/__init__.py
from foo.bar import bar
Empty file.
12 changes: 12 additions & 0 deletions gazelle/testdata/from_imports/import_from_multiple/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_multiple",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = [
"//foo/bar",
"//foo/bar:baz",
],
)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions gazelle/parse.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,18 @@ def parse_import_statements(content, filepath):
"name": subnode.name,
"lineno": node.lineno,
"filepath": filepath,
"from": ""
}
modules.append(module)
elif isinstance(node, ast.ImportFrom) and node.level == 0:
module = {
"name": node.module,
"lineno": node.lineno,
"filepath": filepath,
}
modules.append(module)
for subnode in node.names:
module = {
"name": f"{node.module}.{subnode.name}",
"lineno": node.lineno,
"filepath": filepath,
"from": node.module
}
modules.append(module)
return modules


Expand All@@ -47,9 +50,10 @@ def parse(repo_root, rel_package_path, filename):
abs_filepath = os.path.join(repo_root, rel_filepath)
with open(abs_filepath, "r") as file:
content = file.read()
# From simple benchmarks, 2 workers gave the best performance here.
# From simple benchmarks, 2 workers gave the best performance here.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
modules_future = executor.submit(parse_import_statements, content, rel_filepath)
modules_future = executor.submit(parse_import_statements, content,
rel_filepath)
comments_future = executor.submit(parse_comments, content)
modules = modules_future.result()
comments = comments_future.result()
Expand All@@ -69,11 +73,12 @@ def main(stdin, stdout):
filenames = parse_request["filenames"]
outputs = list()
if len(filenames) == 1:
outputs.append(parse(repo_root, rel_package_path, filenames[0]))
outputs.append(parse(repo_root, rel_package_path,
filenames[0]))
else:
futures = [
executor.submit(parse, repo_root, rel_package_path, filename)
for filename in filenames
executor.submit(parse, repo_root, rel_package_path,
filename) for filename in filenames
if filename != ""
]
for future in concurrent.futures.as_completed(futures):
Expand Down
7 changes: 5 additions & 2 deletions gazelle/parser.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,13 +133,13 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) {
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}

// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) {
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}

Expand DownExpand Up@@ -170,6 +170,9 @@ type module struct {
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}

// moduleComparator compares modules by name.
Expand Down
187 changes: 107 additions & 80 deletions gazelle/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,99 +140,126 @@ func (py *Resolver) Resolve(
it := modules.Iterator()
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
hasFatalError := false
MODULE_LOOP:
MODULES_LOOP:
for it.Next() {
mod := it.Value().(module)
imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
moduleParts := strings.Split(mod.Name, ".")
possibleModules := []string{mod.Name}
for len(moduleParts) > 1 {
// Iterate back through the possible imports until
// a match is found.
// For example, "from foo.bar import baz" where bar is a variable, we should try
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
// in the directory `foo/bar`.
// Or, the import could be variable `bar` in file `foo/bar.py`.
// The import could also be from a standard module, e.g. `six.moves`, where
// the dependency is actually `six`.
moduleParts = moduleParts[:len(moduleParts)-1]
possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
}
errs := []error{}
POSSIBLE_MODULE_LOOP:
for _, moduleName := range possibleModules {
imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
}
} else {
if dep, ok := cfg.FindThirdPartyDependency(mod.Name); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, dep)
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
continue MODULES_LOOP
}
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(mod); err != nil {
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
} else if isStd {
continue MODULE_LOOP
if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
}
if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
mod.Name, mod.LineNumber, mod.Filepath,
)
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
hasFatalError = true
continue MODULE_LOOP
continue MODULES_LOOP
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
log.Println("Error checking if standard module: ", err)
hasFatalError = true
continue POSSIBLE_MODULE_LOOP
} else if isStd {
continue MODULES_LOOP
} else if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
moduleName, mod.LineNumber, mod.Filepath,
)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
}
}
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULE_LOOP
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULES_LOOP
}
filteredMatches = append(filteredMatches, match)
}
filteredMatches = append(filteredMatches, match)
}
if len(filteredMatches) == 0 {
continue
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
if len(filteredMatches) == 0 {
continue POSSIBLE_MODULE_LOOP
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
}
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
filteredMatches = sameRootMatches
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
filteredMatches = sameRootMatches
}
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
continue MODULES_LOOP
}
}
} // End possible modules loop.
if len(errs) > 0 {
// If, after trying all possible modules, we still haven't found anything, error out.
joinedErrs := ""
for _, err := range errs {
joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
}
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
hasFatalError = true
}
}
if hasFatalError {
Expand Down
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
7 changes: 7 additions & 0 deletions gazelle/testdata/from_imports/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
# From Imports

This test case simulates imports of the form:

```python
from foo import bar
```
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/WORKSPACE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@

8 changes: 8 additions & 0 deletions gazelle/testdata/from_imports/foo/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
foo = "foo"
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
bar = "bar"
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/baz.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
baz = "baz"
5 changes: 5 additions & 0 deletions gazelle/testdata/from_imports/gazelle_python.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
manifest:
modules_mapping:
boto3: rootboto3
boto4: rootboto4
pip_deps_repository_name: root_pip_deps
Empty file.
9 changes: 9 additions & 0 deletions gazelle/testdata/from_imports/import_from_init_py/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_init_py",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = ["//foo/bar"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# bar is a variable inside foo/bar/__init__.py
from foo.bar import bar
Empty file.
12 changes: 12 additions & 0 deletions gazelle/testdata/from_imports/import_from_multiple/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_multiple",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = [
"//foo/bar",
"//foo/bar:baz",
],
)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions gazelle/parse.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,18 @@ def parse_import_statements(content, filepath):
"name": subnode.name,
"lineno": node.lineno,
"filepath": filepath,
"from": ""
}
modules.append(module)
elif isinstance(node, ast.ImportFrom) and node.level == 0:
module = {
"name": node.module,
"lineno": node.lineno,
"filepath": filepath,
}
modules.append(module)
for subnode in node.names:
module = {
"name": f"{node.module}.{subnode.name}",
"lineno": node.lineno,
"filepath": filepath,
"from": node.module
}
modules.append(module)
return modules


Expand All@@ -47,9 +50,10 @@ def parse(repo_root, rel_package_path, filename):
abs_filepath = os.path.join(repo_root, rel_filepath)
with open(abs_filepath, "r") as file:
content = file.read()
# From simple benchmarks, 2 workers gave the best performance here.
# From simple benchmarks, 2 workers gave the best performance here.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
modules_future = executor.submit(parse_import_statements, content, rel_filepath)
modules_future = executor.submit(parse_import_statements, content,
rel_filepath)
comments_future = executor.submit(parse_comments, content)
modules = modules_future.result()
comments = comments_future.result()
Expand All@@ -69,11 +73,12 @@ def main(stdin, stdout):
filenames = parse_request["filenames"]
outputs = list()
if len(filenames) == 1:
outputs.append(parse(repo_root, rel_package_path, filenames[0]))
outputs.append(parse(repo_root, rel_package_path,
filenames[0]))
else:
futures = [
executor.submit(parse, repo_root, rel_package_path, filename)
for filename in filenames
executor.submit(parse, repo_root, rel_package_path,
filename) for filename in filenames
if filename != ""
]
for future in concurrent.futures.as_completed(futures):
Expand Down
7 changes: 5 additions & 2 deletions gazelle/parser.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,13 +133,13 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) {
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}

// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) {
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}

Expand DownExpand Up@@ -170,6 +170,9 @@ type module struct {
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}

// moduleComparator compares modules by name.
Expand Down
187 changes: 107 additions & 80 deletions gazelle/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,99 +140,126 @@ func (py *Resolver) Resolve(
it := modules.Iterator()
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
hasFatalError := false
MODULE_LOOP:
MODULES_LOOP:
for it.Next() {
mod := it.Value().(module)
imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
moduleParts := strings.Split(mod.Name, ".")
possibleModules := []string{mod.Name}
for len(moduleParts) > 1 {
// Iterate back through the possible imports until
// a match is found.
// For example, "from foo.bar import baz" where bar is a variable, we should try
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
// in the directory `foo/bar`.
// Or, the import could be variable `bar` in file `foo/bar.py`.
// The import could also be from a standard module, e.g. `six.moves`, where
// the dependency is actually `six`.
moduleParts = moduleParts[:len(moduleParts)-1]
possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
}
errs := []error{}
POSSIBLE_MODULE_LOOP:
for _, moduleName := range possibleModules {
imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
}
} else {
if dep, ok := cfg.FindThirdPartyDependency(mod.Name); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, dep)
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
continue MODULES_LOOP
}
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(mod); err != nil {
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
} else if isStd {
continue MODULE_LOOP
if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
}
if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
mod.Name, mod.LineNumber, mod.Filepath,
)
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
hasFatalError = true
continue MODULE_LOOP
continue MODULES_LOOP
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
log.Println("Error checking if standard module: ", err)
hasFatalError = true
continue POSSIBLE_MODULE_LOOP
} else if isStd {
continue MODULES_LOOP
} else if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
moduleName, mod.LineNumber, mod.Filepath,
)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
}
}
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULE_LOOP
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULES_LOOP
}
filteredMatches = append(filteredMatches, match)
}
filteredMatches = append(filteredMatches, match)
}
if len(filteredMatches) == 0 {
continue
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
if len(filteredMatches) == 0 {
continue POSSIBLE_MODULE_LOOP
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
}
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
filteredMatches = sameRootMatches
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
filteredMatches = sameRootMatches
}
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
continue MODULES_LOOP
}
}
} // End possible modules loop.
if len(errs) > 0 {
// If, after trying all possible modules, we still haven't found anything, error out.
joinedErrs := ""
for _, err := range errs {
joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
}
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
hasFatalError = true
}
}
if hasFatalError {
Expand Down
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
7 changes: 7 additions & 0 deletions gazelle/testdata/from_imports/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
# From Imports

This test case simulates imports of the form:

```python
from foo import bar
```
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/WORKSPACE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@

8 changes: 8 additions & 0 deletions gazelle/testdata/from_imports/foo/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
foo = "foo"
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
bar = "bar"
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/baz.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
baz = "baz"
5 changes: 5 additions & 0 deletions gazelle/testdata/from_imports/gazelle_python.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
manifest:
modules_mapping:
boto3: rootboto3
boto4: rootboto4
pip_deps_repository_name: root_pip_deps
Empty file.
9 changes: 9 additions & 0 deletions gazelle/testdata/from_imports/import_from_init_py/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_init_py",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = ["//foo/bar"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# bar is a variable inside foo/bar/__init__.py
from foo.bar import bar
Empty file.
12 changes: 12 additions & 0 deletions gazelle/testdata/from_imports/import_from_multiple/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_multiple",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = [
"//foo/bar",
"//foo/bar:baz",
],
)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions gazelle/parse.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,18 @@ def parse_import_statements(content, filepath):
"name": subnode.name,
"lineno": node.lineno,
"filepath": filepath,
"from": ""
}
modules.append(module)
elif isinstance(node, ast.ImportFrom) and node.level == 0:
module = {
"name": node.module,
"lineno": node.lineno,
"filepath": filepath,
}
modules.append(module)
for subnode in node.names:
module = {
"name": f"{node.module}.{subnode.name}",
"lineno": node.lineno,
"filepath": filepath,
"from": node.module
}
modules.append(module)
return modules


Expand All@@ -47,9 +50,10 @@ def parse(repo_root, rel_package_path, filename):
abs_filepath = os.path.join(repo_root, rel_filepath)
with open(abs_filepath, "r") as file:
content = file.read()
# From simple benchmarks, 2 workers gave the best performance here.
# From simple benchmarks, 2 workers gave the best performance here.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
modules_future = executor.submit(parse_import_statements, content, rel_filepath)
modules_future = executor.submit(parse_import_statements, content,
rel_filepath)
comments_future = executor.submit(parse_comments, content)
modules = modules_future.result()
comments = comments_future.result()
Expand All@@ -69,11 +73,12 @@ def main(stdin, stdout):
filenames = parse_request["filenames"]
outputs = list()
if len(filenames) == 1:
outputs.append(parse(repo_root, rel_package_path, filenames[0]))
outputs.append(parse(repo_root, rel_package_path,
filenames[0]))
else:
futures = [
executor.submit(parse, repo_root, rel_package_path, filename)
for filename in filenames
executor.submit(parse, repo_root, rel_package_path,
filename) for filename in filenames
if filename != ""
]
for future in concurrent.futures.as_completed(futures):
Expand Down
7 changes: 5 additions & 2 deletions gazelle/parser.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,13 +133,13 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) {
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}

// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) {
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}

Expand DownExpand Up@@ -170,6 +170,9 @@ type module struct {
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}

// moduleComparator compares modules by name.
Expand Down
187 changes: 107 additions & 80 deletions gazelle/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,99 +140,126 @@ func (py *Resolver) Resolve(
it := modules.Iterator()
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
hasFatalError := false
MODULE_LOOP:
MODULES_LOOP:
for it.Next() {
mod := it.Value().(module)
imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
moduleParts := strings.Split(mod.Name, ".")
possibleModules := []string{mod.Name}
for len(moduleParts) > 1 {
// Iterate back through the possible imports until
// a match is found.
// For example, "from foo.bar import baz" where bar is a variable, we should try
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
// in the directory `foo/bar`.
// Or, the import could be variable `bar` in file `foo/bar.py`.
// The import could also be from a standard module, e.g. `six.moves`, where
// the dependency is actually `six`.
moduleParts = moduleParts[:len(moduleParts)-1]
possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
}
errs := []error{}
POSSIBLE_MODULE_LOOP:
for _, moduleName := range possibleModules {
imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
}
} else {
if dep, ok := cfg.FindThirdPartyDependency(mod.Name); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, dep)
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
continue MODULES_LOOP
}
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(mod); err != nil {
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
} else if isStd {
continue MODULE_LOOP
if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
}
if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
mod.Name, mod.LineNumber, mod.Filepath,
)
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
hasFatalError = true
continue MODULE_LOOP
continue MODULES_LOOP
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
log.Println("Error checking if standard module: ", err)
hasFatalError = true
continue POSSIBLE_MODULE_LOOP
} else if isStd {
continue MODULES_LOOP
} else if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
moduleName, mod.LineNumber, mod.Filepath,
)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
}
}
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULE_LOOP
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULES_LOOP
}
filteredMatches = append(filteredMatches, match)
}
filteredMatches = append(filteredMatches, match)
}
if len(filteredMatches) == 0 {
continue
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
if len(filteredMatches) == 0 {
continue POSSIBLE_MODULE_LOOP
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
}
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
filteredMatches = sameRootMatches
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
filteredMatches = sameRootMatches
}
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
continue MODULES_LOOP
}
}
} // End possible modules loop.
if len(errs) > 0 {
// If, after trying all possible modules, we still haven't found anything, error out.
joinedErrs := ""
for _, err := range errs {
joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
}
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
hasFatalError = true
}
}
if hasFatalError {
Expand Down
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
7 changes: 7 additions & 0 deletions gazelle/testdata/from_imports/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
# From Imports

This test case simulates imports of the form:

```python
from foo import bar
```
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/WORKSPACE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@

8 changes: 8 additions & 0 deletions gazelle/testdata/from_imports/foo/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
foo = "foo"
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
bar = "bar"
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/baz.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
baz = "baz"
5 changes: 5 additions & 0 deletions gazelle/testdata/from_imports/gazelle_python.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
manifest:
modules_mapping:
boto3: rootboto3
boto4: rootboto4
pip_deps_repository_name: root_pip_deps
Empty file.
9 changes: 9 additions & 0 deletions gazelle/testdata/from_imports/import_from_init_py/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_init_py",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = ["//foo/bar"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# bar is a variable inside foo/bar/__init__.py
from foo.bar import bar
Empty file.
12 changes: 12 additions & 0 deletions gazelle/testdata/from_imports/import_from_multiple/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_multiple",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = [
"//foo/bar",
"//foo/bar:baz",
],
)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions gazelle/parse.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,18 @@ def parse_import_statements(content, filepath):
"name": subnode.name,
"lineno": node.lineno,
"filepath": filepath,
"from": ""
}
modules.append(module)
elif isinstance(node, ast.ImportFrom) and node.level == 0:
module = {
"name": node.module,
"lineno": node.lineno,
"filepath": filepath,
}
modules.append(module)
for subnode in node.names:
module = {
"name": f"{node.module}.{subnode.name}",
"lineno": node.lineno,
"filepath": filepath,
"from": node.module
}
modules.append(module)
return modules


Expand All@@ -47,9 +50,10 @@ def parse(repo_root, rel_package_path, filename):
abs_filepath = os.path.join(repo_root, rel_filepath)
with open(abs_filepath, "r") as file:
content = file.read()
# From simple benchmarks, 2 workers gave the best performance here.
# From simple benchmarks, 2 workers gave the best performance here.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
modules_future = executor.submit(parse_import_statements, content, rel_filepath)
modules_future = executor.submit(parse_import_statements, content,
rel_filepath)
comments_future = executor.submit(parse_comments, content)
modules = modules_future.result()
comments = comments_future.result()
Expand All@@ -69,11 +73,12 @@ def main(stdin, stdout):
filenames = parse_request["filenames"]
outputs = list()
if len(filenames) == 1:
outputs.append(parse(repo_root, rel_package_path, filenames[0]))
outputs.append(parse(repo_root, rel_package_path,
filenames[0]))
else:
futures = [
executor.submit(parse, repo_root, rel_package_path, filename)
for filename in filenames
executor.submit(parse, repo_root, rel_package_path,
filename) for filename in filenames
if filename != ""
]
for future in concurrent.futures.as_completed(futures):
Expand Down
7 changes: 5 additions & 2 deletions gazelle/parser.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,13 +133,13 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) {
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}

// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) {
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}

Expand DownExpand Up@@ -170,6 +170,9 @@ type module struct {
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}

// moduleComparator compares modules by name.
Expand Down
187 changes: 107 additions & 80 deletions gazelle/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,99 +140,126 @@ func (py *Resolver) Resolve(
it := modules.Iterator()
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
hasFatalError := false
MODULE_LOOP:
MODULES_LOOP:
for it.Next() {
mod := it.Value().(module)
imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
moduleParts := strings.Split(mod.Name, ".")
possibleModules := []string{mod.Name}
for len(moduleParts) > 1 {
// Iterate back through the possible imports until
// a match is found.
// For example, "from foo.bar import baz" where bar is a variable, we should try
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
// in the directory `foo/bar`.
// Or, the import could be variable `bar` in file `foo/bar.py`.
// The import could also be from a standard module, e.g. `six.moves`, where
// the dependency is actually `six`.
moduleParts = moduleParts[:len(moduleParts)-1]
possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
}
errs := []error{}
POSSIBLE_MODULE_LOOP:
for _, moduleName := range possibleModules {
imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
}
} else {
if dep, ok := cfg.FindThirdPartyDependency(mod.Name); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, dep)
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
continue MODULES_LOOP
}
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(mod); err != nil {
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
} else if isStd {
continue MODULE_LOOP
if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
}
if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
mod.Name, mod.LineNumber, mod.Filepath,
)
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
hasFatalError = true
continue MODULE_LOOP
continue MODULES_LOOP
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
log.Println("Error checking if standard module: ", err)
hasFatalError = true
continue POSSIBLE_MODULE_LOOP
} else if isStd {
continue MODULES_LOOP
} else if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
moduleName, mod.LineNumber, mod.Filepath,
)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
}
}
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULE_LOOP
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULES_LOOP
}
filteredMatches = append(filteredMatches, match)
}
filteredMatches = append(filteredMatches, match)
}
if len(filteredMatches) == 0 {
continue
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
if len(filteredMatches) == 0 {
continue POSSIBLE_MODULE_LOOP
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
}
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
filteredMatches = sameRootMatches
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
filteredMatches = sameRootMatches
}
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
continue MODULES_LOOP
}
}
} // End possible modules loop.
if len(errs) > 0 {
// If, after trying all possible modules, we still haven't found anything, error out.
joinedErrs := ""
for _, err := range errs {
joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
}
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
hasFatalError = true
}
}
if hasFatalError {
Expand Down
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
7 changes: 7 additions & 0 deletions gazelle/testdata/from_imports/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
# From Imports

This test case simulates imports of the form:

```python
from foo import bar
```
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/WORKSPACE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@

8 changes: 8 additions & 0 deletions gazelle/testdata/from_imports/foo/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
foo = "foo"
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
bar = "bar"
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/baz.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
baz = "baz"
5 changes: 5 additions & 0 deletions gazelle/testdata/from_imports/gazelle_python.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
manifest:
modules_mapping:
boto3: rootboto3
boto4: rootboto4
pip_deps_repository_name: root_pip_deps
Empty file.
9 changes: 9 additions & 0 deletions gazelle/testdata/from_imports/import_from_init_py/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_init_py",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = ["//foo/bar"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# bar is a variable inside foo/bar/__init__.py
from foo.bar import bar
Empty file.
12 changes: 12 additions & 0 deletions gazelle/testdata/from_imports/import_from_multiple/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_multiple",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = [
"//foo/bar",
"//foo/bar:baz",
],
)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions gazelle/parse.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,18 @@ def parse_import_statements(content, filepath):
"name": subnode.name,
"lineno": node.lineno,
"filepath": filepath,
"from": ""
}
modules.append(module)
elif isinstance(node, ast.ImportFrom) and node.level == 0:
module = {
"name": node.module,
"lineno": node.lineno,
"filepath": filepath,
}
modules.append(module)
for subnode in node.names:
module = {
"name": f"{node.module}.{subnode.name}",
"lineno": node.lineno,
"filepath": filepath,
"from": node.module
}
modules.append(module)
return modules


Expand All@@ -47,9 +50,10 @@ def parse(repo_root, rel_package_path, filename):
abs_filepath = os.path.join(repo_root, rel_filepath)
with open(abs_filepath, "r") as file:
content = file.read()
# From simple benchmarks, 2 workers gave the best performance here.
# From simple benchmarks, 2 workers gave the best performance here.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
modules_future = executor.submit(parse_import_statements, content, rel_filepath)
modules_future = executor.submit(parse_import_statements, content,
rel_filepath)
comments_future = executor.submit(parse_comments, content)
modules = modules_future.result()
comments = comments_future.result()
Expand All@@ -69,11 +73,12 @@ def main(stdin, stdout):
filenames = parse_request["filenames"]
outputs = list()
if len(filenames) == 1:
outputs.append(parse(repo_root, rel_package_path, filenames[0]))
outputs.append(parse(repo_root, rel_package_path,
filenames[0]))
else:
futures = [
executor.submit(parse, repo_root, rel_package_path, filename)
for filename in filenames
executor.submit(parse, repo_root, rel_package_path,
filename) for filename in filenames
if filename != ""
]
for future in concurrent.futures.as_completed(futures):
Expand Down
7 changes: 5 additions & 2 deletions gazelle/parser.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,13 +133,13 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) {
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}

// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) {
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}

Expand DownExpand Up@@ -170,6 +170,9 @@ type module struct {
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}

// moduleComparator compares modules by name.
Expand Down
187 changes: 107 additions & 80 deletions gazelle/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,99 +140,126 @@ func (py *Resolver) Resolve(
it := modules.Iterator()
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
hasFatalError := false
MODULE_LOOP:
MODULES_LOOP:
for it.Next() {
mod := it.Value().(module)
imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
moduleParts := strings.Split(mod.Name, ".")
possibleModules := []string{mod.Name}
for len(moduleParts) > 1 {
// Iterate back through the possible imports until
// a match is found.
// For example, "from foo.bar import baz" where bar is a variable, we should try
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
// in the directory `foo/bar`.
// Or, the import could be variable `bar` in file `foo/bar.py`.
// The import could also be from a standard module, e.g. `six.moves`, where
// the dependency is actually `six`.
moduleParts = moduleParts[:len(moduleParts)-1]
possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
}
errs := []error{}
POSSIBLE_MODULE_LOOP:
for _, moduleName := range possibleModules {
imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
}
} else {
if dep, ok := cfg.FindThirdPartyDependency(mod.Name); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, dep)
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
continue MODULES_LOOP
}
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(mod); err != nil {
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
} else if isStd {
continue MODULE_LOOP
if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
}
if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
mod.Name, mod.LineNumber, mod.Filepath,
)
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
hasFatalError = true
continue MODULE_LOOP
continue MODULES_LOOP
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
log.Println("Error checking if standard module: ", err)
hasFatalError = true
continue POSSIBLE_MODULE_LOOP
} else if isStd {
continue MODULES_LOOP
} else if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
moduleName, mod.LineNumber, mod.Filepath,
)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
}
}
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULE_LOOP
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULES_LOOP
}
filteredMatches = append(filteredMatches, match)
}
filteredMatches = append(filteredMatches, match)
}
if len(filteredMatches) == 0 {
continue
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
if len(filteredMatches) == 0 {
continue POSSIBLE_MODULE_LOOP
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
}
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
filteredMatches = sameRootMatches
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
filteredMatches = sameRootMatches
}
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
continue MODULES_LOOP
}
}
} // End possible modules loop.
if len(errs) > 0 {
// If, after trying all possible modules, we still haven't found anything, error out.
joinedErrs := ""
for _, err := range errs {
joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
}
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
hasFatalError = true
}
}
if hasFatalError {
Expand Down
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
7 changes: 7 additions & 0 deletions gazelle/testdata/from_imports/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
# From Imports

This test case simulates imports of the form:

```python
from foo import bar
```
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/WORKSPACE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@

8 changes: 8 additions & 0 deletions gazelle/testdata/from_imports/foo/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
foo = "foo"
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
bar = "bar"
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/baz.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
baz = "baz"
5 changes: 5 additions & 0 deletions gazelle/testdata/from_imports/gazelle_python.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
manifest:
modules_mapping:
boto3: rootboto3
boto4: rootboto4
pip_deps_repository_name: root_pip_deps
Empty file.
9 changes: 9 additions & 0 deletions gazelle/testdata/from_imports/import_from_init_py/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_init_py",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = ["//foo/bar"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# bar is a variable inside foo/bar/__init__.py
from foo.bar import bar
Empty file.
12 changes: 12 additions & 0 deletions gazelle/testdata/from_imports/import_from_multiple/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_multiple",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = [
"//foo/bar",
"//foo/bar:baz",
],
)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions gazelle/parse.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,18 @@ def parse_import_statements(content, filepath):
"name": subnode.name,
"lineno": node.lineno,
"filepath": filepath,
"from": ""
}
modules.append(module)
elif isinstance(node, ast.ImportFrom) and node.level == 0:
module = {
"name": node.module,
"lineno": node.lineno,
"filepath": filepath,
}
modules.append(module)
for subnode in node.names:
module = {
"name": f"{node.module}.{subnode.name}",
"lineno": node.lineno,
"filepath": filepath,
"from": node.module
}
modules.append(module)
return modules


Expand All@@ -47,9 +50,10 @@ def parse(repo_root, rel_package_path, filename):
abs_filepath = os.path.join(repo_root, rel_filepath)
with open(abs_filepath, "r") as file:
content = file.read()
# From simple benchmarks, 2 workers gave the best performance here.
# From simple benchmarks, 2 workers gave the best performance here.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
modules_future = executor.submit(parse_import_statements, content, rel_filepath)
modules_future = executor.submit(parse_import_statements, content,
rel_filepath)
comments_future = executor.submit(parse_comments, content)
modules = modules_future.result()
comments = comments_future.result()
Expand All@@ -69,11 +73,12 @@ def main(stdin, stdout):
filenames = parse_request["filenames"]
outputs = list()
if len(filenames) == 1:
outputs.append(parse(repo_root, rel_package_path, filenames[0]))
outputs.append(parse(repo_root, rel_package_path,
filenames[0]))
else:
futures = [
executor.submit(parse, repo_root, rel_package_path, filename)
for filename in filenames
executor.submit(parse, repo_root, rel_package_path,
filename) for filename in filenames
if filename != ""
]
for future in concurrent.futures.as_completed(futures):
Expand Down
7 changes: 5 additions & 2 deletions gazelle/parser.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,13 +133,13 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) {
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}

// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) {
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}

Expand DownExpand Up@@ -170,6 +170,9 @@ type module struct {
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}

// moduleComparator compares modules by name.
Expand Down
187 changes: 107 additions & 80 deletions gazelle/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,99 +140,126 @@ func (py *Resolver) Resolve(
it := modules.Iterator()
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
hasFatalError := false
MODULE_LOOP:
MODULES_LOOP:
for it.Next() {
mod := it.Value().(module)
imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
moduleParts := strings.Split(mod.Name, ".")
possibleModules := []string{mod.Name}
for len(moduleParts) > 1 {
// Iterate back through the possible imports until
// a match is found.
// For example, "from foo.bar import baz" where bar is a variable, we should try
// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
// in the directory `foo/bar`.
// Or, the import could be variable `bar` in file `foo/bar.py`.
// The import could also be from a standard module, e.g. `six.moves`, where
// the dependency is actually `six`.
moduleParts = moduleParts[:len(moduleParts)-1]
possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
}
errs := []error{}
POSSIBLE_MODULE_LOOP:
for _, moduleName := range possibleModules {
imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
if override.Repo == "" {
override.Repo = from.Repo
}
}
} else {
if dep, ok := cfg.FindThirdPartyDependency(mod.Name); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, dep)
if !override.Equal(from) {
if override.Repo == from.Repo {
override.Repo = ""
}
dep := override.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves using the \"gazelle:resolve\" directive.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
continue MODULES_LOOP
}
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(mod); err != nil {
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
} else if isStd {
continue MODULE_LOOP
if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the third-party module %q from the wheel %q.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
}
if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
mod.Name, mod.LineNumber, mod.Filepath,
)
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
hasFatalError = true
continue MODULE_LOOP
continue MODULES_LOOP
} else {
matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
if len(matches) == 0 {
// Check if the imported module is part of the standard library.
if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
log.Println("Error checking if standard module: ", err)
hasFatalError = true
continue POSSIBLE_MODULE_LOOP
} else if isStd {
continue MODULES_LOOP
} else if cfg.ValidateImportStatements() {
err := fmt.Errorf(
"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
"\t1. Add it as a dependency in the requirements.txt file.\n"+
"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
moduleName, mod.LineNumber, mod.Filepath,
)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
}
}
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULE_LOOP
filteredMatches := make([]resolve.FindResult, 0, len(matches))
for _, match := range matches {
if match.IsSelfImport(from) {
// Prevent from adding itself as a dependency.
continue MODULES_LOOP
}
filteredMatches = append(filteredMatches, match)
}
filteredMatches = append(filteredMatches, match)
}
if len(filteredMatches) == 0 {
continue
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
if len(filteredMatches) == 0 {
continue POSSIBLE_MODULE_LOOP
}
if len(filteredMatches) > 1 {
sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
for _, match := range filteredMatches {
if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
sameRootMatches = append(sameRootMatches, match)
}
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
errs = append(errs, err)
continue POSSIBLE_MODULE_LOOP
}
filteredMatches = sameRootMatches
}
if len(sameRootMatches) != 1 {
err := fmt.Errorf(
"multiple targets (%s) may be imported with %q at line %d in %q "+
"- this must be fixed using the \"gazelle:resolve\" directive",
targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
log.Println("ERROR: ", err)
hasFatalError = true
continue MODULE_LOOP
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
}
filteredMatches = sameRootMatches
}
matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
dep := matchLabel.String()
deps.Add(dep)
if explainDependency == dep {
log.Printf("Explaining dependency (%s): "+
"in the target %q, the file %q imports %q at line %d, "+
"which resolves from the first-party indexed labels.\n",
explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
continue MODULES_LOOP
}
}
} // End possible modules loop.
if len(errs) > 0 {
// If, after trying all possible modules, we still haven't found anything, error out.
joinedErrs := ""
for _, err := range errs {
joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
}
log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
hasFatalError = true
}
}
if hasFatalError {
Expand Down
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_extension enabled
7 changes: 7 additions & 0 deletions gazelle/testdata/from_imports/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
# From Imports

This test case simulates imports of the form:

```python
from foo import bar
```
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/WORKSPACE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@

8 changes: 8 additions & 0 deletions gazelle/testdata/from_imports/foo/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
foo = "foo"
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
21 changes: 21 additions & 0 deletions gazelle/testdata/from_imports/foo/bar/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library")

# gazelle:python_ignore_files baz.py

py_library(
name = "baz",
srcs = [
"baz.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)

py_library(
name = "bar",
srcs = [
"__init__.py",
],
imports = ["../.."],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
bar = "bar"
1 change: 1 addition & 0 deletions gazelle/testdata/from_imports/foo/bar/baz.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
baz = "baz"
5 changes: 5 additions & 0 deletions gazelle/testdata/from_imports/gazelle_python.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
manifest:
modules_mapping:
boto3: rootboto3
boto4: rootboto4
pip_deps_repository_name: root_pip_deps
Empty file.
9 changes: 9 additions & 0 deletions gazelle/testdata/from_imports/import_from_init_py/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_init_py",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = ["//foo/bar"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# bar is a variable inside foo/bar/__init__.py
from foo.bar import bar
Empty file.
12 changes: 12 additions & 0 deletions gazelle/testdata/from_imports/import_from_multiple/BUILD.out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "import_from_multiple",
srcs = ["__init__.py"],
imports = [".."],
visibility = ["//:__subpackages__"],
deps = [
"//foo/bar",
"//foo/bar:baz",
],
)
Loading