From 68374abbe2585d3e39ab458a92f597b6be891164 Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Thu, 28 May 2026 10:56:11 +0530 Subject: [PATCH 1/7] perf(extract_graph): replace linear search in `_in_type_checking()` with binary search Now `_find_type_checking_ranges()` will return a range array sorted by start line, and the lineno for any import node will be matched using binary search with the help of the `bisect` library for faster lookup. Currently, this assumes that there are no overlapping ranges in the ranges array. Overlapping ranges can occur only when there are nested `if TYPE_CHECKING:` blocks. --- .../common/extraction/extract_graph.py | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/archunitpython/common/extraction/extract_graph.py b/src/archunitpython/common/extraction/extract_graph.py index 5d3d282..9b074e7 100644 --- a/src/archunitpython/common/extraction/extract_graph.py +++ b/src/archunitpython/common/extraction/extract_graph.py @@ -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 @@ -119,19 +120,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 {_normalize(f) for f in py_files_set}: is_external = True edges.append( @@ -156,11 +152,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): @@ -240,23 +232,24 @@ 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) + + 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( From 794641d1624335299601b16dbee834ad0d8aba5e Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Thu, 28 May 2026 10:58:08 +0530 Subject: [PATCH 2/7] style: format code --- src/archunitpython/common/pattern_matching.py | 4 +- .../common/projection/cycles/johnsons_apsp.py | 13 ++--- .../common/projection/cycles/tarjan_scc.py | 8 +-- .../common/projection/project_cycles.py | 3 +- src/archunitpython/common/util/logger.py | 4 +- .../files/assertion/custom_file_logic.py | 12 ++--- .../assertion/depend_on_external_modules.py | 11 ++-- .../files/assertion/depend_on_files.py | 16 ++---- src/archunitpython/files/fluentapi/files.py | 36 ++++--------- .../metrics/assertion/metric_thresholds.py | 4 +- .../metrics/calculation/distance.py | 4 +- .../metrics/extraction/extract_class_info.py | 12 ++--- .../metrics/fluentapi/export_utils.py | 8 +-- .../metrics/fluentapi/metrics.py | 50 +++++-------------- src/archunitpython/slices/fluentapi/slices.py | 16 ++---- .../slices/uml/generate_rules.py | 8 +-- .../testing/common/violation_factory.py | 4 +- 17 files changed, 54 insertions(+), 159 deletions(-) diff --git a/src/archunitpython/common/pattern_matching.py b/src/archunitpython/common/pattern_matching.py index 5ac3304..1956602 100644 --- a/src/archunitpython/common/pattern_matching.py +++ b/src/archunitpython/common/pattern_matching.py @@ -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 diff --git a/src/archunitpython/common/projection/cycles/johnsons_apsp.py b/src/archunitpython/common/projection/cycles/johnsons_apsp.py index 1a86428..91e0601 100644 --- a/src/archunitpython/common/projection/cycles/johnsons_apsp.py +++ b/src/archunitpython/common/projection/cycles/johnsons_apsp.py @@ -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] @@ -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 diff --git a/src/archunitpython/common/projection/cycles/tarjan_scc.py b/src/archunitpython/common/projection/cycles/tarjan_scc.py index 09eae5f..eabea08 100644 --- a/src/archunitpython/common/projection/cycles/tarjan_scc.py +++ b/src/archunitpython/common/projection/cycles/tarjan_scc.py @@ -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 @@ -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) diff --git a/src/archunitpython/common/projection/project_cycles.py b/src/archunitpython/common/projection/project_cycles.py index fd3ded4..9413043 100644 --- a/src/archunitpython/common/projection/project_cycles.py +++ b/src/archunitpython/common/projection/project_cycles.py @@ -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, ) diff --git a/src/archunitpython/common/util/logger.py b/src/archunitpython/common/util/logger.py index 1a2e56e..e42ba81 100644 --- a/src/archunitpython/common/util/logger.py +++ b/src/archunitpython/common/util/logger.py @@ -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: diff --git a/src/archunitpython/files/assertion/custom_file_logic.py b/src/archunitpython/files/assertion/custom_file_logic.py index a3e3e5f..573a872 100644 --- a/src/archunitpython/files/assertion/custom_file_logic.py +++ b/src/archunitpython/files/assertion/custom_file_logic.py @@ -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) @@ -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 diff --git a/src/archunitpython/files/assertion/depend_on_external_modules.py b/src/archunitpython/files/assertion/depend_on_external_modules.py index 181cb9a..2cc6e37 100644 --- a/src/archunitpython/files/assertion/depend_on_external_modules.py +++ b/src/archunitpython/files/assertion/depend_on_external_modules.py @@ -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 @@ -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 diff --git a/src/archunitpython/files/assertion/depend_on_files.py b/src/archunitpython/files/assertion/depend_on_files.py index b845882..99d2946 100644 --- a/src/archunitpython/files/assertion/depend_on_files.py +++ b/src/archunitpython/files/assertion/depend_on_files.py @@ -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 diff --git a/src/archunitpython/files/fluentapi/files.py b/src/archunitpython/files/fluentapi/files.py index c680681..9f31036 100644 --- a/src/archunitpython/files/fluentapi/files.py +++ b/src/archunitpython/files/fluentapi/files.py @@ -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: @@ -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: @@ -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, @@ -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, @@ -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 @@ -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 @@ -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: diff --git a/src/archunitpython/metrics/assertion/metric_thresholds.py b/src/archunitpython/metrics/assertion/metric_thresholds.py index 52d92d6..1bd70f7 100644 --- a/src/archunitpython/metrics/assertion/metric_thresholds.py +++ b/src/archunitpython/metrics/assertion/metric_thresholds.py @@ -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. diff --git a/src/archunitpython/metrics/calculation/distance.py b/src/archunitpython/metrics/calculation/distance.py index 4b74f76..c95e8dd 100644 --- a/src/archunitpython/metrics/calculation/distance.py +++ b/src/archunitpython/metrics/calculation/distance.py @@ -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), ) diff --git a/src/archunitpython/metrics/extraction/extract_class_info.py b/src/archunitpython/metrics/extraction/extract_class_info.py index 24ed42e..e3d08e4 100644 --- a/src/archunitpython/metrics/extraction/extract_class_info.py +++ b/src/archunitpython/metrics/extraction/extract_class_info.py @@ -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: @@ -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: @@ -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) diff --git a/src/archunitpython/metrics/fluentapi/export_utils.py b/src/archunitpython/metrics/fluentapi/export_utils.py index 5c4496f..5e88713 100644 --- a/src/archunitpython/metrics/fluentapi/export_utils.py +++ b/src/archunitpython/metrics/fluentapi/export_utils.py @@ -35,11 +35,7 @@ def export_as_html( HTML content as a string. Also writes to file if output_path specified. """ opts = options or ExportOptions() - timestamp = ( - datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if opts.include_timestamp - else "" - ) + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if opts.include_timestamp else "" css = opts.custom_css or _DEFAULT_CSS @@ -55,7 +51,7 @@ def export_as_html(

{opts.title}

- {f'

Generated: {timestamp}

' if timestamp else ''} + {f'

Generated: {timestamp}

' if timestamp else ""} diff --git a/src/archunitpython/metrics/fluentapi/metrics.py b/src/archunitpython/metrics/fluentapi/metrics.py index 27107fd..2824cb4 100644 --- a/src/archunitpython/metrics/fluentapi/metrics.py +++ b/src/archunitpython/metrics/fluentapi/metrics.py @@ -97,9 +97,7 @@ def custom_metric( ) -def _get_filtered_classes( - project_path: str | None, filters: list[Filter] -) -> list[ClassInfo]: +def _get_filtered_classes(project_path: str | None, filters: list[Filter]) -> list[ClassInfo]: classes = extract_class_info(project_path) if not filters: return classes @@ -119,39 +117,25 @@ def __init__(self, project_path: str | None, filters: list[Filter]) -> None: self._filters = filters def method_count(self) -> "ClassMetricThresholdBuilder": - return ClassMetricThresholdBuilder( - self._project_path, self._filters, MethodCountMetric() - ) + return ClassMetricThresholdBuilder(self._project_path, self._filters, MethodCountMetric()) def field_count(self) -> "ClassMetricThresholdBuilder": - return ClassMetricThresholdBuilder( - self._project_path, self._filters, FieldCountMetric() - ) + return ClassMetricThresholdBuilder(self._project_path, self._filters, FieldCountMetric()) def lines_of_code(self) -> "FileMetricThresholdBuilder": - return FileMetricThresholdBuilder( - self._project_path, self._filters, LinesOfCodeMetric() - ) + return FileMetricThresholdBuilder(self._project_path, self._filters, LinesOfCodeMetric()) def statements(self) -> "FileMetricThresholdBuilder": - return FileMetricThresholdBuilder( - self._project_path, self._filters, StatementCountMetric() - ) + return FileMetricThresholdBuilder(self._project_path, self._filters, StatementCountMetric()) def imports(self) -> "FileMetricThresholdBuilder": - return FileMetricThresholdBuilder( - self._project_path, self._filters, ImportCountMetric() - ) + return FileMetricThresholdBuilder(self._project_path, self._filters, ImportCountMetric()) def classes(self) -> "FileMetricThresholdBuilder": - return FileMetricThresholdBuilder( - self._project_path, self._filters, ClassCountMetric() - ) + return FileMetricThresholdBuilder(self._project_path, self._filters, ClassCountMetric()) def functions(self) -> "FileMetricThresholdBuilder": - return FileMetricThresholdBuilder( - self._project_path, self._filters, FunctionCountMetric() - ) + return FileMetricThresholdBuilder(self._project_path, self._filters, FunctionCountMetric()) class ClassMetricThresholdBuilder: @@ -340,19 +324,13 @@ def __init__(self, project_path: str | None, filters: list[Filter]) -> None: self._filters = filters def abstractness(self) -> "DistanceThresholdBuilder": - return DistanceThresholdBuilder( - self._project_path, self._filters, "abstractness" - ) + return DistanceThresholdBuilder(self._project_path, self._filters, "abstractness") def instability(self) -> "DistanceThresholdBuilder": - return DistanceThresholdBuilder( - self._project_path, self._filters, "instability" - ) + return DistanceThresholdBuilder(self._project_path, self._filters, "instability") def distance_from_main_sequence(self) -> "DistanceThresholdBuilder": - return DistanceThresholdBuilder( - self._project_path, self._filters, "distance" - ) + return DistanceThresholdBuilder(self._project_path, self._filters, "distance") def not_in_zone_of_pain(self) -> "ZoneCondition": return ZoneCondition(self._project_path, self._filters, "pain") @@ -440,11 +418,7 @@ def check(self, options: CheckOptions | None = None) -> list[Violation]: for file_result in files: dm = calculate_file_distance_metrics(file_result, files) - in_zone = ( - dm.in_zone_of_pain - if self._zone_type == "pain" - else dm.in_zone_of_uselessness - ) + in_zone = dm.in_zone_of_pain if self._zone_type == "pain" else dm.in_zone_of_uselessness if in_zone: violations.append( diff --git a/src/archunitpython/slices/fluentapi/slices.py b/src/archunitpython/slices/fluentapi/slices.py index cf314fb..31d2d28 100644 --- a/src/archunitpython/slices/fluentapi/slices.py +++ b/src/archunitpython/slices/fluentapi/slices.py @@ -61,15 +61,11 @@ def defined_by_regex(self, regex: re.Pattern[str]) -> "SliceConditionBuilder": def should(self) -> "PositiveConditionBuilder": """Begin positive assertion (slices SHOULD ...).""" - return PositiveConditionBuilder( - self._project_path, self._pattern, self._regex - ) + return PositiveConditionBuilder(self._project_path, self._pattern, self._regex) def should_not(self) -> "NegativeConditionBuilder": """Begin negative assertion (slices SHOULD NOT ...).""" - return NegativeConditionBuilder( - self._project_path, self._pattern, self._regex - ) + return NegativeConditionBuilder(self._project_path, self._pattern, self._regex) class PositiveConditionBuilder: @@ -133,9 +129,7 @@ def __init__( self._regex = regex self._forbidden_deps: list[tuple[str, str]] = [] - def contain_dependency( - self, source: str, target: str - ) -> "NegativeSliceCondition": + def contain_dependency(self, source: str, target: str) -> "NegativeSliceCondition": """Assert that a specific dependency should NOT exist.""" return NegativeSliceCondition( self._project_path, @@ -170,9 +164,7 @@ def check(self, options: CheckOptions | None = None) -> list[Violation]: mapper = self._get_mapper() edges = project_edges(graph, mapper) - return gather_positive_violations( - edges, rules, contained_nodes, self._coherence_options - ) + return gather_positive_violations(edges, rules, contained_nodes, self._coherence_options) def _get_mapper(self) -> MapFunction: if self._pattern: diff --git a/src/archunitpython/slices/uml/generate_rules.py b/src/archunitpython/slices/uml/generate_rules.py index 851c114..de65704 100644 --- a/src/archunitpython/slices/uml/generate_rules.py +++ b/src/archunitpython/slices/uml/generate_rules.py @@ -43,9 +43,7 @@ def generate_rule(puml_content: str) -> tuple[list[Rule], list[str]]: continue # Match component declarations: component [Name] or component [Name] #Color - comp_match = re.match( - r"component\s+\[([^\]]+)\]", stripped - ) + comp_match = re.match(r"component\s+\[([^\]]+)\]", stripped) if comp_match: name = comp_match.group(1).strip() if name not in contained_nodes: @@ -53,9 +51,7 @@ def generate_rule(puml_content: str) -> tuple[list[Rule], list[str]]: continue # Match relationships: [Source] --> [Target] or [Source] -> [Target] - rel_match = re.match( - r"\[([^\]]+)\]\s*-+>\s*\[([^\]]+)\]", stripped - ) + rel_match = re.match(r"\[([^\]]+)\]\s*-+>\s*\[([^\]]+)\]", stripped) if rel_match: source = rel_match.group(1).strip() target = rel_match.group(2).strip() diff --git a/src/archunitpython/testing/common/violation_factory.py b/src/archunitpython/testing/common/violation_factory.py index a3ae6d6..326984f 100644 --- a/src/archunitpython/testing/common/violation_factory.py +++ b/src/archunitpython/testing/common/violation_factory.py @@ -65,9 +65,7 @@ def from_violation(violation: Violation) -> TestViolation: ) if isinstance(violation, ViolatingCycle): - cycle_str = " -> ".join( - e.source_label for e in violation.cycle - ) + cycle_str = " -> ".join(e.source_label for e in violation.cycle) return TestViolation( message="Circular dependency detected", details=f"Cycle: {cycle_str}", From 3e993543f5947940cf2aaf4a849ecac0e3c15278 Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Thu, 28 May 2026 11:17:11 +0530 Subject: [PATCH 3/7] fix(extract_graph): validate AST line number before range lookup Use `isinstance()` to ensure `node.lineno` is an integer before checking TYPE_CHECKING ranges. This avoids returning `Any` from `_in_type_checking()` and fixes the mypy `no-any-return` error. --- src/archunitpython/common/extraction/extract_graph.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/archunitpython/common/extraction/extract_graph.py b/src/archunitpython/common/extraction/extract_graph.py index 9b074e7..a2adca5 100644 --- a/src/archunitpython/common/extraction/extract_graph.py +++ b/src/archunitpython/common/extraction/extract_graph.py @@ -245,6 +245,9 @@ def _in_type_checking(node: ast.AST, ranges: list[tuple[int, int]]) -> bool: return False lineno = node.lineno + 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 From d83b632711d2db6a50fde0f7db3fb082297464c4 Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Fri, 29 May 2026 19:43:16 +0530 Subject: [PATCH 4/7] perf(extract_graph): optimize file path normalization Replaced the inline normalization of file paths with a precomputed set to improve performance and reduce redundant computations during graph extraction. --- src/archunitpython/common/extraction/extract_graph.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/archunitpython/common/extraction/extract_graph.py b/src/archunitpython/common/extraction/extract_graph.py index a2adca5..899d389 100644 --- a/src/archunitpython/common/extraction/extract_graph.py +++ b/src/archunitpython/common/extraction/extract_graph.py @@ -106,6 +106,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) @@ -127,7 +128,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( From 9a8fbed3dabc8b88881d8ad1039f6d1f0f72a0ee Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Mon, 15 Jun 2026 18:13:05 +0530 Subject: [PATCH 5/7] revert: Reverted the binary search implementation in `_in_type_checking()` function. --- src/archunitpython/common/extraction/extract_graph.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/archunitpython/common/extraction/extract_graph.py b/src/archunitpython/common/extraction/extract_graph.py index 899d389..52c7797 100644 --- a/src/archunitpython/common/extraction/extract_graph.py +++ b/src/archunitpython/common/extraction/extract_graph.py @@ -4,7 +4,6 @@ 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 @@ -245,15 +244,7 @@ def _in_type_checking(node: ast.AST, ranges: list[tuple[int, int]]) -> bool: if not hasattr(node, "lineno"): return False lineno = node.lineno - - 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 + return any(start <= lineno <= end for start, end in ranges) def _resolve_import( From 8b0482a618f453bb47eac5b2afa7578e1bc0f61d Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Mon, 15 Jun 2026 18:23:04 +0530 Subject: [PATCH 6/7] style: format code --- tests/common/test_extract_graph.py | 24 ++----- tests/files/test_files_fluentapi.py | 87 +++++------------------ tests/fixtures/metrics_project/service.py | 3 +- tests/integration/test_e2e.py | 11 ++- tests/metrics/test_export.py | 8 +-- tests/metrics/test_metrics.py | 5 ++ tests/metrics/test_metrics_fluentapi.py | 51 +++---------- tests/slices/test_slices.py | 12 +++- 8 files changed, 55 insertions(+), 146 deletions(-) diff --git a/tests/common/test_extract_graph.py b/tests/common/test_extract_graph.py index 5339658..38b3f80 100644 --- a/tests/common/test_extract_graph.py +++ b/tests/common/test_extract_graph.py @@ -83,11 +83,7 @@ def test_self_referencing_edges(self): def test_internal_edges_detected(self): graph = extract_graph(SAMPLE_PROJECT) - internal_non_self = [ - e - for e in graph - if not e.external and e.source != e.target - ] + internal_non_self = [e for e in graph if not e.external and e.source != e.target] assert len(internal_non_self) > 0 def test_external_edges_detected(self): @@ -106,11 +102,7 @@ def test_relative_import_resolved(self): ) # service_b imports from .service (relative) rel_edges = [ - e - for e in graph - if e.source == service_b - and e.target == service - and not e.external + e for e in graph if e.source == service_b and e.target == service and not e.external ] assert len(rel_edges) == 1 @@ -121,9 +113,7 @@ def test_caching(self): def test_cache_clear(self): graph1 = extract_graph(SAMPLE_PROJECT) - graph2 = extract_graph( - SAMPLE_PROJECT, options=CheckOptions(clear_cache=True) - ) + graph2 = extract_graph(SAMPLE_PROJECT, options=CheckOptions(clear_cache=True)) assert graph1 is not graph2 # Different objects after cache clear def test_edge_has_import_kinds(self): @@ -185,9 +175,7 @@ def test_type_checking_imports_included_by_default(self): ).replace("\\", "/") edges = [ - edge - for edge in graph - if edge.source == service_path and edge.target == models_path + edge for edge in graph if edge.source == service_path and edge.target == models_path ] assert len(edges) == 1 assert ImportKind.TYPE_IMPORT in edges[0].import_kinds @@ -207,9 +195,7 @@ def test_type_checking_imports_can_be_ignored(self): ).replace("\\", "/") edges = [ - edge - for edge in graph - if edge.source == service_path and edge.target == models_path + edge for edge in graph if edge.source == service_path and edge.target == models_path ] assert edges == [] diff --git a/tests/files/test_files_fluentapi.py b/tests/files/test_files_fluentapi.py index c17993d..77cac5a 100644 --- a/tests/files/test_files_fluentapi.py +++ b/tests/files/test_files_fluentapi.py @@ -33,12 +33,7 @@ def test_no_cycles_in_sample_project(self): assert len(cycle_violations) == 0 def test_cycle_detection_with_filter(self): - rule = ( - project_files(FIXTURES_DIR) - .in_folder("**/services*") - .should() - .have_no_cycles() - ) + rule = project_files(FIXTURES_DIR).in_folder("**/services*").should().have_no_cycles() violations = rule.check() cycle_violations = [v for v in violations if isinstance(v, ViolatingCycle)] assert len(cycle_violations) == 0 @@ -58,9 +53,7 @@ def test_should_not_depend(self): .in_folder("**/utils*") ) violations = rule.check() - dep_violations = [ - v for v in violations if isinstance(v, ViolatingFileDependency) - ] + dep_violations = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_violations) == 0 def test_should_not_depend_violation(self): @@ -73,9 +66,7 @@ def test_should_not_depend_violation(self): .in_folder("**/services*") ) violations = rule.check() - dep_violations = [ - v for v in violations if isinstance(v, ViolatingFileDependency) - ] + dep_violations = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_violations) > 0 @@ -92,11 +83,7 @@ def test_should_not_depend_on_specific_external_module(self): .matching("json") ) violations = rule.check() - dep_violations = [ - v - for v in violations - if isinstance(v, ViolatingExternalModuleDependency) - ] + dep_violations = [v for v in violations if isinstance(v, ViolatingExternalModuleDependency)] assert len(dep_violations) == 1 assert dep_violations[0].dependency.target_label == "json" @@ -109,11 +96,7 @@ def test_should_not_depend_on_unmatched_external_module(self): .matching("requests") ) violations = rule.check() - dep_violations = [ - v - for v in violations - if isinstance(v, ViolatingExternalModuleDependency) - ] + dep_violations = [v for v in violations if isinstance(v, ViolatingExternalModuleDependency)] assert len(dep_violations) == 0 def test_matching_multiple_external_modules_uses_or_semantics(self): @@ -126,11 +109,7 @@ def test_matching_multiple_external_modules_uses_or_semantics(self): .matching("typing") ) violations = rule.check() - dep_violations = [ - v - for v in violations - if isinstance(v, ViolatingExternalModuleDependency) - ] + dep_violations = [v for v in violations if isinstance(v, ViolatingExternalModuleDependency)] assert {v.dependency.target_label for v in dep_violations} == { "json", "typing", @@ -145,11 +124,7 @@ def test_positive_external_dependency_rule_acts_as_allowlist(self): .matching("typing") ) violations = rule.check() - dep_violations = [ - v - for v in violations - if isinstance(v, ViolatingExternalModuleDependency) - ] + dep_violations = [v for v in violations if isinstance(v, ViolatingExternalModuleDependency)] assert {v.dependency.target_label for v in dep_violations} == { "json", "os", @@ -181,9 +156,7 @@ def test_lines_of_code_limit(self): .adhere_to(lambda f: f.lines_of_code < 1000, "File too long") ) violations = rule.check() - custom_violations = [ - v for v in violations if isinstance(v, CustomFileViolation) - ] + custom_violations = [v for v in violations if isinstance(v, CustomFileViolation)] assert len(custom_violations) == 0 def test_extension_check(self): @@ -193,9 +166,7 @@ def test_extension_check(self): .adhere_to(lambda f: f.extension == ".py", "Must be Python files") ) violations = rule.check() - custom_violations = [ - v for v in violations if isinstance(v, CustomFileViolation) - ] + custom_violations = [v for v in violations if isinstance(v, CustomFileViolation)] assert len(custom_violations) == 0 @@ -204,27 +175,15 @@ def setup_method(self): clear_graph_cache() def test_empty_test_violation(self): - rule = ( - project_files(FIXTURES_DIR) - .in_folder("**/nonexistent*") - .should() - .have_no_cycles() - ) + rule = project_files(FIXTURES_DIR).in_folder("**/nonexistent*").should().have_no_cycles() violations = rule.check() - empty_violations = [ - v for v in violations if isinstance(v, EmptyTestViolation) - ] + empty_violations = [v for v in violations if isinstance(v, EmptyTestViolation)] assert len(empty_violations) == 1 def test_empty_test_allowed(self): from archunitpython.common.fluentapi.checkable import CheckOptions - rule = ( - project_files(FIXTURES_DIR) - .in_folder("**/nonexistent*") - .should() - .have_no_cycles() - ) + rule = project_files(FIXTURES_DIR).in_folder("**/nonexistent*").should().have_no_cycles() violations = rule.check(CheckOptions(allow_empty_tests=True)) assert len(violations) == 0 @@ -235,11 +194,7 @@ def setup_method(self): def test_full_chain_no_cycles(self): violations = ( - project_files(FIXTURES_DIR) - .in_folder("**/services*") - .should() - .have_no_cycles() - .check() + project_files(FIXTURES_DIR).in_folder("**/services*").should().have_no_cycles().check() ) cycle_violations = [v for v in violations if isinstance(v, ViolatingCycle)] assert len(cycle_violations) == 0 @@ -253,9 +208,7 @@ def test_full_chain_depend_on(self): .in_folder("**/utils*") .check() ) - dep_violations = [ - v for v in violations if isinstance(v, ViolatingFileDependency) - ] + dep_violations = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_violations) == 0 def test_builder_with_multiple_filters(self): @@ -330,9 +283,7 @@ def test_type_checking_imports_affect_rules_by_default(self): .in_folder("**/models*") ) violations = rule.check() - dep_violations = [ - v for v in violations if isinstance(v, ViolatingFileDependency) - ] + dep_violations = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_violations) == 1 def test_type_checking_imports_can_be_ignored_for_rules(self): @@ -346,10 +297,6 @@ def test_type_checking_imports_can_be_ignored_for_rules(self): .depend_on_files() .in_folder("**/models*") ) - violations = rule.check( - CheckOptions(ignore_type_checking_imports=True) - ) - dep_violations = [ - v for v in violations if isinstance(v, ViolatingFileDependency) - ] + violations = rule.check(CheckOptions(ignore_type_checking_imports=True)) + dep_violations = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_violations) == 0 diff --git a/tests/fixtures/metrics_project/service.py b/tests/fixtures/metrics_project/service.py index 1d2de42..9d0f92b 100644 --- a/tests/fixtures/metrics_project/service.py +++ b/tests/fixtures/metrics_project/service.py @@ -19,8 +19,7 @@ def __init__(self): self.logger = None @abstractmethod - def process(self): - ... + def process(self): ... def get_cache(self): return self.cache diff --git a/tests/integration/test_e2e.py b/tests/integration/test_e2e.py index 8ff854a..9ecc99d 100644 --- a/tests/integration/test_e2e.py +++ b/tests/integration/test_e2e.py @@ -123,6 +123,7 @@ def test_common_does_not_depend_on_files(self): ) violations = rule.check() from archunitpython.files.assertion.depend_on_files import ViolatingFileDependency + dep_v = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_v) == 0, format_violations(dep_v) @@ -139,6 +140,7 @@ def test_common_does_not_depend_on_slices(self): ) violations = rule.check() from archunitpython.files.assertion.depend_on_files import ViolatingFileDependency + dep_v = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_v) == 0, format_violations(dep_v) @@ -155,6 +157,7 @@ def test_common_does_not_depend_on_testing(self): ) violations = rule.check() from archunitpython.files.assertion.depend_on_files import ViolatingFileDependency + dep_v = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_v) == 0, format_violations(dep_v) @@ -177,16 +180,12 @@ def test_layered_architecture_check(self): ) violations = rule.check() from archunitpython.files.assertion.depend_on_files import ViolatingFileDependency + dep_v = [v for v in violations if isinstance(v, ViolatingFileDependency)] assert len(dep_v) == 0 def test_metrics_on_sample(self): - rule = ( - metrics(FIXTURES_DIR) - .count() - .method_count() - .should_be_below(100) - ) + rule = metrics(FIXTURES_DIR).count().method_count().should_be_below(100) assert_passes(rule) def test_custom_condition(self): diff --git a/tests/metrics/test_export.py b/tests/metrics/test_export.py index b781e84..cad0fd9 100644 --- a/tests/metrics/test_export.py +++ b/tests/metrics/test_export.py @@ -15,9 +15,7 @@ def test_basic_export(self): def test_custom_title(self): data = {"Metric": "Value"} - html = MetricsExporter.export_as_html( - data, ExportOptions(title="My Report") - ) + html = MetricsExporter.export_as_html(data, ExportOptions(title="My Report")) assert "My Report" in html def test_custom_css(self): @@ -29,9 +27,7 @@ def test_custom_css(self): def test_no_timestamp(self): data = {"Metric": "Value"} - html = MetricsExporter.export_as_html( - data, ExportOptions(include_timestamp=False) - ) + html = MetricsExporter.export_as_html(data, ExportOptions(include_timestamp=False)) assert "Generated:" not in html def test_file_output(self, tmp_path): diff --git a/tests/metrics/test_metrics.py b/tests/metrics/test_metrics.py index c63c305..85d7dd9 100644 --- a/tests/metrics/test_metrics.py +++ b/tests/metrics/test_metrics.py @@ -40,6 +40,7 @@ # --- TICKET-14: Class Info Extraction --- + class TestExtractClassInfo: def test_extracts_classes(self): classes = extract_class_info(FIXTURES_DIR) @@ -85,6 +86,7 @@ def test_enhanced_extraction(self): # --- TICKET-15: Count Metrics --- + class TestCountMetrics: def test_method_count(self): ci = ClassInfo( @@ -125,6 +127,7 @@ def test_function_count(self): # --- TICKET-16: LCOM Metrics --- + def _make_perfect_cohesion(): """All methods access all fields → perfect cohesion.""" return ClassInfo( @@ -219,6 +222,7 @@ def test_all_defined(self): # --- TICKET-17: Distance Metrics --- + class TestDistanceMetrics: def test_project_summary(self): results = extract_enhanced_class_info(FIXTURES_DIR) @@ -234,6 +238,7 @@ def test_empty_project(self): # --- Threshold Checking --- + class TestCheckThreshold: def test_below(self): assert check_threshold(10, 5, "below") is True # 10 >= 5 → violation diff --git a/tests/metrics/test_metrics_fluentapi.py b/tests/metrics/test_metrics_fluentapi.py index fd812ec..278ba82 100644 --- a/tests/metrics/test_metrics_fluentapi.py +++ b/tests/metrics/test_metrics_fluentapi.py @@ -15,83 +15,50 @@ class TestCountMetricsFluentAPI: def test_method_count_below(self): - violations = ( - metrics(FIXTURES_DIR).count().method_count().should_be_below(50).check() - ) + violations = metrics(FIXTURES_DIR).count().method_count().should_be_below(50).check() metric_violations = [v for v in violations if isinstance(v, MetricViolation)] assert len(metric_violations) == 0 def test_method_count_violation(self): - violations = ( - metrics(FIXTURES_DIR).count().method_count().should_be_below(2).check() - ) + violations = metrics(FIXTURES_DIR).count().method_count().should_be_below(2).check() metric_violations = [v for v in violations if isinstance(v, MetricViolation)] assert len(metric_violations) > 0 def test_field_count_below(self): - violations = ( - metrics(FIXTURES_DIR).count().field_count().should_be_below(20).check() - ) + violations = metrics(FIXTURES_DIR).count().field_count().should_be_below(20).check() metric_violations = [v for v in violations if isinstance(v, MetricViolation)] assert len(metric_violations) == 0 def test_lines_of_code_below(self): - violations = ( - metrics(FIXTURES_DIR) - .count() - .lines_of_code() - .should_be_below(5000) - .check() - ) + violations = metrics(FIXTURES_DIR).count().lines_of_code().should_be_below(5000).check() file_violations = [v for v in violations if isinstance(v, FileCountViolation)] assert len(file_violations) == 0 def test_lines_of_code_violation(self): - violations = ( - metrics(FIXTURES_DIR) - .count() - .lines_of_code() - .should_be_below(5) - .check() - ) + violations = metrics(FIXTURES_DIR).count().lines_of_code().should_be_below(5).check() file_violations = [v for v in violations if isinstance(v, FileCountViolation)] assert len(file_violations) > 0 class TestLCOMMetricsFluentAPI: def test_lcom96b_below(self): - violations = ( - metrics(FIXTURES_DIR).lcom().lcom96b().should_be_below(1.0).check() - ) + violations = metrics(FIXTURES_DIR).lcom().lcom96b().should_be_below(1.0).check() metric_violations = [v for v in violations if isinstance(v, MetricViolation)] assert len(metric_violations) == 0 def test_lcom4_below(self): - violations = ( - metrics(FIXTURES_DIR).lcom().lcom4().should_be_below(10).check() - ) + violations = metrics(FIXTURES_DIR).lcom().lcom4().should_be_below(10).check() metric_violations = [v for v in violations if isinstance(v, MetricViolation)] assert len(metric_violations) == 0 class TestDistanceMetricsFluentAPI: def test_abstractness_below(self): - violations = ( - metrics(FIXTURES_DIR) - .distance() - .abstractness() - .should_be_below(1.0) - .check() - ) + violations = metrics(FIXTURES_DIR).distance().abstractness().should_be_below(1.0).check() assert isinstance(violations, list) def test_not_in_zone_of_uselessness(self): - violations = ( - metrics(FIXTURES_DIR) - .distance() - .not_in_zone_of_uselessness() - .check() - ) + violations = metrics(FIXTURES_DIR).distance().not_in_zone_of_uselessness().check() assert isinstance(violations, list) diff --git a/tests/slices/test_slices.py b/tests/slices/test_slices.py index f65df41..f53b327 100644 --- a/tests/slices/test_slices.py +++ b/tests/slices/test_slices.py @@ -28,6 +28,7 @@ # --- TICKET-10: Slicing Projections --- + class TestSliceByPattern: def test_basic_pattern(self): mapper = slice_by_pattern("src/(**)/**") @@ -59,6 +60,7 @@ def test_self_edge_filtered(self): class TestSliceByRegex: def test_regex_capture(self): import re + mapper = slice_by_regex(re.compile(r"src/([^/]+)/")) edge = Edge(source="src/controllers/ctrl.py", target="src/services/svc.py", external=False) result = mapper(edge) @@ -101,6 +103,7 @@ def test_filters_self_edge(self): # --- TICKET-11: PlantUML Parsing --- + class TestGenerateRule: def test_basic_diagram(self): puml = """ @@ -170,6 +173,7 @@ def test_export_simple(self): # --- TICKET-12: Slice Assertions --- + class TestGatherViolations: def test_forbidden_dependency_found(self): edges = [ProjectedEdge(source_label="ui", target_label="db")] @@ -202,7 +206,9 @@ def test_ignore_orphan_slices(self): edges = [ProjectedEdge(source_label="unknown", target_label="db")] rules = [] violations = gather_positive_violations( - edges, rules, ["db"], + edges, + rules, + ["db"], CoherenceOptions(ignore_orphan_slices=True), ) assert len(violations) == 0 @@ -210,12 +216,14 @@ def test_ignore_orphan_slices(self): # --- TICKET-13: Slices Fluent API --- + class TestSlicesFluentAPI: def setup_method(self): clear_graph_cache() def test_should_not_contain_dependency(self): import re + rule = ( project_slices(FIXTURES_DIR) .defined_by_regex(re.compile(r"/([^/]+)/[^/]+\.py$")) @@ -228,6 +236,7 @@ def test_should_not_contain_dependency(self): def test_adhere_to_diagram_in_file(self): import re + puml_path = os.path.join(FIXTURES_DIR, "architecture.puml") rule = ( project_slices(FIXTURES_DIR) @@ -245,6 +254,7 @@ def test_adhere_to_diagram_in_file(self): def test_adhere_to_diagram_inline(self): import re + puml = """ @startuml component [controllers] From bab0ec254efb3668236092311a3ac188608d7c0b Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Mon, 15 Jun 2026 18:37:16 +0530 Subject: [PATCH 7/7] style: code format --- src/archunitpython/common/extraction/extract_graph.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/archunitpython/common/extraction/extract_graph.py b/src/archunitpython/common/extraction/extract_graph.py index 52c7797..13e7494 100644 --- a/src/archunitpython/common/extraction/extract_graph.py +++ b/src/archunitpython/common/extraction/extract_graph.py @@ -58,13 +58,11 @@ def extract_graph( project_path = os.getcwd() project_path = os.path.abspath(project_path) - 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 + 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) if options and options.clear_cache: _graph_cache.pop(cache_key, None)
MetricValue