diff --git a/src/archunitpython/common/extraction/extract_graph.py b/src/archunitpython/common/extraction/extract_graph.py index 4d05d72..4acf4fc 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 @@ -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) @@ -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) @@ -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( @@ -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): @@ -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( 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(
| Metric | Value |
|---|