Repository: abdelrahman0w/ouro (language: python)

Issue:
create-imports-graph



Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index 15dc46a..f4f99f9 100644
--- a/Makefile
+++ b/Makefile
@@ -33,26 +33,29 @@ SITE_PACKAGES := $(shell python -c 'import site; print(site.getsitepackages()[0]
 help:
 	@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
 
-clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
+clean: clean-build clean-pyc clean-test clear-my-py ## remove all build, test, coverage and Python artifacts
 
 clean-build: ## remove build artifacts
-	@rm -fr build/
-	@rm -fr dist/
-	@rm -fr .eggs/
-	@find . -name '*.egg-info' -exec rm -fr {} +
+	@rm -rf build/
+	@rm -rf dist/
+	@rm -rf .eggs/
+	@find . -name '*.egg-info' -exec rm -rf {} +
 	@find . -name '*.egg' -exec rm -f {} +
 
 clean-pyc: ## remove Python file artifacts
 	@find . -name '*.pyc' -exec rm -f {} +
 	@find . -name '*.pyo' -exec rm -f {} +
 	@find . -name '*~' -exec rm -f {} +
-	@find . -name '__pycache__' -exec rm -fr {} +
+	@find . -name '__pycache__' -exec rm -rf {} +
 
 clean-test: ## remove test and coverage artifacts
-	@rm -fr .tox/
+	@rm -rf .tox/
 	@rm -f .coverage
-	@rm -fr htmlcov/
-	@rm -fr .pytest_cache
+	@rm -rf htmlcov/
+	@rm -rf .pytest_cache
+
+clear-my-py: ## remove mypy cache
+	@find . -name '.mypy_cache' -exec rm -rf {} +
 
 lint/flake8: ## check style with flake8
 	@flake8 ouro tests
diff --git a/ouro/imports_graph.py b/ouro/imports_graph.py
new file mode 100644
index 0000000..751589c
--- /dev/null
+++ b/ouro/imports_graph.py
@@ -0,0 +1,101 @@
+import ast
+from collections import defaultdict
+from pathlib import Path
+from typing import Dict
+from typing import List
+from typing import Union
+
+from ouro.reader import Reader
+
+
+class Node:
+    def __init__(
+        self,
+        name: str,
+    ) -> None:
+        self.name = name
+        self._parent: Union["Node", None] = None
+        self._imports: List["Node"] = []
+        self._imported_from: Dict[str, List[int]] = defaultdict(list)
+
+    def add(self, item: "Node") -> None:
+        self._imports.append(item)
+
+
+class ImportsGraph:
+    def __init__(self, reader_obj: "Reader") -> None:
+        self._prj = reader_obj.read
+        self._nodes: Dict[str, "Node"] = {}
+
+        self._parse()
+
+    def _get_node(self, file: str) -> "Node":
+        if file not in self._nodes:
+            self._nodes[file] = Node(file)
+
+        return self._nodes[file]
+
+    def _get_imports(
+        self, content: str
+    ) -> List[Union[ast.Import, ast.ImportFrom]]:
+        return [
+            node
+            for node in ast.walk(ast.parse(content))
+            if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom)
+        ]
+
+    def _get_class_defs(self, content: str) -> List[ast.ClassDef]:
+        return [
+            node
+            for node in ast.walk(ast.parse(content))
+            if isinstance(node, ast.ClassDef)
+        ]
+
+    def _get_func_defs(self, content: str) -> List[ast.FunctionDef]:
+        return [
+            node
+            for node in ast.walk(ast.parse(content))
+            if isinstance(node, ast.FunctionDef)
+        ]
+
+    def _get_module_path(
+        self, node: Union[ast.ImportFrom, ast.Import]
+    ) -> Union[str, None]:
+        if isinstance(node, ast.Import):
+            return (
+                f"{Path(*(node.names[0].name.split('.')))}.py"
+                if node.names
+                else None
+            )
+        elif isinstance(node, ast.ImportFrom):
+            return (
+                f"{Path(*(node.module.split('.')))}.py"
+                if node.module
+                else None
+            )
+
+        return None
+
+    def _handle_import(
+        self,
+        importing_node: "Node",
+        imported: Union[ast.ImportFrom, ast.Import],
+    ) -> None:
+        imported_module_path = self._get_module_path(imported)
+        if imported_module_path:
+            imported_node = self._get_node(imported_module_path)
+            imported_node._imported_from[importing_node.name].append(
+                imported.lineno
+            )
+            importing_node.add(imported_node)
+
+    def _parse(self) -> None:
+        for file, content in self._prj:
+            node = self._get_node(file)
+            imports = self._get_imports(content)
+
+            for imp in imports:
+                self._handle_import(node, imp)
+
+    def __iter__(self):
+        return iter(self._nodes.items())


Test patch:
diff --git a/tests/test_imports_graph.py b/tests/test_imports_graph.py
new file mode 100644
index 0000000..af79073
--- /dev/null
+++ b/tests/test_imports_graph.py
@@ -0,0 +1,13 @@
+from pathlib import Path
+
+from ouro.imports_graph import ImportsGraph
+from ouro.reader import Reader
+
+
+def test_imports_graph():
+    test_pkg_dir = Path(__file__).resolve().parent / "test_pkg"
+
+    graph = ImportsGraph(Reader(test_pkg_dir))
+    for path, node in graph:
+        if path.endswith("another_module.py"):
+            assert node._imports[0].name == "test_pkg/module.py"
diff --git a/tests/test_pkg/__init__.py b/tests/test_pkg/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_pkg/another_module.py b/tests/test_pkg/another_module.py
new file mode 100644
index 0000000..13809e1
--- /dev/null
+++ b/tests/test_pkg/another_module.py
@@ -0,0 +1,4 @@
+from test_pkg.module import VAR
+
+
+THIS_VAR = VAR
diff --git a/tests/test_reader.py b/tests/test_reader.py
index c77ff4d..4c2c078 100644
--- a/tests/test_reader.py
+++ b/tests/test_reader.py
@@ -11,6 +11,6 @@ def test_reader(mock_git_dir, mock_gitignore):
     with Reader(path=test_pkg_dir, ignore=["ignore*"]) as reader:
         files_lst, content_lst = zip(*reader)
 
-    assert len(files_lst) == 4
-    assert len(content_lst) == 4
+    assert len(files_lst) == 6
+    assert len(content_lst) == 6
     assert "INVALID" not in " ".join(content_lst)