Skip to content

Commit

Permalink
ruff + publishing
Browse files Browse the repository at this point in the history
  • Loading branch information
brunovcosta committed Dec 29, 2024
1 parent d966dda commit 9c58d2a
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 39 deletions.
14 changes: 8 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ jobs:

- uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.11

- name: Install dependencies
- name: Install Poetry
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install setuptools wheel twine
pip install poetry
- name: Install dependencies
run: poetry install

- name: "Builds"
run: python setup.py sdist bdist_wheel
- name: Build package
run: poetry build

- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
6 changes: 3 additions & 3 deletions circular_imports/main.py → circular_imports/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def cycles(self, path: str, exclude: str = None):
assert path_.exists(), f"Path {path_} does not exist"
assert path_.is_dir(), f"Path {path_} is not a directory"

all_python_files: Set[Path] = set(path_.glob('**/*.py'))
all_python_files: Set[Path] = set(path_.glob("**/*.py"))

graph: Set[Tuple[str, str]] = set()
for python_file in all_python_files:
Expand All @@ -30,7 +30,7 @@ def cycles(self, path: str, exclude: str = None):
dot_code += "}"

print(dot_code)


if __name__ == '__main__':

if __name__ == "__main__":
fire.Fire(CLI)
46 changes: 33 additions & 13 deletions circular_imports/find_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ast
from typing import Optional


def path2module(path: Path) -> str:
if path.suffix == ".py":
path = path.with_suffix("")
Expand Down Expand Up @@ -39,6 +40,7 @@ def iter_modules(module: str):
for i in range(1, len(module_blocks)):
yield ".".join(module_blocks[:-i])


class DependencyFinder(ast.NodeVisitor):
deps: Set[Path]
typechecking_imported_name: Optional[str]
Expand Down Expand Up @@ -66,13 +68,16 @@ def visit_Import(self, node: ast.Import):
module = alias.name

for module in iter_modules(module):

module_path = self.base_path / module2path(module, False)
if module_path.exists() and not any(e in module_path.name for e in self.excluded_patterns):
if module_path.exists() and not any(
e in module_path.name for e in self.excluded_patterns
):
self.deps.add(module_path.relative_to(self.base_path))

module_path = self.base_path / module2path(module, True)
if module_path.exists() and not any(e in module_path.name for e in self.excluded_patterns):
if module_path.exists() and not any(
e in module_path.name for e in self.excluded_patterns
):
self.deps.add(module_path.relative_to(self.base_path))

if node.names == ["typing"]:
Expand All @@ -97,15 +102,18 @@ def visit_ImportFrom(self, node: ast.ImportFrom):
search_path = search_path.parent

for module in iter_modules(module):

module_path = search_path / module2path(module, True)
if module_path.exists() and not any(e in module_path.name for e in self.excluded_patterns):
if module_path.exists() and not any(
e in module_path.name for e in self.excluded_patterns
):
self.deps.add(module_path.relative_to(self.base_path))

module_path = search_path / module2path(module, False)
if module_path.exists() and not any(e in module_path.name for e in self.excluded_patterns):
if module_path.exists() and not any(
e in module_path.name for e in self.excluded_patterns
):
self.deps.add(module_path.relative_to(self.base_path))

if node.module == "typing":
for alias in node.names:
if alias.name == "TYPE_CHECKING":
Expand All @@ -115,29 +123,41 @@ def visit_ImportFrom(self, node: ast.ImportFrom):

def visit_If(self, node: ast.If):
# if TYPE_CHECKING:
if self.typechecking_imported_name is not None and isinstance(node.test, ast.Name) and node.test.id == self.typechecking_imported_name:
if (
self.typechecking_imported_name is not None
and isinstance(node.test, ast.Name)
and node.test.id == self.typechecking_imported_name
):
print("found type checking block")
self.in_type_checking_block = True
self.generic_visit(node)
self.in_type_checking_block = False

# if typing.TYPE_CHECKING:
elif self.typing_imported_name is not None and isinstance(node.test, ast.Attribute) and node.test.attr == "TYPE_CHECKING" and isinstance(node.test.value, ast.Name) and node.test.value.id == self.typing_imported_name:
elif (
self.typing_imported_name is not None
and isinstance(node.test, ast.Attribute)
and node.test.attr == "TYPE_CHECKING"
and isinstance(node.test.value, ast.Name)
and node.test.value.id == self.typing_imported_name
):
print("found type checking block")
self.in_type_checking_block = True
self.generic_visit(node)
self.in_type_checking_block = False

else:
self.generic_visit(node)


def find_deps(base_path: Path, code_path: Path, excluded_patterns: List[str]) -> Set[Path]:
def find_deps(
base_path: Path, code_path: Path, excluded_patterns: List[str]
) -> Set[Path]:
tree = ast.parse(code_path.read_text("utf-8"))
visitor = DependencyFinder(base_path, excluded_patterns, code_path)
visitor.visit(tree)

print(code_path)
for dep in visitor.deps:
print(" ", dep)
return visitor.deps
return visitor.deps
7 changes: 4 additions & 3 deletions circular_imports/graph.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import List, Set, Tuple, Dict


def find_cycles(graph: Set[Tuple[str, str]]) -> Set[List[str]]:
graph_: Dict[str, Set[str]] = {}
for a, b in graph:
Expand All @@ -18,10 +19,10 @@ def dfs(node: str, visited: Set[str], path: List[str]) -> List[List[str]]:
if neighbor not in visited:
dfs(neighbor, visited, path)
elif neighbor in path:
cycles.append(normalize_cycle(path[path.index(neighbor):]))
cycles.append(normalize_cycle(path[path.index(neighbor) :]))
path.pop()
return cycles

for node in graph_:
if node not in visited:
dfs(node, visited, [])
Expand All @@ -31,4 +32,4 @@ def dfs(node: str, visited: Set[str], path: List[str]) -> List[List[str]]:

def normalize_cycle(cycle: List[str]) -> List[str]:
min_index = cycle.index(min(cycle))
return cycle[min_index:] + cycle[:min_index]
return cycle[min_index:] + cycle[:min_index]
17 changes: 6 additions & 11 deletions circular_imports/graph_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest import TestCase
from graph import find_cycles, normalize_cycle


class TestNormalizeCycle(TestCase):
def test_normalize_cycle_abc(self):
cycle_a = ["a", "b", "c"]
Expand All @@ -11,20 +12,14 @@ def test_normalize_cycle_abc(self):
self.assertEqual(normalize_cycle(cycle_b), ["a", "b", "c"])
self.assertEqual(normalize_cycle(cycle_c), ["a", "b", "c"])


class TestFindCycles(TestCase):
def test_find_cycles(self):
graph = {
("a", "b"),
("b", "a")
}
graph = {("a", "b"), ("b", "a")}
cycles = find_cycles(graph)
self.assertEqual(cycles, [["a", "b"]])

def test_find_long_cycles(self):
graph = {
("a", "b"),
("b", "c"),
("c", "a")
}
graph = {("a", "b"), ("b", "c"), ("c", "a")}
cycles = find_cycles(graph)
self.assertEqual(cycles, [["a", "b", "c"]])
self.assertEqual(cycles, [["a", "b", "c"]])
8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
[tool.poetry]
name = "circular-imports"
version = "0.1.0"
description = ""
authors = ["Bruno Vieira Costa <[email protected]>"]
description = "Detect circular imports in codebase."
authors = ["Bruno Costa <[email protected]>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"

[tool.poetry.scripts]
circular-imports = "circular_imports.cli:CLI"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
build-backend = "poetry.core.masonry.api"

0 comments on commit 9c58d2a

Please sign in to comment.