Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 19 additions & 26 deletions src/archunitpython/common/extraction/extract_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import ast
import os
from bisect import bisect_right

from archunitpython.common.extraction.graph import Edge, Graph, ImportKind
from archunitpython.common.fluentapi.checkable import CheckOptions
Expand Down Expand Up @@ -59,12 +60,8 @@ def extract_graph(

project_path = os.path.abspath(project_path)
excludes = list(exclude_patterns) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
ignore_type_checking_imports = bool(
options and options.ignore_type_checking_imports
)
cache_key = _build_cache_key(
project_path, excludes, ignore_type_checking_imports
)
ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)

if options and options.clear_cache:
_graph_cache.pop(cache_key, None)
Expand Down Expand Up @@ -105,6 +102,7 @@ def _extract_graph_uncached(

edges: list[Edge] = []
py_files_set = set(py_files)
normalized_py_file_set = {_normalize(f) for f in py_files_set}

for file_path in py_files:
# Add self-referencing edge (ensures the file appears as a node)
Expand All @@ -119,19 +117,14 @@ def _extract_graph_uncached(
# Extract and resolve imports
imports = _extract_imports(file_path)
for module_name, import_kind in imports:
if (
ignore_type_checking_imports
and import_kind == ImportKind.TYPE_IMPORT
):
if ignore_type_checking_imports and import_kind == ImportKind.TYPE_IMPORT:
continue
resolved, is_external = _resolve_import(
module_name, file_path, project_path, import_kind
)
if resolved and resolved != _normalize(file_path):
# Check if the resolved path is in our project
if not is_external and resolved not in {
_normalize(f) for f in py_files_set
}:
if not is_external and resolved not in normalized_py_file_set:
is_external = True

edges.append(
Expand All @@ -156,11 +149,7 @@ def _find_python_files(root: str, exclude: list[str]) -> list[str]:
py_files: list[str] = []
for dirpath, dirnames, filenames in os.walk(root):
# Filter out excluded directories in-place
dirnames[:] = [
d
for d in dirnames
if not _should_exclude(d, exclude)
]
dirnames[:] = [d for d in dirnames if not _should_exclude(d, exclude)]

for filename in filenames:
if filename.endswith(".py") and not _should_exclude(filename, exclude):
Expand Down Expand Up @@ -240,23 +229,27 @@ def _find_type_checking_ranges(tree: ast.Module) -> list[tuple[int, int]]:
if is_type_checking and node.body:
start = node.body[0].lineno
end = max(
getattr(n, "end_lineno", n.lineno)
for n in node.body
if hasattr(n, "lineno")
getattr(n, "end_lineno", n.lineno) for n in node.body if hasattr(n, "lineno")
)
ranges.append((start, end))

return ranges
return sorted(ranges, key=lambda ele: ele[0])


def _in_type_checking(
node: ast.AST, ranges: list[tuple[int, int]]
) -> bool:
def _in_type_checking(node: ast.AST, ranges: list[tuple[int, int]]) -> bool:
"""Check if a node is inside a TYPE_CHECKING block."""
if not hasattr(node, "lineno"):
return False
lineno = node.lineno
return any(start <= lineno <= end for start, end in ranges)

if not isinstance(lineno,int):
return False

matched_index = bisect_right(ranges, lineno, key=lambda ele: ele[0]) - 1
if matched_index < 0:
return False
start, end = ranges[matched_index]
return start <= lineno <= end


def _resolve_import(
Expand Down
4 changes: 1 addition & 3 deletions src/archunitpython/common/pattern_matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ def matches_pattern(file_path: str, filter_: Filter) -> bool:
return bool(filter_.regexp.search(target_string))


def matches_pattern_classname(
class_name: str, file_path: str, filter_: Filter
) -> bool:
def matches_pattern_classname(class_name: str, file_path: str, filter_: Filter) -> bool:
"""Check if a class/file matches a filter, supporting classname target."""
target = filter_.options.target

Expand Down
13 changes: 4 additions & 9 deletions src/archunitpython/common/projection/cycles/johnsons_apsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,9 @@ def _explore_neighbours(self, current_node: NumberNode) -> None:
if self._is_part_of_current_start_cycle(current_node):
self._unblock(current_node)
else:
for neighbour in CycleUtils.get_outgoing_neighbours(
current_node, self._graph
):
for neighbour in CycleUtils.get_outgoing_neighbours(current_node, self._graph):
if self._is_blocked(neighbour):
self._blocked_map.append(
_BlockedBy(blocked=current_node, by=neighbour)
)
self._blocked_map.append(_BlockedBy(blocked=current_node, by=neighbour))

def _unblock(self, node: NumberNode) -> None:
self._blocked = [n for n in self._blocked if n is not node]
Expand All @@ -74,9 +70,8 @@ def _is_part_of_current_start_cycle(self, current_node: NumberNode) -> bool:
if self._start is None:
return False
for cycle in self._cycles:
if (
cycle[0].from_node == self._start.node
and any(e.from_node == current_node.node for e in cycle)
if cycle[0].from_node == self._start.node and any(
e.from_node == current_node.node for e in cycle
):
return True
return False
Expand Down
8 changes: 2 additions & 6 deletions src/archunitpython/common/projection/cycles/tarjan_scc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ def __init__(self, node_id: int) -> None:
class TarjanSCC:
"""Tarjan's algorithm for finding strongly connected components."""

def find_strongly_connected_components(
self, edges: list[NumberEdge]
) -> list[list[NumberEdge]]:
def find_strongly_connected_components(self, edges: list[NumberEdge]) -> list[list[NumberEdge]]:
"""Find all strongly connected components in the graph.

Returns a list of edge lists, where each inner list contains
Expand Down Expand Up @@ -78,9 +76,7 @@ def _visit(self, vertex: _Vertex) -> None:
if scc_vertices:
scc_ids = {v.id for v in scc_vertices}
scc_edges = [
e
for e in self._edges
if e.from_node in scc_ids and e.to_node in scc_ids
e for e in self._edges if e.from_node in scc_ids and e.to_node in scc_ids
]
if scc_edges:
self._sccs.append(scc_edges)
3 changes: 1 addition & 2 deletions src/archunitpython/common/projection/project_cycles.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ def _from_domain(self, cycles: list[list[NumberEdge]]) -> ProjectedCycles:
(
se
for se in self._source_edges
if se.source_label == source_label
and se.target_label == target_label
if se.source_label == source_label and se.target_label == target_label
),
None,
)
Expand Down
4 changes: 1 addition & 3 deletions src/archunitpython/common/util/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ def _ensure_file_handler(self, options: LoggingOptions) -> None:

mode = "a" if options.append_to_log_file else "w"
self._file_handler = logging.FileHandler(str(log_path), mode=mode)
self._file_handler.setFormatter(
logging.Formatter("[%(levelname)s] %(message)s")
)
self._file_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
self._logger.addHandler(self._file_handler)

def _log(self, level: str, options: LoggingOptions | None, message: str) -> None:
Expand Down
12 changes: 3 additions & 9 deletions src/archunitpython/files/assertion/custom_file_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ def gather_custom_file_violations(

for node in nodes:
# Check if node matches all pre-filters
if pre_filters and not all(
matches_pattern(node.label, f) for f in pre_filters
):
if pre_filters and not all(matches_pattern(node.label, f) for f in pre_filters):
continue

file_info = _build_file_info(node.label)
Expand All @@ -94,14 +92,10 @@ def gather_custom_file_violations(
if is_negated:
# shouldNot: violation if condition IS True
if result:
violations.append(
CustomFileViolation(message=message, file_info=file_info)
)
violations.append(CustomFileViolation(message=message, file_info=file_info))
else:
# should: violation if condition is NOT True
if not result:
violations.append(
CustomFileViolation(message=message, file_info=file_info)
)
violations.append(CustomFileViolation(message=message, file_info=file_info))

return violations
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ def gather_depend_on_external_module_violations(

for edge in edges:
source_matches = all(
matches_pattern(edge.source_label, filter_)
for filter_ in subject_filters
matches_pattern(edge.source_label, filter_) for filter_ in subject_filters
)
if not source_matches:
continue
Expand All @@ -49,16 +48,12 @@ def gather_depend_on_external_module_violations(
if is_negated:
if target_matches:
violations.append(
ViolatingExternalModuleDependency(
dependency=edge, is_negated=True
)
ViolatingExternalModuleDependency(dependency=edge, is_negated=True)
)
else:
if not target_matches:
violations.append(
ViolatingExternalModuleDependency(
dependency=edge, is_negated=False
)
ViolatingExternalModuleDependency(dependency=edge, is_negated=False)
)

return violations
16 changes: 4 additions & 12 deletions src/archunitpython/files/assertion/depend_on_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,19 @@ def gather_depend_on_file_violations(
violations: list[Violation] = []

for edge in edges:
source_matches = all(
matches_pattern(edge.source_label, f) for f in subject_filters
)
source_matches = all(matches_pattern(edge.source_label, f) for f in subject_filters)
if not source_matches:
continue

target_matches = all(
matches_pattern(edge.target_label, f) for f in object_filters
)
target_matches = all(matches_pattern(edge.target_label, f) for f in object_filters)

if is_negated:
# shouldNot: violation if dependency EXISTS
if target_matches:
violations.append(
ViolatingFileDependency(dependency=edge, is_negated=True)
)
violations.append(ViolatingFileDependency(dependency=edge, is_negated=True))
else:
# should: violation if dependency does NOT match
if not target_matches:
violations.append(
ViolatingFileDependency(dependency=edge, is_negated=False)
)
violations.append(ViolatingFileDependency(dependency=edge, is_negated=False))

return violations
36 changes: 9 additions & 27 deletions src/archunitpython/files/fluentapi/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,11 @@ def in_path(self, path: Pattern) -> "FilesShouldCondition":

def should(self) -> "PositiveMatchPatternFileConditionBuilder":
"""Begin positive assertion (files SHOULD ...)."""
return PositiveMatchPatternFileConditionBuilder(
self._project_path, list(self._filters)
)
return PositiveMatchPatternFileConditionBuilder(self._project_path, list(self._filters))

def should_not(self) -> "NegatedMatchPatternFileConditionBuilder":
"""Begin negative assertion (files SHOULD NOT ...)."""
return NegatedMatchPatternFileConditionBuilder(
self._project_path, list(self._filters)
)
return NegatedMatchPatternFileConditionBuilder(self._project_path, list(self._filters))


class FilesShouldCondition:
Expand All @@ -111,15 +107,11 @@ def in_path(self, path: Pattern) -> "FilesShouldCondition":

def should(self) -> "PositiveMatchPatternFileConditionBuilder":
"""Begin positive assertion (files SHOULD ...)."""
return PositiveMatchPatternFileConditionBuilder(
self._project_path, list(self._filters)
)
return PositiveMatchPatternFileConditionBuilder(self._project_path, list(self._filters))

def should_not(self) -> "NegatedMatchPatternFileConditionBuilder":
"""Begin negative assertion (files SHOULD NOT ...)."""
return NegatedMatchPatternFileConditionBuilder(
self._project_path, list(self._filters)
)
return NegatedMatchPatternFileConditionBuilder(self._project_path, list(self._filters))


class PositiveMatchPatternFileConditionBuilder:
Expand All @@ -135,9 +127,7 @@ def have_no_cycles(self) -> "CycleFreeFileCondition":

def depend_on_files(self) -> "DependOnFileConditionBuilder":
"""Begin dependency assertion - files SHOULD depend on ..."""
return DependOnFileConditionBuilder(
self._project_path, self._filters, is_negated=False
)
return DependOnFileConditionBuilder(self._project_path, self._filters, is_negated=False)

def depend_on_external_modules(
self,
Expand Down Expand Up @@ -192,9 +182,7 @@ def __init__(self, project_path: str | None, filters: list[Filter]) -> None:

def depend_on_files(self) -> "DependOnFileConditionBuilder":
"""Begin dependency assertion - files SHOULD NOT depend on ..."""
return DependOnFileConditionBuilder(
self._project_path, self._filters, is_negated=True
)
return DependOnFileConditionBuilder(self._project_path, self._filters, is_negated=True)

def depend_on_external_modules(
self,
Expand Down Expand Up @@ -243,9 +231,7 @@ def adhere_to(
class DependOnFileConditionBuilder:
"""Configure dependency target patterns."""

def __init__(
self, project_path: str | None, filters: list[Filter], is_negated: bool
) -> None:
def __init__(self, project_path: str | None, filters: list[Filter], is_negated: bool) -> None:
self._project_path = project_path
self._filters = filters
self._is_negated = is_negated
Expand Down Expand Up @@ -285,9 +271,7 @@ def in_path(self, path: Pattern) -> "DependOnFileCondition":
class DependOnExternalModuleConditionBuilder:
"""Configure external module dependency target patterns."""

def __init__(
self, project_path: str | None, filters: list[Filter], is_negated: bool
) -> None:
def __init__(self, project_path: str | None, filters: list[Filter], is_negated: bool) -> None:
self._project_path = project_path
self._filters = filters
self._is_negated = is_negated
Expand Down Expand Up @@ -447,9 +431,7 @@ def check(self, options: CheckOptions | None = None) -> list[Violation]:
if empty is not None:
return empty

return gather_regex_matching_violations(
nodes, self._check_filters, self._is_negated
)
return gather_regex_matching_violations(nodes, self._check_filters, self._is_negated)


class CustomFileCheckableCondition:
Expand Down
4 changes: 1 addition & 3 deletions src/archunitpython/metrics/assertion/metric_thresholds.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ class FileCountViolation(Violation):
comparison: MetricComparison


def check_threshold(
value: float, threshold: float, comparison: MetricComparison
) -> bool:
def check_threshold(value: float, threshold: float, comparison: MetricComparison) -> bool:
"""Check if a value violates a threshold.

Returns True if the value is a VIOLATION.
Expand Down
4 changes: 1 addition & 3 deletions src/archunitpython/metrics/calculation/distance.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ def calculate_distance_metrics_for_project(
average_instability=sum(m.instability for m in metrics) / len(metrics),
average_distance=sum(m.distance for m in metrics) / len(metrics),
files_in_zone_of_pain=sum(1 for m in metrics if m.in_zone_of_pain),
files_in_zone_of_uselessness=sum(
1 for m in metrics if m.in_zone_of_uselessness
),
files_in_zone_of_uselessness=sum(1 for m in metrics if m.in_zone_of_uselessness),
total_files=len(files),
)
Loading