Skip to content

Commit 6f27511

Browse files
yushan26yushan8dougthor42
authored
fix(gazelle) Update gazelle to properly process multi-line python imports (bazel-contrib#3077)
A python import may be imported as: ``` from foo.bar.application.\ pipeline.model import ( Baz ) ``` However, gazelle fails to resolve this import with the error: `line 30: "foo.bar.application.pipeline.model\\\n pipeline.mode.Baz" is an invalid dependency:` Clean up the imports such that whitespace and \n are removed from the import path. --------- Co-authored-by: yushan <yushan@uber.com> Co-authored-by: Douglas Thor <dougthor42@users.noreply.github.com>
1 parent c30980a commit 6f27511

File tree

4 files changed

+112
-1
lines changed

4 files changed

+112
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ END_UNRELEASED_TEMPLATE
8787
({gh-issue}`3043`).
8888
* (pypi) The pipstar `defaults` configuration now supports any custom platform
8989
name.
90+
* Multi-line python imports (e.g. with escaped newlines) are now correctly processed by Gazelle.
9091

9192
{#v0-0-0-added}
9293
### Added

gazelle/python/file_parser.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ func parseImportStatement(node *sitter.Node, code []byte) (Module, bool) {
144144
return Module{}, false
145145
}
146146

147+
// cleanImportString removes backslashes and all whitespace from the string.
148+
func cleanImportString(s string) string {
149+
s = strings.ReplaceAll(s, "\r\n", "")
150+
s = strings.ReplaceAll(s, "\\", "")
151+
s = strings.ReplaceAll(s, " ", "")
152+
s = strings.ReplaceAll(s, "\n", "")
153+
s = strings.ReplaceAll(s, "\t", "")
154+
return s
155+
}
156+
147157
// parseImportStatements parses a node for import statements, returning true if the node is
148158
// an import statement. It updates FileParser.output.Modules with the `module` that the
149159
// import represents.
@@ -154,6 +164,8 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool {
154164
if !ok {
155165
continue
156166
}
167+
m.From = cleanImportString(m.From)
168+
m.Name = cleanImportString(m.Name)
157169
m.Filepath = p.relFilepath
158170
m.TypeCheckingOnly = p.inTypeCheckingBlock
159171
if strings.HasPrefix(m.Name, ".") {
@@ -163,6 +175,7 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool {
163175
}
164176
} else if node.Type() == sitterNodeTypeImportFromStatement {
165177
from := node.Child(1).Content(p.code)
178+
from = cleanImportString(from)
166179
// If the import is from the current package, we don't need to add it to the modules i.e. from . import Class1.
167180
// If the import is from a different relative package i.e. from .package1 import foo, we need to add it to the modules.
168181
if from == "." {
@@ -175,6 +188,7 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool {
175188
}
176189
m.Filepath = p.relFilepath
177190
m.From = from
191+
m.Name = cleanImportString(m.Name)
178192
m.Name = fmt.Sprintf("%s.%s", from, m.Name)
179193
m.TypeCheckingOnly = p.inTypeCheckingBlock
180194
p.output.Modules = append(p.output.Modules, m)

gazelle/python/file_parser_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,95 @@ def example_function():
291291
}
292292
}
293293
}
294+
295+
func TestParseImportStatements_MultilineWithBackslashAndWhitespace(t *testing.T) {
296+
t.Parallel()
297+
t.Run("multiline from import", func(t *testing.T) {
298+
p := NewFileParser()
299+
code := []byte(`from foo.bar.\
300+
baz import (
301+
Something,
302+
AnotherThing
303+
)
304+
305+
from foo\
306+
.test import (
307+
Foo,
308+
Bar
309+
)
310+
`)
311+
p.SetCodeAndFile(code, "", "test.py")
312+
output, err := p.Parse(context.Background())
313+
assert.NoError(t, err)
314+
// Updated expected to match parser output
315+
expected := []Module{
316+
{
317+
Name: "foo.bar.baz.Something",
318+
LineNumber: 3,
319+
Filepath: "test.py",
320+
From: "foo.bar.baz",
321+
},
322+
{
323+
Name: "foo.bar.baz.AnotherThing",
324+
LineNumber: 4,
325+
Filepath: "test.py",
326+
From: "foo.bar.baz",
327+
},
328+
{
329+
Name: "foo.test.Foo",
330+
LineNumber: 9,
331+
Filepath: "test.py",
332+
From: "foo.test",
333+
},
334+
{
335+
Name: "foo.test.Bar",
336+
LineNumber: 10,
337+
Filepath: "test.py",
338+
From: "foo.test",
339+
},
340+
}
341+
assert.ElementsMatch(t, expected, output.Modules)
342+
})
343+
t.Run("multiline import", func(t *testing.T) {
344+
p := NewFileParser()
345+
code := []byte(`import foo.bar.\
346+
baz
347+
`)
348+
p.SetCodeAndFile(code, "", "test.py")
349+
output, err := p.Parse(context.Background())
350+
assert.NoError(t, err)
351+
// Updated expected to match parser output
352+
expected := []Module{
353+
{
354+
Name: "foo.bar.baz",
355+
LineNumber: 1,
356+
Filepath: "test.py",
357+
From: "",
358+
},
359+
}
360+
assert.ElementsMatch(t, expected, output.Modules)
361+
})
362+
t.Run("windows line endings", func(t *testing.T) {
363+
p := NewFileParser()
364+
code := []byte("from foo.bar.\r\n baz import (\r\n Something,\r\n AnotherThing\r\n)\r\n")
365+
p.SetCodeAndFile(code, "", "test.py")
366+
output, err := p.Parse(context.Background())
367+
assert.NoError(t, err)
368+
// Updated expected to match parser output
369+
expected := []Module{
370+
{
371+
Name: "foo.bar.baz.Something",
372+
LineNumber: 3,
373+
Filepath: "test.py",
374+
From: "foo.bar.baz",
375+
},
376+
{
377+
Name: "foo.bar.baz.AnotherThing",
378+
LineNumber: 4,
379+
Filepath: "test.py",
380+
From: "foo.bar.baz",
381+
},
382+
}
383+
assert.ElementsMatch(t, expected, output.Modules)
384+
})
385+
}

gazelle/python/testdata/from_imports/import_nested_var/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
# limitations under the License.
1414

1515
# baz is a variable in foo/bar/baz.py
16-
from foo.bar.baz import baz
16+
from foo\
17+
.bar.\
18+
baz import (
19+
baz
20+
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy