fix(gazelle) Update gazelle to properly process multi-line python imports (#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>diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad68669..834a2c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -87,6 +87,7 @@
({gh-issue}`3043`).
* (pypi) The pipstar `defaults` configuration now supports any custom platform
name.
+* Multi-line python imports (e.g. with escaped newlines) are now correctly processed by Gazelle.
{#v0-0-0-added}
### Added
diff --git a/gazelle/python/file_parser.go b/gazelle/python/file_parser.go
index 31fce02..e129337 100644
--- a/gazelle/python/file_parser.go
+++ b/gazelle/python/file_parser.go
@@ -144,6 +144,16 @@
return Module{}, false
}
+// cleanImportString removes backslashes and all whitespace from the string.
+func cleanImportString(s string) string {
+ s = strings.ReplaceAll(s, "\r\n", "")
+ s = strings.ReplaceAll(s, "\\", "")
+ s = strings.ReplaceAll(s, " ", "")
+ s = strings.ReplaceAll(s, "\n", "")
+ s = strings.ReplaceAll(s, "\t", "")
+ return s
+}
+
// parseImportStatements parses a node for import statements, returning true if the node is
// an import statement. It updates FileParser.output.Modules with the `module` that the
// import represents.
@@ -154,6 +164,8 @@
if !ok {
continue
}
+ m.From = cleanImportString(m.From)
+ m.Name = cleanImportString(m.Name)
m.Filepath = p.relFilepath
m.TypeCheckingOnly = p.inTypeCheckingBlock
if strings.HasPrefix(m.Name, ".") {
@@ -163,6 +175,7 @@
}
} else if node.Type() == sitterNodeTypeImportFromStatement {
from := node.Child(1).Content(p.code)
+ from = cleanImportString(from)
// If the import is from the current package, we don't need to add it to the modules i.e. from . import Class1.
// If the import is from a different relative package i.e. from .package1 import foo, we need to add it to the modules.
if from == "." {
@@ -175,6 +188,7 @@
}
m.Filepath = p.relFilepath
m.From = from
+ m.Name = cleanImportString(m.Name)
m.Name = fmt.Sprintf("%s.%s", from, m.Name)
m.TypeCheckingOnly = p.inTypeCheckingBlock
p.output.Modules = append(p.output.Modules, m)
diff --git a/gazelle/python/file_parser_test.go b/gazelle/python/file_parser_test.go
index f4db1a3..0a6fd1b 100644
--- a/gazelle/python/file_parser_test.go
+++ b/gazelle/python/file_parser_test.go
@@ -291,3 +291,95 @@
}
}
}
+
+func TestParseImportStatements_MultilineWithBackslashAndWhitespace(t *testing.T) {
+ t.Parallel()
+ t.Run("multiline from import", func(t *testing.T) {
+ p := NewFileParser()
+ code := []byte(`from foo.bar.\
+ baz import (
+ Something,
+ AnotherThing
+)
+
+from foo\
+ .test import (
+ Foo,
+ Bar
+)
+`)
+ p.SetCodeAndFile(code, "", "test.py")
+ output, err := p.Parse(context.Background())
+ assert.NoError(t, err)
+ // Updated expected to match parser output
+ expected := []Module{
+ {
+ Name: "foo.bar.baz.Something",
+ LineNumber: 3,
+ Filepath: "test.py",
+ From: "foo.bar.baz",
+ },
+ {
+ Name: "foo.bar.baz.AnotherThing",
+ LineNumber: 4,
+ Filepath: "test.py",
+ From: "foo.bar.baz",
+ },
+ {
+ Name: "foo.test.Foo",
+ LineNumber: 9,
+ Filepath: "test.py",
+ From: "foo.test",
+ },
+ {
+ Name: "foo.test.Bar",
+ LineNumber: 10,
+ Filepath: "test.py",
+ From: "foo.test",
+ },
+ }
+ assert.ElementsMatch(t, expected, output.Modules)
+ })
+ t.Run("multiline import", func(t *testing.T) {
+ p := NewFileParser()
+ code := []byte(`import foo.bar.\
+ baz
+`)
+ p.SetCodeAndFile(code, "", "test.py")
+ output, err := p.Parse(context.Background())
+ assert.NoError(t, err)
+ // Updated expected to match parser output
+ expected := []Module{
+ {
+ Name: "foo.bar.baz",
+ LineNumber: 1,
+ Filepath: "test.py",
+ From: "",
+ },
+ }
+ assert.ElementsMatch(t, expected, output.Modules)
+ })
+ t.Run("windows line endings", func(t *testing.T) {
+ p := NewFileParser()
+ code := []byte("from foo.bar.\r\n baz import (\r\n Something,\r\n AnotherThing\r\n)\r\n")
+ p.SetCodeAndFile(code, "", "test.py")
+ output, err := p.Parse(context.Background())
+ assert.NoError(t, err)
+ // Updated expected to match parser output
+ expected := []Module{
+ {
+ Name: "foo.bar.baz.Something",
+ LineNumber: 3,
+ Filepath: "test.py",
+ From: "foo.bar.baz",
+ },
+ {
+ Name: "foo.bar.baz.AnotherThing",
+ LineNumber: 4,
+ Filepath: "test.py",
+ From: "foo.bar.baz",
+ },
+ }
+ assert.ElementsMatch(t, expected, output.Modules)
+ })
+}
diff --git a/gazelle/python/testdata/from_imports/import_nested_var/__init__.py b/gazelle/python/testdata/from_imports/import_nested_var/__init__.py
index d0f51c4..20eda53 100644
--- a/gazelle/python/testdata/from_imports/import_nested_var/__init__.py
+++ b/gazelle/python/testdata/from_imports/import_nested_var/__init__.py
@@ -13,4 +13,8 @@
# limitations under the License.
# baz is a variable in foo/bar/baz.py
-from foo.bar.baz import baz
+from foo\
+ .bar.\
+ baz import (
+ baz
+ )