archunitpython 1.4.0__py3-none-any.whl → 1.5.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.4.0"
3
+ __version__ = "1.5.0"
4
4
 
5
5
  # Files API
6
6
  # Common
@@ -12,6 +12,7 @@ from archunitpython.common import (
12
12
  Violation,
13
13
  )
14
14
  from archunitpython.common.extraction import clear_graph_cache, extract_graph
15
+ from archunitpython.config import ConfiguredRule, rules_from_config
15
16
  from archunitpython.files import files, project_files
16
17
  from archunitpython.graph import dependency_graph, project_graph
17
18
  from archunitpython.layers import layers, project_layers
@@ -35,6 +36,9 @@ __all__ = [
35
36
  # Layers
36
37
  "project_layers",
37
38
  "layers",
39
+ # Config
40
+ "rules_from_config",
41
+ "ConfiguredRule",
38
42
  # Slices
39
43
  "project_slices",
40
44
  # Metrics
@@ -0,0 +1,5 @@
1
+ """Configuration-file support for common architecture rules."""
2
+
3
+ from archunitpython.config.loader import ConfiguredRule, rules_from_config
4
+
5
+ __all__ = ["ConfiguredRule", "rules_from_config"]
@@ -0,0 +1,119 @@
1
+ """Load common architecture rules from a JSON configuration file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from archunitpython.common.assertion.violation import Violation
12
+ from archunitpython.common.error.errors import UserError
13
+ from archunitpython.common.fluentapi.checkable import Checkable, CheckOptions
14
+ from archunitpython.files.fluentapi.files import project_files
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ConfiguredRule:
19
+ """A named rule loaded from a configuration file."""
20
+
21
+ name: str
22
+ rule: Checkable
23
+
24
+ def check(self, options: CheckOptions | None = None) -> list[Violation]:
25
+ """Run the configured rule."""
26
+ return self.rule.check(options)
27
+
28
+
29
+ def rules_from_config(config_path: str) -> list[ConfiguredRule]:
30
+ """Load common architecture rules from a JSON config file.
31
+
32
+ The fluent Python API remains the primary interface. Config files provide a
33
+ lightweight way to share straightforward rules across projects or teams.
34
+ """
35
+ path = Path(config_path)
36
+ try:
37
+ raw_config = json.loads(path.read_text(encoding="utf-8"))
38
+ except OSError as exc:
39
+ raise UserError(f"Could not read config file: {config_path}") from exc
40
+ except json.JSONDecodeError as exc:
41
+ raise UserError(f"Invalid JSON config file: {config_path}") from exc
42
+
43
+ if not isinstance(raw_config, dict):
44
+ raise UserError("Architecture config must be a JSON object.")
45
+
46
+ project_path = _optional_string(raw_config, "project_path") or os.getcwd()
47
+ rules = raw_config.get("rules")
48
+ if not isinstance(rules, list):
49
+ raise UserError("Architecture config must define a 'rules' list.")
50
+
51
+ base_dir = str(path.parent if path.parent != Path("") else Path.cwd())
52
+ resolved_project_path = _resolve_project_path(base_dir, project_path)
53
+
54
+ return [_build_rule(resolved_project_path, item, index) for index, item in enumerate(rules, 1)]
55
+
56
+
57
+ def _build_rule(project_path: str, item: Any, index: int) -> ConfiguredRule:
58
+ if not isinstance(item, dict):
59
+ raise UserError(f"Rule #{index} must be a JSON object.")
60
+
61
+ rule_type = _required_string(item, "type", index)
62
+ name = _optional_string(item, "name") or f"{rule_type} rule #{index}"
63
+ rule: Checkable
64
+ if rule_type == "no_cycles":
65
+ subject = _optional_string(item, "subject")
66
+ builder = project_files(project_path)
67
+ if subject is not None:
68
+ rule = builder.in_path(subject).should().have_no_cycles()
69
+ else:
70
+ rule = builder.should().have_no_cycles()
71
+ elif rule_type == "forbidden_dependency":
72
+ source = _required_string(item, "source", index)
73
+ target = _required_string(item, "target", index)
74
+ rule = (
75
+ project_files(project_path)
76
+ .in_path(source)
77
+ .should_not()
78
+ .depend_on_files()
79
+ .in_path(target)
80
+ )
81
+ elif rule_type == "forbidden_external_dependency":
82
+ source = _required_string(item, "source", index)
83
+ module = _required_string(item, "module", index)
84
+ rule = (
85
+ project_files(project_path)
86
+ .in_path(source)
87
+ .should_not()
88
+ .depend_on_external_modules()
89
+ .matching(module)
90
+ )
91
+ else:
92
+ raise UserError(
93
+ f"Unsupported rule type '{rule_type}'. Supported types: "
94
+ "no_cycles, forbidden_dependency, forbidden_external_dependency."
95
+ )
96
+
97
+ return ConfiguredRule(name=name, rule=rule)
98
+
99
+
100
+ def _resolve_project_path(base_dir: str, project_path: str) -> str:
101
+ if os.path.isabs(project_path):
102
+ return project_path
103
+ return os.path.abspath(os.path.join(base_dir, project_path))
104
+
105
+
106
+ def _required_string(rule: dict[str, Any], key: str, index: int) -> str:
107
+ value = rule.get(key)
108
+ if not isinstance(value, str) or not value.strip():
109
+ raise UserError(f"Rule #{index} must define a non-empty string '{key}'.")
110
+ return value
111
+
112
+
113
+ def _optional_string(rule: dict[str, Any], key: str) -> str | None:
114
+ value = rule.get(key)
115
+ if value is None:
116
+ return None
117
+ if not isinstance(value, str) or not value.strip():
118
+ raise UserError(f"Config value '{key}' must be a non-empty string.")
119
+ return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: archunitpython
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -206,6 +206,42 @@ migrations/*.py
206
206
 
207
207
  Patterns support comments, blank lines, glob syntax, root-relative paths, path
208
208
  patterns, and directory patterns with a trailing `/`.
209
+
210
+ ### Loading Common Rules From Config
211
+
212
+ For straightforward shared rules, you can load a JSON config file and still run
213
+ the resulting rules in your normal test suite:
214
+
215
+ ```json
216
+ {
217
+ "project_path": "src",
218
+ "rules": [
219
+ {
220
+ "name": "controllers must not use services directly",
221
+ "type": "forbidden_dependency",
222
+ "source": "**/controllers/**",
223
+ "target": "**/services/**"
224
+ },
225
+ {
226
+ "name": "source files have no cycles",
227
+ "type": "no_cycles"
228
+ }
229
+ ]
230
+ }
231
+ ```
232
+
233
+ ```python
234
+ from archunitpython import assert_passes, rules_from_config
235
+
236
+ def test_configured_architecture_rules():
237
+ for rule in rules_from_config("archunitpython.json"):
238
+ assert_passes(rule)
239
+ ```
240
+
241
+ Supported rule types are `no_cycles`, `forbidden_dependency`, and
242
+ `forbidden_external_dependency`. The fluent Python API remains the primary and
243
+ most flexible interface.
244
+
209
245
  ### Explaining Rules With `.because(...)`
210
246
 
211
247
  Attach a rationale to a rule so failing assertions explain why the rule exists:
@@ -1,4 +1,4 @@
1
- archunitpython/__init__.py,sha256=8MQMKFXjGNHJdCtifcWJcAgXLkjqJhDiXYxqp2fmxlQ,1154
1
+ archunitpython/__init__.py,sha256=nXfB-DvtQN-2XR6XM87aM1E71BpqDbTBe-wJLLCgWEs,1282
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
@@ -30,6 +30,8 @@ archunitpython/common/projection/cycles/tarjan_scc.py,sha256=pIj1ub8XgcqVLj8skdZ
30
30
  archunitpython/common/util/__init__.py,sha256=g-W8kWiVqwdohxm74J3yUonPkb_yDUJrp73S5s4jpgI,117
31
31
  archunitpython/common/util/declaration_detector.py,sha256=XgrpgaUIQa9MNFlnhVQz2X_uW7zCO9nrMbGa3tVFyPM,3407
32
32
  archunitpython/common/util/logger.py,sha256=2did2mRAlkrElH9SYgv9xYiSzr4A-jXHg3cp6TodhoA,3326
33
+ archunitpython/config/__init__.py,sha256=jl8qQ040ej2h7KOxzNx4Xtct89YKCDZA2kcdJ-RiNK0,191
34
+ archunitpython/config/loader.py,sha256=sXsq8X3rijBT2C_t5EEn1GC-22ZVxxLYcFU4Fuinkcc,4301
33
35
  archunitpython/files/__init__.py,sha256=wgl8IOqTeQGpNISu-Q8b_j6rTn8aEgoz6t1S5j8z13g,108
34
36
  archunitpython/files/assertion/__init__.py,sha256=K2qiEFfmo6SCgxfOKNrPzYgT5ScKUaw2W2CrDa3jBTw,1068
35
37
  archunitpython/files/assertion/custom_file_logic.py,sha256=1v5D80QX_NnAKBwKwyhetY4rA8rrXyxN2BsHlvnLgq8,2956
@@ -77,7 +79,7 @@ archunitpython/testing/common/__init__.py,sha256=Wc4bC-N4t6giChKjj6wuTGKkb39thcY
77
79
  archunitpython/testing/common/color_utils.py,sha256=2I8Z1SZfWhhgudMgmXY6PPydGxGl35cK_To-GXnSyJg,1226
78
80
  archunitpython/testing/common/violation_factory.py,sha256=yWlvv2U7u-7KoTMfp1QCeJXxTLpPEagKqDHZmPSyrlk,4677
79
81
  archunitpython/testing/pytest_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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,,
82
+ archunitpython-1.5.0.dist-info/METADATA,sha256=ek9DQj2hd0a0h-tvws_nnelozR1PzhZNBLWouBBH0aM,31627
83
+ archunitpython-1.5.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
84
+ archunitpython-1.5.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
85
+ archunitpython-1.5.0.dist-info/RECORD,,