Skip to content

Commit

Permalink
fix: Fix relative to absolute import conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Jan 14, 2022
1 parent b71f7a2 commit 464c39e
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 21 deletions.
26 changes: 26 additions & 0 deletions src/griffe/agents/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from ast import Gt as NodeGt
from ast import GtE as NodeGtE
from ast import IfExp as NodeIfExp
from ast import ImportFrom as NodeImportFrom
from ast import In as NodeIn
from ast import Invert as NodeInvert
from ast import Is as NodeIs
Expand Down Expand Up @@ -64,6 +65,7 @@
from ast import UnaryOp as NodeUnaryOp
from ast import USub as NodeUSub
from ast import Yield as NodeYield
from ast import alias as NodeAlias
from ast import arguments as NodeArguments
from ast import comprehension as NodeComprehension
from ast import keyword as NodeKeyword
Expand Down Expand Up @@ -1176,3 +1178,27 @@ def get_parameter_default(node: AST, filepath: Path, lines_collection: LinesColl
return lines_collection[filepath][node.lineno - 1][node.col_offset : node.end_col_offset] # type: ignore[attr-defined]
# TODO: handle multiple line defaults
return None


# ==========================================================
# relative imports
def relative_to_absolute(node: NodeImportFrom, name: NodeAlias, current_module: Module) -> str:
"""Convert a relative import path to an absolute one.
Parameters:
node: The "from ... import ..." AST node.
name: The imported name.
current_module: The module in which the import happens.
Returns:
The absolute import path.
"""
level = node.level
if level > 0 and current_module.is_package or current_module.is_subpackage:
level -= 1
while level > 0 and current_module.parent is not None:
current_module = current_module.parent # type: ignore[assignment]
level -= 1
base = current_module.path + "." if node.level > 0 else ""
node_module = node.module + "." if node.module else ""
return base + node_module + name.name
34 changes: 14 additions & 20 deletions src/griffe/agents/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
get_parameter_default,
get_value,
parse__all__,
relative_to_absolute,
)
from griffe.collections import LinesCollection, ModulesCollection
from griffe.dataclasses import (
Expand Down Expand Up @@ -388,27 +389,20 @@ def visit_importfrom(self, node: ast.ImportFrom) -> None: # noqa: WPS231
node: The node to visit.
"""
for name in node.names:
alias_name = name.asname or name.name
level = node.level
module_path = node.module
if level > 0:
if module_path is None:
level -= 1
parent: Module = self.current.module
if parent.is_package or parent.is_subpackage:
level -= 1
while level > 0:
parent = parent.parent # type: ignore[assignment]
level -= 1
if module_path:
module_path = f"{parent.path}.{module_path}"
else:
module_path = parent.path
if alias_name == "*":
alias_name = module_path.replace(".", "/") + "/*" # type: ignore[union-attr]
alias_path = module_path
if not node.module and node.level == 1:
if not name.asname:
# special case: when being in `a` and doing `from . import b`,
# we are effectively creating a member `b` in `a` that is pointing to `a.b`
# -> cyclic alias! in that case, we just skip it, as both the member and module
# have the same name and can be accessed the same way
continue

if name.name == "*":
alias_name = node.module.replace(".", "/") + "/*" # type: ignore[union-attr]
alias_path = node.module
else:
alias_path = f"{module_path}.{name.name}"
alias_name = name.asname or name.name
alias_path = relative_to_absolute(node, name, self.current.module)
self.current.imports[alias_name] = alias_path
self.current[alias_name] = Alias(
alias_name,
Expand Down
2 changes: 1 addition & 1 deletion src/griffe/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ def is_init_module(self) -> bool:
True or False.
"""
try:
return self.filepath.name == "__init__.py"
return self.filepath.name.split(".", 1)[0] == "__init__"
except BuiltinModuleError:
return False

Expand Down
39 changes: 39 additions & 0 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Test nodes utilities."""

from ast import PyCF_ONLY_AST

import pytest

from griffe.agents.nodes import relative_to_absolute
from tests.helpers import module_vtree


@pytest.mark.parametrize(
("code", "path", "is_package", "expected"),
[
("from . import b", "a", False, "a.b"),
("from . import c", "a.b", False, "a.c"),
("from . import d", "a.b.c", False, "a.b.d"),
("from .c import d", "a", False, "a.c.d"),
("from .c import d", "a.b", False, "a.c.d"),
("from .b import c", "a.b", True, "a.b.b.c"),
("from .. import e", "a.c.d.i", False, "a.c.e"),
("from ..d import e", "a.c.d.i", False, "a.c.d.e"),
("from ... import f", "a.c.d.i", False, "a.f"),
("from ...b import f", "a.c.d.i", False, "a.b.f"),
("from ...c.d import e", "a.c.d.i", False, "a.c.d.e"),
],
)
def test_relative_to_absolute_imports(code, path, is_package, expected):
"""Check if relative imports are correctly converted to absolute ones.
Parameters:
code: The parametrized module code.
path: The parametrized module path.
is_package: Whether the module is a package (or subpackage) (parametrized).
expected: The parametrized expected absolute path.
"""
node = compile(code, mode="exec", filename="<>", flags=PyCF_ONLY_AST).body[0]
module = module_vtree(path, leaf_package=is_package, return_leaf=True)
for name in node.names:
assert relative_to_absolute(node, name, module) == expected

0 comments on commit 464c39e

Please sign in to comment.