archunitpython 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  """ArchUnitPython - Architecture testing library for Python projects."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.4.0"
4
4
 
5
5
  # Files API
6
6
  # Common
@@ -29,6 +29,8 @@ _DEFAULT_EXCLUDE = [
29
29
  "*.egg-info",
30
30
  ]
31
31
 
32
+ _ARCHIGNORE_FILE = ".archignore"
33
+
32
34
  _IGNORE_DIRECTIVE_REGEX = re.compile(
33
35
  r"#\s*archunit(?::|-)\s*ignore"
34
36
  r"(?:\([^)]*\))?"
@@ -88,9 +90,7 @@ def extract_graph(
88
90
  project_path = os.getcwd()
89
91
 
90
92
  project_path = os.path.abspath(project_path)
91
- excludes = (
92
- list(set(exclude_patterns)) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
93
- )
93
+ excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
94
94
  ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
95
95
  cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
96
96
 
@@ -122,6 +122,34 @@ def _build_cache_key(
122
122
  )
123
123
 
124
124
 
125
+ def _resolve_exclude_patterns(
126
+ project_path: str,
127
+ exclude_patterns: list[str] | None,
128
+ ) -> list[str]:
129
+ """Resolve exclude patterns (explicit or defaults) plus any .archignore patterns."""
130
+ excludes = list(exclude_patterns) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
131
+ excludes.extend(_load_archignore_patterns(project_path))
132
+ return excludes
133
+
134
+
135
+ def _load_archignore_patterns(project_path: str) -> list[str]:
136
+ """Load .archignore patterns from a project root, if present."""
137
+ archignore_path = os.path.join(project_path, _ARCHIGNORE_FILE)
138
+ try:
139
+ with open(archignore_path, "r", encoding="utf-8", errors="replace") as f:
140
+ lines = f.readlines()
141
+ except OSError:
142
+ return []
143
+
144
+ patterns: list[str] = []
145
+ for line in lines:
146
+ pattern = line.strip()
147
+ if not pattern or pattern.startswith("#"):
148
+ continue
149
+ patterns.append(pattern)
150
+ return patterns
151
+
152
+
125
153
  def _extract_graph_uncached(
126
154
  project_path: str,
127
155
  exclude_patterns: list[str],
@@ -160,7 +188,7 @@ def _extract_graph_uncached(
160
188
  if resolved and resolved != _normalize(file_path):
161
189
  # Check if the resolved path is in our project
162
190
  if not is_external and resolved not in normalized_py_file_set:
163
- is_external = True
191
+ continue
164
192
 
165
193
  edges.append(
166
194
  Edge(
@@ -182,25 +210,65 @@ def _normalize(path: str) -> str:
182
210
  def _find_python_files(root: str, exclude: list[str]) -> list[str]:
183
211
  """Recursively find all .py files, excluding specified patterns."""
184
212
  py_files: list[str] = []
213
+ root = os.path.abspath(root)
185
214
  for dirpath, dirnames, filenames in os.walk(root):
186
215
  # Filter out excluded directories in-place
187
- dirnames[:] = [d for d in dirnames if not _should_exclude(d, exclude)]
216
+ dirnames[:] = [
217
+ d
218
+ for d in dirnames
219
+ if not _should_exclude_path(os.path.join(dirpath, d), root, exclude, is_dir=True)
220
+ ]
188
221
 
189
222
  for filename in filenames:
190
- if filename.endswith(".py") and not _should_exclude(filename, exclude):
191
- full_path = os.path.join(dirpath, filename)
223
+ full_path = os.path.join(dirpath, filename)
224
+ if filename.endswith(".py") and not _should_exclude_path(
225
+ full_path, root, exclude, is_dir=False
226
+ ):
192
227
  py_files.append(os.path.abspath(full_path))
193
228
 
194
229
  return py_files
195
230
 
196
231
 
197
- def _should_exclude(name: str, patterns: list[str]) -> bool:
198
- """Check if a name matches any exclude pattern."""
232
+ def _should_exclude_path(
233
+ path: str,
234
+ root: str,
235
+ patterns: list[str],
236
+ *,
237
+ is_dir: bool,
238
+ ) -> bool:
239
+ """Check if a path matches any exclude pattern."""
199
240
  import fnmatch
200
241
 
201
- for pattern in patterns:
202
- if fnmatch.fnmatch(name, pattern):
242
+ rel_path = _normalize(os.path.relpath(path, root))
243
+ name = os.path.basename(path)
244
+
245
+ for raw_pattern in patterns:
246
+ pattern = raw_pattern.strip().replace("\\", "/")
247
+ if not pattern or pattern.startswith("#"):
248
+ continue
249
+
250
+ pattern = pattern.removeprefix("./")
251
+ anchored = pattern.startswith("/")
252
+ if anchored:
253
+ pattern = pattern[1:]
254
+
255
+ dir_only = pattern.endswith("/")
256
+ if dir_only:
257
+ pattern = pattern.rstrip("/")
258
+ if not is_dir:
259
+ continue
260
+
261
+ if not pattern:
262
+ continue
263
+
264
+ if "/" in pattern or anchored:
265
+ if fnmatch.fnmatch(rel_path, pattern):
266
+ return True
267
+ if is_dir and rel_path == pattern:
268
+ return True
269
+ elif fnmatch.fnmatch(name, pattern):
203
270
  return True
271
+
204
272
  return False
205
273
 
206
274
 
@@ -5,7 +5,10 @@ from __future__ import annotations
5
5
  import ast
6
6
  import os
7
7
 
8
- from archunitpython.common.extraction.extract_graph import _DEFAULT_EXCLUDE, _find_python_files
8
+ from archunitpython.common.extraction.extract_graph import (
9
+ _find_python_files,
10
+ _resolve_exclude_patterns,
11
+ )
9
12
  from archunitpython.metrics.common.types import (
10
13
  ClassInfo,
11
14
  EnhancedClassInfo,
@@ -33,7 +36,7 @@ def extract_class_info(
33
36
  project_path = os.getcwd()
34
37
 
35
38
  project_path = os.path.abspath(project_path)
36
- excludes = exclude_patterns if exclude_patterns is not None else _DEFAULT_EXCLUDE
39
+ excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
37
40
  py_files = _find_python_files(project_path, excludes)
38
41
 
39
42
  classes: list[ClassInfo] = []
@@ -53,7 +56,7 @@ def extract_enhanced_class_info(
53
56
  project_path = os.getcwd()
54
57
 
55
58
  project_path = os.path.abspath(project_path)
56
- excludes = exclude_patterns if exclude_patterns is not None else _DEFAULT_EXCLUDE
59
+ excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
57
60
  py_files = _find_python_files(project_path, excludes)
58
61
 
59
62
  results: list[FileAnalysisResult] = []
@@ -251,13 +251,13 @@ class FileMetricCondition(RuleRationaleMixin):
251
251
  import os
252
252
 
253
253
  from archunitpython.common.extraction.extract_graph import (
254
- _DEFAULT_EXCLUDE,
255
254
  _find_python_files,
255
+ _resolve_exclude_patterns,
256
256
  )
257
257
 
258
258
  project = self._project_path or os.getcwd()
259
259
  project = os.path.abspath(project)
260
- files = _find_python_files(project, _DEFAULT_EXCLUDE)
260
+ files = _find_python_files(project, _resolve_exclude_patterns(project, None))
261
261
  violations: list[Violation] = []
262
262
 
263
263
  for file_path in files:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: archunitpython
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Architecture testing library for Python projects. Enforce dependency rules, detect cycles, validate metrics.
5
5
  Project-URL: Homepage, https://github.com/LukasNiessen/ArchUnitPython
6
6
  Project-URL: Repository, https://github.com/LukasNiessen/ArchUnitPython.git
@@ -188,6 +188,24 @@ options = CheckOptions(
188
188
  violations = rule.check(options)
189
189
  ```
190
190
 
191
+ ### Excluding Files With `.archignore`
192
+
193
+ Add a `.archignore` file to your project root to permanently exclude generated or
194
+ irrelevant files from architecture checks and file-based metrics:
195
+
196
+ ```gitignore
197
+ # Generated code
198
+ generated/
199
+
200
+ # Migration scripts
201
+ migrations/*.py
202
+
203
+ # A single root-level file
204
+ /legacy_adapter.py
205
+ ```
206
+
207
+ Patterns support comments, blank lines, glob syntax, root-relative paths, path
208
+ patterns, and directory patterns with a trailing `/`.
191
209
  ### Explaining Rules With `.because(...)`
192
210
 
193
211
  Attach a rationale to a rule so failing assertions explain why the rule exists:
@@ -450,7 +468,7 @@ def test_no_forbidden_dependency():
450
468
 
451
469
  Generate dependency graph reports in multiple formats and narrow them to the part of the codebase you want to inspect.
452
470
 
453
- **Using `requests` library repo for example**
471
+ **Using [`requests`](https://github.com/psf/requests) library repo for example**
454
472
 
455
473
  ```python
456
474
  from archunitpython import project_graph
@@ -463,7 +481,7 @@ def test_export_dependency_graph_reports():
463
481
  if __name__ == "__main__":
464
482
  test_export_dependency_graph_reports()
465
483
  ```
466
- **Rendered mermain diagram**
484
+ **Exported mermaid diagram**
467
485
  ``` mermaid
468
486
  flowchart LR
469
487
  n0["__init__.py"]
@@ -1,4 +1,4 @@
1
- archunitpython/__init__.py,sha256=alLY-bGp-tUtdb8BRV0r8K-qIZKkWYnJ5K-mTDnFOrQ,1154
1
+ archunitpython/__init__.py,sha256=8MQMKFXjGNHJdCtifcWJcAgXLkjqJhDiXYxqp2fmxlQ,1154
2
2
  archunitpython/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  archunitpython/common/__init__.py,sha256=TKL39Z0kBpWqMH99jU4LsDSUZ5lQ_rcAJfLnmUTDTOI,656
4
4
  archunitpython/common/pattern_matching.py,sha256=HMAfo8GsooHvA4d2IxCi3btrYTdwfOQXw4bDNr117P0,2669
@@ -9,7 +9,7 @@ archunitpython/common/assertion/violation.py,sha256=TnMOykN3kPoGrSf1KcxBXYRfaxfX
9
9
  archunitpython/common/error/__init__.py,sha256=UWcdIKpGAJvo4WEGOVSaUjQQnKoPi8R496uDGXBWKsE,116
10
10
  archunitpython/common/error/errors.py,sha256=y7mcXoZPyK7uD2dMMO1qBt1y7KcIeGlRYiVeI6bjms4,249
11
11
  archunitpython/common/extraction/__init__.py,sha256=RkJOcxJoLYQxEagh3uWhjsUCprK3Nr3Bn3o7nmzk4_Q,284
12
- archunitpython/common/extraction/extract_graph.py,sha256=HIfAZLZiv4Zbi1Nm8jmNnnu8ggn0wQXunBlZP_QK1x8,15254
12
+ archunitpython/common/extraction/extract_graph.py,sha256=PcKInkTFLBxWO5RqppcGJKDii2LrTXgkxm7a0G16ZAg,17186
13
13
  archunitpython/common/extraction/graph.py,sha256=Rk-0eDDOLoHvScO27J2JzXjyMq3e5eKtrHnFBh5B6SU,1052
14
14
  archunitpython/common/fluentapi/__init__.py,sha256=LeS7qS2p9-FqqBhDL8xRPex__W5Qk0UWfcpl7nVVLfI,178
15
15
  archunitpython/common/fluentapi/checkable.py,sha256=BJFYibVhHLMFlnWdISPCIL3DYI1_3JN2-1jEQOaVaGE,1547
@@ -56,10 +56,10 @@ archunitpython/metrics/calculation/lcom.py,sha256=LK_kNT-BM6yuR5TUOLWQLkVvMpqR6q
56
56
  archunitpython/metrics/common/__init__.py,sha256=xtEyAhS4X0UbJXHxnpdiXljVZmKIKKekZAZ1zflSPFE,335
57
57
  archunitpython/metrics/common/types.py,sha256=w1BUlw6p_3K8qZDoQXv50P_CcKixGYiyS_YBGYILySA,1601
58
58
  archunitpython/metrics/extraction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- archunitpython/metrics/extraction/extract_class_info.py,sha256=IFokUFwNaExmPtKnHW5hgnYWodRtBM3cWhXBGb7lcjs,7751
59
+ archunitpython/metrics/extraction/extract_class_info.py,sha256=u-i_AkRRdIbOVXhH7LVCCVIxRrz4IX6nOqukPxbB5Iw,7747
60
60
  archunitpython/metrics/fluentapi/__init__.py,sha256=HD72MWF0hCh2LzKfyk3mMJp2tf7TJebpwTtZ_kppjso,84
61
61
  archunitpython/metrics/fluentapi/export_utils.py,sha256=1r-zTQAUqn_agp4_lhbcffdzfdASNY3LebYvKAG9kcQ,2333
62
- archunitpython/metrics/fluentapi/metrics.py,sha256=EaH1seQKppXyx7DiPz1D-S1YYRt48OBWqdKn47BQhoA,19338
62
+ archunitpython/metrics/fluentapi/metrics.py,sha256=8OBOL3jVKbM9v_CjNNM9FMfV1JBfl5UEd1-PPOEScRs,19371
63
63
  archunitpython/metrics/projection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
64
  archunitpython/slices/__init__.py,sha256=tb8MoZiEqIdmWy7uyhmEtMNm5Fxjuw6atSvDsD131DE,96
65
65
  archunitpython/slices/assertion/__init__.py,sha256=MRwV3d69ljAJ7hFYdxI6UDNC8U4Dm1ImeuLUVKEC9To,280
@@ -77,7 +77,7 @@ archunitpython/testing/common/__init__.py,sha256=Wc4bC-N4t6giChKjj6wuTGKkb39thcY
77
77
  archunitpython/testing/common/color_utils.py,sha256=2I8Z1SZfWhhgudMgmXY6PPydGxGl35cK_To-GXnSyJg,1226
78
78
  archunitpython/testing/common/violation_factory.py,sha256=yWlvv2U7u-7KoTMfp1QCeJXxTLpPEagKqDHZmPSyrlk,4677
79
79
  archunitpython/testing/pytest_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
- archunitpython-1.3.0.dist-info/METADATA,sha256=79jbCuDyAMQr3aY3A_fesygIwS8hF8ceYaPRzJvUmrU,30275
81
- archunitpython-1.3.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
82
- archunitpython-1.3.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
83
- archunitpython-1.3.0.dist-info/RECORD,,
80
+ archunitpython-1.4.0.dist-info/METADATA,sha256=z6zirY281brtJcOrmADYb_Mrll-MNzEC6GKuTPQRkA4,30762
81
+ archunitpython-1.4.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
82
+ archunitpython-1.4.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
83
+ archunitpython-1.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any