Skip to content
Merged
30 changes: 8 additions & 22 deletions src/archunitpython/common/extraction/extract_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,8 @@ def extract_graph(
excludes = (
list(set(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 @@ -137,6 +133,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 @@ -148,7 +145,6 @@ def _extract_graph_uncached(
)
)

# Extract and resolve imports
imports = _extract_located_imports(file_path)
for located_import in imports:
module_name = located_import.module_name
Expand All @@ -163,9 +159,7 @@ def _extract_graph_uncached(
)
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 @@ -190,11 +184,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 @@ -349,18 +339,14 @@ 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
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),
)
12 changes: 3 additions & 9 deletions src/archunitpython/metrics/extraction/extract_class_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,7 @@ def _extract_class(node: ast.ClassDef, file_path: str) -> ClassInfo:
for item in ast.walk(node):
if isinstance(item, ast.Assign):
for target in item.targets:
if isinstance(target, ast.Attribute) and isinstance(
target.value, ast.Name
):
if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name):
if target.value.id == "self":
field_name = target.attr
if field_name not in fields:
Expand All @@ -142,9 +140,7 @@ def _extract_class(node: ast.ClassDef, file_path: str) -> ClassInfo:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
method_name = item.name
accessed = _find_field_accesses(item, set(fields.keys()))
methods.append(
MethodInfo(name=method_name, accessed_fields=accessed)
)
methods.append(MethodInfo(name=method_name, accessed_fields=accessed))
# Update field access tracking
for field_name in accessed:
if field_name in fields:
Expand All @@ -159,9 +155,7 @@ def _extract_class(node: ast.ClassDef, file_path: str) -> ClassInfo:
)


def _extract_enhanced_class(
node: ast.ClassDef, file_path: str
) -> EnhancedClassInfo:
def _extract_enhanced_class(node: ast.ClassDef, file_path: str) -> EnhancedClassInfo:
"""Extract EnhancedClassInfo from a ClassDef AST node."""
base = _extract_class(node, file_path)

Expand Down
Loading