Skip to content

Commit 1f70b01

Browse files
Merge pull request #7 from DebanKsahu/perf/_in_type_checking
Optimize type checking range lookup with binary search
2 parents dd08680 + 03ca332 commit 1f70b01

26 files changed

Lines changed: 112 additions & 314 deletions

File tree

src/archunitpython/common/extraction/extract_graph.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,8 @@ def extract_graph(
9191
excludes = (
9292
list(set(exclude_patterns)) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
9393
)
94-
ignore_type_checking_imports = bool(
95-
options and options.ignore_type_checking_imports
96-
)
97-
cache_key = _build_cache_key(
98-
project_path, excludes, ignore_type_checking_imports
99-
)
94+
ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
95+
cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
10096

10197
if options and options.clear_cache:
10298
_graph_cache.pop(cache_key, None)
@@ -137,6 +133,7 @@ def _extract_graph_uncached(
137133

138134
edges: list[Edge] = []
139135
py_files_set = set(py_files)
136+
normalized_py_file_set = {_normalize(f) for f in py_files_set}
140137

141138
for file_path in py_files:
142139
# Add self-referencing edge (ensures the file appears as a node)
@@ -148,7 +145,6 @@ def _extract_graph_uncached(
148145
)
149146
)
150147

151-
# Extract and resolve imports
152148
imports = _extract_located_imports(file_path)
153149
for located_import in imports:
154150
module_name = located_import.module_name
@@ -163,9 +159,7 @@ def _extract_graph_uncached(
163159
)
164160
if resolved and resolved != _normalize(file_path):
165161
# Check if the resolved path is in our project
166-
if not is_external and resolved not in {
167-
_normalize(f) for f in py_files_set
168-
}:
162+
if not is_external and resolved not in normalized_py_file_set:
169163
is_external = True
170164

171165
edges.append(
@@ -190,11 +184,7 @@ def _find_python_files(root: str, exclude: list[str]) -> list[str]:
190184
py_files: list[str] = []
191185
for dirpath, dirnames, filenames in os.walk(root):
192186
# Filter out excluded directories in-place
193-
dirnames[:] = [
194-
d
195-
for d in dirnames
196-
if not _should_exclude(d, exclude)
197-
]
187+
dirnames[:] = [d for d in dirnames if not _should_exclude(d, exclude)]
198188

199189
for filename in filenames:
200190
if filename.endswith(".py") and not _should_exclude(filename, exclude):
@@ -349,18 +339,14 @@ def _find_type_checking_ranges(tree: ast.Module) -> list[tuple[int, int]]:
349339
if is_type_checking and node.body:
350340
start = node.body[0].lineno
351341
end = max(
352-
getattr(n, "end_lineno", n.lineno)
353-
for n in node.body
354-
if hasattr(n, "lineno")
342+
getattr(n, "end_lineno", n.lineno) for n in node.body if hasattr(n, "lineno")
355343
)
356344
ranges.append((start, end))
357345

358-
return ranges
346+
return sorted(ranges, key=lambda ele: ele[0])
359347

360348

361-
def _in_type_checking(
362-
node: ast.AST, ranges: list[tuple[int, int]]
363-
) -> bool:
349+
def _in_type_checking(node: ast.AST, ranges: list[tuple[int, int]]) -> bool:
364350
"""Check if a node is inside a TYPE_CHECKING block."""
365351
if not hasattr(node, "lineno"):
366352
return False

src/archunitpython/common/pattern_matching.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ def matches_pattern(file_path: str, filter_: Filter) -> bool:
5050
return bool(filter_.regexp.search(target_string))
5151

5252

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

src/archunitpython/common/projection/cycles/johnsons_apsp.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,9 @@ def _explore_neighbours(self, current_node: NumberNode) -> None:
5353
if self._is_part_of_current_start_cycle(current_node):
5454
self._unblock(current_node)
5555
else:
56-
for neighbour in CycleUtils.get_outgoing_neighbours(
57-
current_node, self._graph
58-
):
56+
for neighbour in CycleUtils.get_outgoing_neighbours(current_node, self._graph):
5957
if self._is_blocked(neighbour):
60-
self._blocked_map.append(
61-
_BlockedBy(blocked=current_node, by=neighbour)
62-
)
58+
self._blocked_map.append(_BlockedBy(blocked=current_node, by=neighbour))
6359

6460
def _unblock(self, node: NumberNode) -> None:
6561
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:
7470
if self._start is None:
7571
return False
7672
for cycle in self._cycles:
77-
if (
78-
cycle[0].from_node == self._start.node
79-
and any(e.from_node == current_node.node for e in cycle)
73+
if cycle[0].from_node == self._start.node and any(
74+
e.from_node == current_node.node for e in cycle
8075
):
8176
return True
8277
return False

src/archunitpython/common/projection/cycles/tarjan_scc.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ def __init__(self, node_id: int) -> None:
1818
class TarjanSCC:
1919
"""Tarjan's algorithm for finding strongly connected components."""
2020

21-
def find_strongly_connected_components(
22-
self, edges: list[NumberEdge]
23-
) -> list[list[NumberEdge]]:
21+
def find_strongly_connected_components(self, edges: list[NumberEdge]) -> list[list[NumberEdge]]:
2422
"""Find all strongly connected components in the graph.
2523
2624
Returns a list of edge lists, where each inner list contains
@@ -78,9 +76,7 @@ def _visit(self, vertex: _Vertex) -> None:
7876
if scc_vertices:
7977
scc_ids = {v.id for v in scc_vertices}
8078
scc_edges = [
81-
e
82-
for e in self._edges
83-
if e.from_node in scc_ids and e.to_node in scc_ids
79+
e for e in self._edges if e.from_node in scc_ids and e.to_node in scc_ids
8480
]
8581
if scc_edges:
8682
self._sccs.append(scc_edges)

src/archunitpython/common/projection/project_cycles.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ def _from_domain(self, cycles: list[list[NumberEdge]]) -> ProjectedCycles:
7272
(
7373
se
7474
for se in self._source_edges
75-
if se.source_label == source_label
76-
and se.target_label == target_label
75+
if se.source_label == source_label and se.target_label == target_label
7776
),
7877
None,
7978
)

src/archunitpython/common/util/logger.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ def _ensure_file_handler(self, options: LoggingOptions) -> None:
4444

4545
mode = "a" if options.append_to_log_file else "w"
4646
self._file_handler = logging.FileHandler(str(log_path), mode=mode)
47-
self._file_handler.setFormatter(
48-
logging.Formatter("[%(levelname)s] %(message)s")
49-
)
47+
self._file_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
5048
self._logger.addHandler(self._file_handler)
5149

5250
def _log(self, level: str, options: LoggingOptions | None, message: str) -> None:

src/archunitpython/files/assertion/custom_file_logic.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ def gather_custom_file_violations(
8383

8484
for node in nodes:
8585
# Check if node matches all pre-filters
86-
if pre_filters and not all(
87-
matches_pattern(node.label, f) for f in pre_filters
88-
):
86+
if pre_filters and not all(matches_pattern(node.label, f) for f in pre_filters):
8987
continue
9088

9189
file_info = _build_file_info(node.label)
@@ -94,14 +92,10 @@ def gather_custom_file_violations(
9492
if is_negated:
9593
# shouldNot: violation if condition IS True
9694
if result:
97-
violations.append(
98-
CustomFileViolation(message=message, file_info=file_info)
99-
)
95+
violations.append(CustomFileViolation(message=message, file_info=file_info))
10096
else:
10197
# should: violation if condition is NOT True
10298
if not result:
103-
violations.append(
104-
CustomFileViolation(message=message, file_info=file_info)
105-
)
99+
violations.append(CustomFileViolation(message=message, file_info=file_info))
106100

107101
return violations

src/archunitpython/files/assertion/depend_on_external_modules.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ def gather_depend_on_external_module_violations(
3434

3535
for edge in edges:
3636
source_matches = all(
37-
matches_pattern(edge.source_label, filter_)
38-
for filter_ in subject_filters
37+
matches_pattern(edge.source_label, filter_) for filter_ in subject_filters
3938
)
4039
if not source_matches:
4140
continue
@@ -49,16 +48,12 @@ def gather_depend_on_external_module_violations(
4948
if is_negated:
5049
if target_matches:
5150
violations.append(
52-
ViolatingExternalModuleDependency(
53-
dependency=edge, is_negated=True
54-
)
51+
ViolatingExternalModuleDependency(dependency=edge, is_negated=True)
5552
)
5653
else:
5754
if not target_matches:
5855
violations.append(
59-
ViolatingExternalModuleDependency(
60-
dependency=edge, is_negated=False
61-
)
56+
ViolatingExternalModuleDependency(dependency=edge, is_negated=False)
6257
)
6358

6459
return violations

src/archunitpython/files/assertion/depend_on_files.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,19 @@ def gather_depend_on_file_violations(
4141
violations: list[Violation] = []
4242

4343
for edge in edges:
44-
source_matches = all(
45-
matches_pattern(edge.source_label, f) for f in subject_filters
46-
)
44+
source_matches = all(matches_pattern(edge.source_label, f) for f in subject_filters)
4745
if not source_matches:
4846
continue
4947

50-
target_matches = all(
51-
matches_pattern(edge.target_label, f) for f in object_filters
52-
)
48+
target_matches = all(matches_pattern(edge.target_label, f) for f in object_filters)
5349

5450
if is_negated:
5551
# shouldNot: violation if dependency EXISTS
5652
if target_matches:
57-
violations.append(
58-
ViolatingFileDependency(dependency=edge, is_negated=True)
59-
)
53+
violations.append(ViolatingFileDependency(dependency=edge, is_negated=True))
6054
else:
6155
# should: violation if dependency does NOT match
6256
if not target_matches:
63-
violations.append(
64-
ViolatingFileDependency(dependency=edge, is_negated=False)
65-
)
57+
violations.append(ViolatingFileDependency(dependency=edge, is_negated=False))
6658

6759
return violations

src/archunitpython/files/fluentapi/files.py

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,11 @@ def in_path(self, path: Pattern) -> "FilesShouldCondition":
7676

7777
def should(self) -> "PositiveMatchPatternFileConditionBuilder":
7878
"""Begin positive assertion (files SHOULD ...)."""
79-
return PositiveMatchPatternFileConditionBuilder(
80-
self._project_path, list(self._filters)
81-
)
79+
return PositiveMatchPatternFileConditionBuilder(self._project_path, list(self._filters))
8280

8381
def should_not(self) -> "NegatedMatchPatternFileConditionBuilder":
8482
"""Begin negative assertion (files SHOULD NOT ...)."""
85-
return NegatedMatchPatternFileConditionBuilder(
86-
self._project_path, list(self._filters)
87-
)
83+
return NegatedMatchPatternFileConditionBuilder(self._project_path, list(self._filters))
8884

8985

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

112108
def should(self) -> "PositiveMatchPatternFileConditionBuilder":
113109
"""Begin positive assertion (files SHOULD ...)."""
114-
return PositiveMatchPatternFileConditionBuilder(
115-
self._project_path, list(self._filters)
116-
)
110+
return PositiveMatchPatternFileConditionBuilder(self._project_path, list(self._filters))
117111

118112
def should_not(self) -> "NegatedMatchPatternFileConditionBuilder":
119113
"""Begin negative assertion (files SHOULD NOT ...)."""
120-
return NegatedMatchPatternFileConditionBuilder(
121-
self._project_path, list(self._filters)
122-
)
114+
return NegatedMatchPatternFileConditionBuilder(self._project_path, list(self._filters))
123115

124116

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

136128
def depend_on_files(self) -> "DependOnFileConditionBuilder":
137129
"""Begin dependency assertion - files SHOULD depend on ..."""
138-
return DependOnFileConditionBuilder(
139-
self._project_path, self._filters, is_negated=False
140-
)
130+
return DependOnFileConditionBuilder(self._project_path, self._filters, is_negated=False)
141131

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

193183
def depend_on_files(self) -> "DependOnFileConditionBuilder":
194184
"""Begin dependency assertion - files SHOULD NOT depend on ..."""
195-
return DependOnFileConditionBuilder(
196-
self._project_path, self._filters, is_negated=True
197-
)
185+
return DependOnFileConditionBuilder(self._project_path, self._filters, is_negated=True)
198186

199187
def depend_on_external_modules(
200188
self,
@@ -243,9 +231,7 @@ def adhere_to(
243231
class DependOnFileConditionBuilder:
244232
"""Configure dependency target patterns."""
245233

246-
def __init__(
247-
self, project_path: str | None, filters: list[Filter], is_negated: bool
248-
) -> None:
234+
def __init__(self, project_path: str | None, filters: list[Filter], is_negated: bool) -> None:
249235
self._project_path = project_path
250236
self._filters = filters
251237
self._is_negated = is_negated
@@ -285,9 +271,7 @@ def in_path(self, path: Pattern) -> "DependOnFileCondition":
285271
class DependOnExternalModuleConditionBuilder:
286272
"""Configure external module dependency target patterns."""
287273

288-
def __init__(
289-
self, project_path: str | None, filters: list[Filter], is_negated: bool
290-
) -> None:
274+
def __init__(self, project_path: str | None, filters: list[Filter], is_negated: bool) -> None:
291275
self._project_path = project_path
292276
self._filters = filters
293277
self._is_negated = is_negated
@@ -447,9 +431,7 @@ def check(self, options: CheckOptions | None = None) -> list[Violation]:
447431
if empty is not None:
448432
return empty
449433

450-
return gather_regex_matching_violations(
451-
nodes, self._check_filters, self._is_negated
452-
)
434+
return gather_regex_matching_violations(nodes, self._check_filters, self._is_negated)
453435

454436

455437
class CustomFileCheckableCondition:

0 commit comments

Comments
 (0)