pytestarch 4.0.0__tar.gz → 5.0.0__tar.gz
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.
- pytestarch-5.0.0/PKG-INFO +176 -0
- pytestarch-5.0.0/README.md +145 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/pyproject.toml +14 -19
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/diagram_parser.py +1 -1
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/diagram_rule.py +1 -1
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/breadth_first_searches.py +14 -10
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/evaluable_architecture.py +8 -7
- pytestarch-5.0.0/src/pytestarch/eval_structure/exceptions.py +18 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/module_name_converter.py +4 -5
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/networkxgraph.py +30 -3
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/utils.py +5 -1
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/parser.py +1 -2
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/pytestarch.py +3 -3
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/base_language.py +3 -3
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/layered_architecture_rule.py +5 -5
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/rule.py +21 -16
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/error_message/message_generator.py +0 -8
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/rule_matcher.py +1 -1
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/rule_violation_detector.py +7 -1
- pytestarch-4.0.0/PKG-INFO +0 -77
- pytestarch-4.0.0/README.md +0 -47
- pytestarch-4.0.0/src/pytestarch/eval_structure/exceptions.py +0 -9
- {pytestarch-4.0.0 → pytestarch-5.0.0}/LICENSE +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/dependency_to_rule_converter.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/exceptions.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/parsed_dependencies.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/evaluable_graph.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/evaluable_structures.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/types.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/config.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/converter.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/exceptions.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/file_filter.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/import_filter.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/import_types.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/importee_module_calculator.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/graph_generation/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/graph_generation/graph_generator.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/py.typed +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/LANGUAGE_DEFINTION.md +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/exceptions.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/multiple_rule_applier.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/error_message/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/exceptions.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/behavior_requirement.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/layer_rule_violation_detector.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/module_requirement.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/rule_violations.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/utils/__init__.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/utils/decorators.py +0 -0
- {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/utils/partial_match_to_regex_converter.py +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyTestArch
|
|
3
|
+
Version: 5.0.0
|
|
4
|
+
Summary: Test framework for software architecture based on imports between modules
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: architecture,test
|
|
8
|
+
Author: zyskarch
|
|
9
|
+
Author-email: zyskarch@gmail.com
|
|
10
|
+
Maintainer: zyskarch
|
|
11
|
+
Maintainer-email: zyskarch@gmail.com
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Intended Audience :: Developers
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing :: Unit
|
|
23
|
+
Provides-Extra: visualization
|
|
24
|
+
Requires-Dist: matplotlib (>=3.11) ; extra == "visualization"
|
|
25
|
+
Requires-Dist: networkx (>=3.6)
|
|
26
|
+
Project-URL: Documentation, https://zyskarch.github.io/pytestarch/latest
|
|
27
|
+
Project-URL: Repository, https://github.com/zyskarch/pytestarch
|
|
28
|
+
Project-URL: issues, https://github.com/zyskarch/pytestarch/issues
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Welcome to PyTestArch
|
|
32
|
+
|
|
33
|
+
PyTestArch is an open source library that allows users to define architectural rules and test their code against them. It is
|
|
34
|
+
generally inspired by [ArchUnit](https://www.archunit.org/).
|
|
35
|
+
|
|
36
|
+
## Installation Guide
|
|
37
|
+
PyTestArch is available via [PyPI](https://pypi.org/project/pytestarch/) and can be installed e.g. via pip: `pip install pytestarch`. To also install the
|
|
38
|
+
optional dependency matplotlib, which is required to draw the created dependency graphs, install `pytestarch[visualization]`
|
|
39
|
+
|
|
40
|
+
## Usage Guide
|
|
41
|
+
Three steps are required to test an architectural rule:
|
|
42
|
+
|
|
43
|
+
1) Create an evaluable representation of the source code you want to test
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
from pytestarch import get_evaluable_architecture
|
|
47
|
+
|
|
48
|
+
evaluable = get_evaluable_architecture("/home/dummy/project", "/home/dummy/project/src")
|
|
49
|
+
```
|
|
50
|
+
This will scan all python files under /home/dummy/project/src for imports and build an internal representation that can
|
|
51
|
+
later be queried. The first parameter /home/dummy/project helps PyTestArch to differentiate between internal and external
|
|
52
|
+
dependencies. This evaluable can be used for multiple architectural rule checks; if you are using [pytest](https://docs.pytest.org/en/7.1.x/),
|
|
53
|
+
you could use a fixture for this evaluable object.
|
|
54
|
+
|
|
55
|
+
2) Define an architectural rule
|
|
56
|
+
```
|
|
57
|
+
from pytestarch import Rule
|
|
58
|
+
|
|
59
|
+
rule = (
|
|
60
|
+
Rule()
|
|
61
|
+
.modules_that()
|
|
62
|
+
.are_named("project.src.moduleB")
|
|
63
|
+
.should_not()
|
|
64
|
+
.be_imported_by_modules_that()
|
|
65
|
+
.are_sub_modules_of("project.src.moduleA")
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This rule represents the architectural requirements that a module named "project.src.moduleB" should not be imported by any module
|
|
70
|
+
that is a submodule of "project.src.moduleA", excluding "project.src.moduleA" itself.
|
|
71
|
+
|
|
72
|
+
3) Evaluate your code against this rule
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
rule.assert_applies(evaluable)
|
|
76
|
+
```
|
|
77
|
+
That's it!
|
|
78
|
+
|
|
79
|
+
## Real-World Example
|
|
80
|
+
|
|
81
|
+
The snippet below is taken directly from pytestarch's own architecture tests
|
|
82
|
+
([`tests/test_architecture.py`](https://github.com/zyskarch/pytestarch/blob/main/tests/test_architecture.py)).
|
|
83
|
+
It guards the library's own internal module boundaries
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from __future__ import annotations
|
|
87
|
+
|
|
88
|
+
from pathlib import Path
|
|
89
|
+
|
|
90
|
+
import pytest
|
|
91
|
+
|
|
92
|
+
from pytestarch import EvaluableArchitecture, Rule, get_evaluable_architecture
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.fixture(scope="session")
|
|
96
|
+
def pytestarch_architecture() -> EvaluableArchitecture:
|
|
97
|
+
cwd = Path.cwd()
|
|
98
|
+
while cwd.stem != "pytestarch":
|
|
99
|
+
cwd = cwd.parent
|
|
100
|
+
src_folder = str(cwd / "src/pytestarch")
|
|
101
|
+
|
|
102
|
+
return get_evaluable_architecture(
|
|
103
|
+
src_folder,
|
|
104
|
+
src_folder,
|
|
105
|
+
("*__pycache__", "*__init__.py"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
BASE_MODULE = "pytestarch"
|
|
110
|
+
DIAGRAM = f"{BASE_MODULE}.diagram_extension"
|
|
111
|
+
STRUCTURE = f"{BASE_MODULE}.eval_structure"
|
|
112
|
+
GENERATION = f"{BASE_MODULE}.eval_structure_generation"
|
|
113
|
+
LANGUAGE = f"{BASE_MODULE}.query_language"
|
|
114
|
+
ASSESSMENT = f"{BASE_MODULE}.rule_assessment"
|
|
115
|
+
UTILS = f"{BASE_MODULE}.utils"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
top_level_rules = [
|
|
119
|
+
pytest.param(
|
|
120
|
+
Rule()
|
|
121
|
+
.modules_that()
|
|
122
|
+
.are_named(DIAGRAM)
|
|
123
|
+
.should_only()
|
|
124
|
+
.import_modules_that()
|
|
125
|
+
.are_named([LANGUAGE, STRUCTURE]),
|
|
126
|
+
id="diagram_extension",
|
|
127
|
+
),
|
|
128
|
+
pytest.param(
|
|
129
|
+
Rule().modules_that().are_named(STRUCTURE).should_not().import_anything(),
|
|
130
|
+
id="eval_structure",
|
|
131
|
+
),
|
|
132
|
+
pytest.param(
|
|
133
|
+
Rule()
|
|
134
|
+
.modules_that()
|
|
135
|
+
.are_named(GENERATION)
|
|
136
|
+
.should_only()
|
|
137
|
+
.import_modules_that()
|
|
138
|
+
.are_named(STRUCTURE),
|
|
139
|
+
id="eval_structure_generation",
|
|
140
|
+
),
|
|
141
|
+
pytest.param(
|
|
142
|
+
Rule()
|
|
143
|
+
.modules_that()
|
|
144
|
+
.are_named(LANGUAGE)
|
|
145
|
+
.should_only()
|
|
146
|
+
.import_modules_that()
|
|
147
|
+
.are_named([STRUCTURE, ASSESSMENT, UTILS]),
|
|
148
|
+
id="query_language",
|
|
149
|
+
),
|
|
150
|
+
pytest.param(
|
|
151
|
+
Rule()
|
|
152
|
+
.modules_that()
|
|
153
|
+
.are_named(ASSESSMENT)
|
|
154
|
+
.should_only()
|
|
155
|
+
.import_modules_that()
|
|
156
|
+
.are_named(STRUCTURE),
|
|
157
|
+
id="rule_assessment",
|
|
158
|
+
),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize("rule", top_level_rules)
|
|
163
|
+
def test_top_level_module_structure(
|
|
164
|
+
rule: Rule, pytestarch_architecture: EvaluableArchitecture
|
|
165
|
+
) -> None:
|
|
166
|
+
rule.assert_applies(pytestarch_architecture)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Further Resources
|
|
170
|
+
|
|
171
|
+
- [Full documentation](https://zyskarch.github.io/pytestarch/latest)
|
|
172
|
+
- More test examples on GitHub:
|
|
173
|
+
- [`tests/test_architecture.py`](https://github.com/zyskarch/pytestarch/blob/main/tests/test_architecture.py) — module boundary rules (shown above)
|
|
174
|
+
- [`tests/diagram_extension/`](https://github.com/zyskarch/pytestarch/tree/main/tests/diagram_extension) — generating rules from PlantUML diagrams
|
|
175
|
+
- [`tests/integration/`](https://github.com/zyskarch/pytestarch/tree/main/tests/integration) — extensive parametrized test cases
|
|
176
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Welcome to PyTestArch
|
|
2
|
+
|
|
3
|
+
PyTestArch is an open source library that allows users to define architectural rules and test their code against them. It is
|
|
4
|
+
generally inspired by [ArchUnit](https://www.archunit.org/).
|
|
5
|
+
|
|
6
|
+
## Installation Guide
|
|
7
|
+
PyTestArch is available via [PyPI](https://pypi.org/project/pytestarch/) and can be installed e.g. via pip: `pip install pytestarch`. To also install the
|
|
8
|
+
optional dependency matplotlib, which is required to draw the created dependency graphs, install `pytestarch[visualization]`
|
|
9
|
+
|
|
10
|
+
## Usage Guide
|
|
11
|
+
Three steps are required to test an architectural rule:
|
|
12
|
+
|
|
13
|
+
1) Create an evaluable representation of the source code you want to test
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
from pytestarch import get_evaluable_architecture
|
|
17
|
+
|
|
18
|
+
evaluable = get_evaluable_architecture("/home/dummy/project", "/home/dummy/project/src")
|
|
19
|
+
```
|
|
20
|
+
This will scan all python files under /home/dummy/project/src for imports and build an internal representation that can
|
|
21
|
+
later be queried. The first parameter /home/dummy/project helps PyTestArch to differentiate between internal and external
|
|
22
|
+
dependencies. This evaluable can be used for multiple architectural rule checks; if you are using [pytest](https://docs.pytest.org/en/7.1.x/),
|
|
23
|
+
you could use a fixture for this evaluable object.
|
|
24
|
+
|
|
25
|
+
2) Define an architectural rule
|
|
26
|
+
```
|
|
27
|
+
from pytestarch import Rule
|
|
28
|
+
|
|
29
|
+
rule = (
|
|
30
|
+
Rule()
|
|
31
|
+
.modules_that()
|
|
32
|
+
.are_named("project.src.moduleB")
|
|
33
|
+
.should_not()
|
|
34
|
+
.be_imported_by_modules_that()
|
|
35
|
+
.are_sub_modules_of("project.src.moduleA")
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This rule represents the architectural requirements that a module named "project.src.moduleB" should not be imported by any module
|
|
40
|
+
that is a submodule of "project.src.moduleA", excluding "project.src.moduleA" itself.
|
|
41
|
+
|
|
42
|
+
3) Evaluate your code against this rule
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
rule.assert_applies(evaluable)
|
|
46
|
+
```
|
|
47
|
+
That's it!
|
|
48
|
+
|
|
49
|
+
## Real-World Example
|
|
50
|
+
|
|
51
|
+
The snippet below is taken directly from pytestarch's own architecture tests
|
|
52
|
+
([`tests/test_architecture.py`](https://github.com/zyskarch/pytestarch/blob/main/tests/test_architecture.py)).
|
|
53
|
+
It guards the library's own internal module boundaries
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
|
|
60
|
+
import pytest
|
|
61
|
+
|
|
62
|
+
from pytestarch import EvaluableArchitecture, Rule, get_evaluable_architecture
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture(scope="session")
|
|
66
|
+
def pytestarch_architecture() -> EvaluableArchitecture:
|
|
67
|
+
cwd = Path.cwd()
|
|
68
|
+
while cwd.stem != "pytestarch":
|
|
69
|
+
cwd = cwd.parent
|
|
70
|
+
src_folder = str(cwd / "src/pytestarch")
|
|
71
|
+
|
|
72
|
+
return get_evaluable_architecture(
|
|
73
|
+
src_folder,
|
|
74
|
+
src_folder,
|
|
75
|
+
("*__pycache__", "*__init__.py"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
BASE_MODULE = "pytestarch"
|
|
80
|
+
DIAGRAM = f"{BASE_MODULE}.diagram_extension"
|
|
81
|
+
STRUCTURE = f"{BASE_MODULE}.eval_structure"
|
|
82
|
+
GENERATION = f"{BASE_MODULE}.eval_structure_generation"
|
|
83
|
+
LANGUAGE = f"{BASE_MODULE}.query_language"
|
|
84
|
+
ASSESSMENT = f"{BASE_MODULE}.rule_assessment"
|
|
85
|
+
UTILS = f"{BASE_MODULE}.utils"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
top_level_rules = [
|
|
89
|
+
pytest.param(
|
|
90
|
+
Rule()
|
|
91
|
+
.modules_that()
|
|
92
|
+
.are_named(DIAGRAM)
|
|
93
|
+
.should_only()
|
|
94
|
+
.import_modules_that()
|
|
95
|
+
.are_named([LANGUAGE, STRUCTURE]),
|
|
96
|
+
id="diagram_extension",
|
|
97
|
+
),
|
|
98
|
+
pytest.param(
|
|
99
|
+
Rule().modules_that().are_named(STRUCTURE).should_not().import_anything(),
|
|
100
|
+
id="eval_structure",
|
|
101
|
+
),
|
|
102
|
+
pytest.param(
|
|
103
|
+
Rule()
|
|
104
|
+
.modules_that()
|
|
105
|
+
.are_named(GENERATION)
|
|
106
|
+
.should_only()
|
|
107
|
+
.import_modules_that()
|
|
108
|
+
.are_named(STRUCTURE),
|
|
109
|
+
id="eval_structure_generation",
|
|
110
|
+
),
|
|
111
|
+
pytest.param(
|
|
112
|
+
Rule()
|
|
113
|
+
.modules_that()
|
|
114
|
+
.are_named(LANGUAGE)
|
|
115
|
+
.should_only()
|
|
116
|
+
.import_modules_that()
|
|
117
|
+
.are_named([STRUCTURE, ASSESSMENT, UTILS]),
|
|
118
|
+
id="query_language",
|
|
119
|
+
),
|
|
120
|
+
pytest.param(
|
|
121
|
+
Rule()
|
|
122
|
+
.modules_that()
|
|
123
|
+
.are_named(ASSESSMENT)
|
|
124
|
+
.should_only()
|
|
125
|
+
.import_modules_that()
|
|
126
|
+
.are_named(STRUCTURE),
|
|
127
|
+
id="rule_assessment",
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.parametrize("rule", top_level_rules)
|
|
133
|
+
def test_top_level_module_structure(
|
|
134
|
+
rule: Rule, pytestarch_architecture: EvaluableArchitecture
|
|
135
|
+
) -> None:
|
|
136
|
+
rule.assert_applies(pytestarch_architecture)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Further Resources
|
|
140
|
+
|
|
141
|
+
- [Full documentation](https://zyskarch.github.io/pytestarch/latest)
|
|
142
|
+
- More test examples on GitHub:
|
|
143
|
+
- [`tests/test_architecture.py`](https://github.com/zyskarch/pytestarch/blob/main/tests/test_architecture.py) — module boundary rules (shown above)
|
|
144
|
+
- [`tests/diagram_extension/`](https://github.com/zyskarch/pytestarch/tree/main/tests/diagram_extension) — generating rules from PlantUML diagrams
|
|
145
|
+
- [`tests/integration/`](https://github.com/zyskarch/pytestarch/tree/main/tests/integration) — extensive parametrized test cases
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "PyTestArch"
|
|
3
|
-
version = "
|
|
4
|
-
requires-python = ">=3.
|
|
3
|
+
version = "5.0.0"
|
|
4
|
+
requires-python = ">=3.11"
|
|
5
5
|
description = "Test framework for software architecture based on imports between modules"
|
|
6
6
|
authors = [{ name = "zyskarch", email = "zyskarch@gmail.com" }]
|
|
7
7
|
maintainers = [{ name = "zyskarch", email = "zyskarch@gmail.com" }]
|
|
@@ -10,11 +10,11 @@ license = "Apache-2.0"
|
|
|
10
10
|
|
|
11
11
|
keywords = ["architecture", "test"]
|
|
12
12
|
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3.15",
|
|
14
|
+
"Programming Language :: Python :: 3.14",
|
|
13
15
|
"Programming Language :: Python :: 3.13",
|
|
14
16
|
"Programming Language :: Python :: 3.12",
|
|
15
17
|
"Programming Language :: Python :: 3.11",
|
|
16
|
-
"Programming Language :: Python :: 3.10",
|
|
17
|
-
"Programming Language :: Python :: 3.9",
|
|
18
18
|
"License :: OSI Approved :: Apache Software License",
|
|
19
19
|
"Operating System :: OS Independent",
|
|
20
20
|
"Intended Audience :: Developers",
|
|
@@ -22,10 +22,10 @@ classifiers = [
|
|
|
22
22
|
"Topic :: Software Development :: Testing :: Unit",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
|
-
dependencies = ["networkx>=3.
|
|
25
|
+
dependencies = ["networkx>=3.6"]
|
|
26
26
|
|
|
27
27
|
[project.optional-dependencies]
|
|
28
|
-
visualization = ["matplotlib>=3.
|
|
28
|
+
visualization = ["matplotlib>=3.11"]
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
31
31
|
issues = "https://github.com/zyskarch/pytestarch/issues"
|
|
@@ -33,19 +33,14 @@ documentation = "https://zyskarch.github.io/pytestarch/latest"
|
|
|
33
33
|
repository = "https://github.com/zyskarch/pytestarch"
|
|
34
34
|
|
|
35
35
|
[tool.poetry.group.dev.dependencies]
|
|
36
|
-
|
|
37
|
-
mkdocstrings-python = "
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
pre-commit = "3.7.1"
|
|
45
|
-
pytest = "^8.2.1"
|
|
46
|
-
coverage = "^7.5.2"
|
|
47
|
-
mike = "2.1.1"
|
|
48
|
-
ruff = "^0.7.0"
|
|
36
|
+
zensical = "0.0.63"
|
|
37
|
+
mkdocstrings-python = "2.0.8"
|
|
38
|
+
mike = { git = "https://github.com/squidfunk/mike.git" }
|
|
39
|
+
nox = "2026.8.17"
|
|
40
|
+
pre-commit = "4.6.2"
|
|
41
|
+
pytest = "^9.1.1"
|
|
42
|
+
coverage = "^7.16.1"
|
|
43
|
+
ruff = "^0.16.8"
|
|
49
44
|
|
|
50
45
|
[build-system]
|
|
51
46
|
requires = ["poetry-core>=2.0"]
|
|
@@ -57,7 +57,7 @@ class PumlParser(DiagramParser):
|
|
|
57
57
|
* component names must be enclosed in square brackets
|
|
58
58
|
* exception: if a component as been given an alias via `[module name] as alias`, then the alias should not be
|
|
59
59
|
wrapped in square brackets
|
|
60
|
-
* dependencies must be with either
|
|
60
|
+
* dependencies must be with either `-->`, `->`, `<--`, `<-`, `-text->`, or `<-text-`. The dependee is to be placed
|
|
61
61
|
on the side of the arrow head, the dependor on the opposite side
|
|
62
62
|
Args:
|
|
63
63
|
file_path: .puml file to parse
|
|
@@ -56,7 +56,7 @@ class DiagramRule(FileRule, BaseModuleSpecifier, RuleApplier):
|
|
|
56
56
|
|
|
57
57
|
Args:
|
|
58
58
|
should_only_rule: if True, edges between components will be converted into 'should only import' rules.
|
|
59
|
-
|
|
59
|
+
If False, 'should import' rules will be generated instead.
|
|
60
60
|
"""
|
|
61
61
|
self._file_path: Path | None = None
|
|
62
62
|
self._name_relative_to_root: str | None = None
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/breadth_first_searches.py
RENAMED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pytestarch.eval_structure.evaluable_architecture import Dependency, ModuleFilter
|
|
4
4
|
from pytestarch.eval_structure.evaluable_structures import AbstractGraph, AbstractNode
|
|
5
|
-
from pytestarch.eval_structure.utils import get_node, get_parent_nodes,
|
|
5
|
+
from pytestarch.eval_structure.utils import get_node, get_parent_nodes, to_module
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def get_dependency_between_modules(
|
|
@@ -16,7 +16,7 @@ def get_dependency_between_modules(
|
|
|
16
16
|
nodes_to_check = [dependent_node]
|
|
17
17
|
checked_nodes = set()
|
|
18
18
|
|
|
19
|
-
dependencies = []
|
|
19
|
+
dependencies: list[Dependency] = []
|
|
20
20
|
|
|
21
21
|
while nodes_to_check:
|
|
22
22
|
node = nodes_to_check.pop()
|
|
@@ -37,8 +37,8 @@ def get_dependency_between_modules(
|
|
|
37
37
|
and node not in nodes_to_exclude
|
|
38
38
|
and child not in nodes_to_exclude
|
|
39
39
|
):
|
|
40
|
-
dependencies.append(
|
|
41
|
-
return dependencies
|
|
40
|
+
dependencies.append((to_module(node), to_module(child)))
|
|
41
|
+
return dependencies
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def any_dependency_to_module_other_than(
|
|
@@ -51,7 +51,7 @@ def any_dependency_to_module_other_than(
|
|
|
51
51
|
continue
|
|
52
52
|
nodes_to_exclude.update(get_all_submodules_of(graph, dependent_upon))
|
|
53
53
|
|
|
54
|
-
nodes_fulfilling_criteria = []
|
|
54
|
+
nodes_fulfilling_criteria: list[Dependency] = []
|
|
55
55
|
|
|
56
56
|
# nodes that count as not fulfilling the criterion are nodes that would technically fulfill the criterion,
|
|
57
57
|
# but have to be excluded for consistency reasons. For example, if the dependent is called A and has two
|
|
@@ -102,11 +102,13 @@ def any_dependency_to_module_other_than(
|
|
|
102
102
|
child not in nodes_to_exclude
|
|
103
103
|
and child not in nodes_that_do_not_fulfill_criterion
|
|
104
104
|
):
|
|
105
|
-
nodes_fulfilling_criteria.append(
|
|
105
|
+
nodes_fulfilling_criteria.append(
|
|
106
|
+
(to_module(node), to_module(child))
|
|
107
|
+
)
|
|
106
108
|
else:
|
|
107
109
|
nodes_to_check.append(child)
|
|
108
110
|
|
|
109
|
-
return nodes_fulfilling_criteria
|
|
111
|
+
return nodes_fulfilling_criteria
|
|
110
112
|
|
|
111
113
|
|
|
112
114
|
def any_other_dependency_to_module_than(
|
|
@@ -122,7 +124,7 @@ def any_other_dependency_to_module_than(
|
|
|
122
124
|
continue
|
|
123
125
|
nodes_to_exclude.update(get_all_submodules_of(graph, dependent))
|
|
124
126
|
|
|
125
|
-
nodes_fulfilling_criteria = []
|
|
127
|
+
nodes_fulfilling_criteria: list[Dependency] = []
|
|
126
128
|
|
|
127
129
|
# nodes that count as not fulfilling the criterion are nodes that would technically fulfill the criterion,
|
|
128
130
|
# but have to be excluded for consistency reasons. For example, if the dependent upon is called A and has two
|
|
@@ -171,12 +173,14 @@ def any_other_dependency_to_module_than(
|
|
|
171
173
|
parent not in nodes_to_exclude
|
|
172
174
|
and parent not in nodes_that_count_as_not_fulfilling_criterion
|
|
173
175
|
):
|
|
174
|
-
nodes_fulfilling_criteria.append(
|
|
176
|
+
nodes_fulfilling_criteria.append(
|
|
177
|
+
(to_module(parent), to_module(node))
|
|
178
|
+
)
|
|
175
179
|
|
|
176
180
|
if parent not in nodes_to_exclude:
|
|
177
181
|
nodes_to_check.append(parent)
|
|
178
182
|
|
|
179
|
-
return nodes_fulfilling_criteria
|
|
183
|
+
return nodes_fulfilling_criteria
|
|
180
184
|
|
|
181
185
|
|
|
182
186
|
def get_all_submodules_of(
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/evaluable_architecture.py
RENAMED
|
@@ -236,8 +236,8 @@ class EvaluableArchitecture(Protocol):
|
|
|
236
236
|
matches.
|
|
237
237
|
|
|
238
238
|
Args:
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
dependents: Module(s)
|
|
240
|
+
dependent_upons: Module(s)
|
|
241
241
|
|
|
242
242
|
Returns:
|
|
243
243
|
Importer and importee per pair of dependent and dependent_upon module if there are any
|
|
@@ -260,8 +260,8 @@ class EvaluableArchitecture(Protocol):
|
|
|
260
260
|
Y's parent is such a module, as this parent module can and usually will also contain other modules than Y.
|
|
261
261
|
|
|
262
262
|
Args:
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
dependents: Module
|
|
264
|
+
dependent_upons: Module
|
|
265
265
|
|
|
266
266
|
Returns:
|
|
267
267
|
All modules other than dependent_upon on which dependent module as any dependency per dependent module
|
|
@@ -282,8 +282,8 @@ class EvaluableArchitecture(Protocol):
|
|
|
282
282
|
If the dependent upon module is defined via a parent module, this parent module is not taken into account.
|
|
283
283
|
|
|
284
284
|
Args:
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
dependents: Module
|
|
286
|
+
dependent_upons: Module
|
|
287
287
|
|
|
288
288
|
Returns:
|
|
289
289
|
All modules other than dependent that have any dependency on the dependent upon module per dependent_upon module
|
|
@@ -296,7 +296,8 @@ class EvaluableArchitecture(Protocol):
|
|
|
296
296
|
Args:
|
|
297
297
|
**kwargs: Any formatting options available for networkx' drawing function, as this is currently the only
|
|
298
298
|
available backend.
|
|
299
|
-
Exception: If 'spacing' is set, this will be interpreted as the parameter 'k' of the spring layout
|
|
299
|
+
Exception: If 'spacing' is set, this will be interpreted as the parameter 'k' of the spring layout
|
|
300
|
+
([networkx spring layout](https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.spring_layout.html#networkx.drawing.layout.spring_layout)).
|
|
300
301
|
"""
|
|
301
302
|
raise NotImplementedError()
|
|
302
303
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ImpossibleMatch(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LayerMismatch(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModuleUnknown(Exception):
|
|
13
|
+
"""Raised when a queried module is not present in the dependency graph.
|
|
14
|
+
|
|
15
|
+
The exception message includes actionable guidance on common causes,
|
|
16
|
+
such as the module being an external library that was excluded from the
|
|
17
|
+
graph or the module name being misspelled.
|
|
18
|
+
"""
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/module_name_converter.py
RENAMED
|
@@ -27,8 +27,8 @@ class ModuleNameConverter:
|
|
|
27
27
|
"""Converts each regex pattern that serves to identify module names into actual modules that match this pattern.
|
|
28
28
|
|
|
29
29
|
Args:
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
modules: list of modules, some of which may need converting since their names are regex patterns
|
|
31
|
+
arch: architecture that contains actual modules
|
|
32
32
|
|
|
33
33
|
Returns:
|
|
34
34
|
- list of module filters, now without any regex patterns. Will be used for later dependency graph queries.
|
|
@@ -60,8 +60,7 @@ class ModuleNameConverter:
|
|
|
60
60
|
converted_module_filters.add(converted_module_filter)
|
|
61
61
|
conversion_mapping[module_to_match].append(converted_module)
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
never_matched.remove(module_to_match)
|
|
63
|
+
never_matched.discard(module_to_match)
|
|
65
64
|
|
|
66
65
|
submodules_of_match = get_all_submodules_of(
|
|
67
66
|
arch._graph, # type: ignore
|
|
@@ -74,7 +73,7 @@ class ModuleNameConverter:
|
|
|
74
73
|
|
|
75
74
|
if never_matched:
|
|
76
75
|
raise ImpossibleMatch(
|
|
77
|
-
f
|
|
76
|
+
f"No modules found that match: {', '.join(never_matched)}"
|
|
78
77
|
)
|
|
79
78
|
|
|
80
79
|
all_converted_modules = list(converted_module_filters) + other_modules # type: ignore
|
|
@@ -10,6 +10,7 @@ import networkx as nx
|
|
|
10
10
|
from networkx import draw_networkx, has_path, spring_layout
|
|
11
11
|
|
|
12
12
|
from pytestarch.eval_structure.evaluable_structures import AbstractGraph, AbstractNode
|
|
13
|
+
from pytestarch.eval_structure.exceptions import ModuleUnknown
|
|
13
14
|
from pytestarch.eval_structure.types import Import, get_parent_modules
|
|
14
15
|
|
|
15
16
|
EXPECTED_EDGE_AND_NODE_TYPES = "Only str and tuple of two str supported."
|
|
@@ -140,7 +141,7 @@ class NetworkxGraph(AbstractGraph):
|
|
|
140
141
|
self._graph.add_edge(node_start, node_end, inherits=inherits)
|
|
141
142
|
|
|
142
143
|
def __contains__(self, item) -> bool:
|
|
143
|
-
if not isinstance(item,
|
|
144
|
+
if not isinstance(item, str | tuple):
|
|
144
145
|
raise TypeError(EXPECTED_EDGE_AND_NODE_TYPES)
|
|
145
146
|
|
|
146
147
|
if isinstance(item, str):
|
|
@@ -179,7 +180,11 @@ class NetworkxGraph(AbstractGraph):
|
|
|
179
180
|
|
|
180
181
|
Returns:
|
|
181
182
|
all predecessor nodes
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ModuleUnknown: if node is not present in the dependency graph
|
|
182
186
|
"""
|
|
187
|
+
self._assert_node_exists(node)
|
|
183
188
|
return sorted(self._graph.predecessors(node))
|
|
184
189
|
|
|
185
190
|
def direct_successor_nodes(self, node: Node) -> list[Node]:
|
|
@@ -190,7 +195,11 @@ class NetworkxGraph(AbstractGraph):
|
|
|
190
195
|
|
|
191
196
|
Returns:
|
|
192
197
|
all successor nodes
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
ModuleUnknown: if node is not present in the dependency graph
|
|
193
201
|
"""
|
|
202
|
+
self._assert_node_exists(node)
|
|
194
203
|
return sorted(self._graph.successors(node))
|
|
195
204
|
|
|
196
205
|
def parent_child_relationship(
|
|
@@ -224,8 +233,8 @@ class NetworkxGraph(AbstractGraph):
|
|
|
224
233
|
priority, e.g. with the same modules as above and
|
|
225
234
|
aliases == {'a': 'A', 'a.c': 'C'} the plot labels will be A, A.b, C,
|
|
226
235
|
C.d.
|
|
227
|
-
Remaining keyword args are passed to draw_networkx. Notably, "ax"
|
|
228
|
-
|
|
236
|
+
**kwargs: Remaining keyword args are passed to draw_networkx. Notably, "ax"
|
|
237
|
+
can be used to draw the graph onto an existing matploblib Axis object.
|
|
229
238
|
"""
|
|
230
239
|
try:
|
|
231
240
|
import matplotlib # noqa: F401
|
|
@@ -295,6 +304,24 @@ class NetworkxGraph(AbstractGraph):
|
|
|
295
304
|
except StopIteration: # no alias for module or parent module
|
|
296
305
|
return module_name
|
|
297
306
|
|
|
307
|
+
def _assert_node_exists(self, node: Node) -> None:
|
|
308
|
+
"""Raises ModuleUnknown with a diagnostic message if node is absent from the graph.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
node: the node to check for existence
|
|
312
|
+
"""
|
|
313
|
+
if not self._graph.has_node(node):
|
|
314
|
+
raise ModuleUnknown(
|
|
315
|
+
f"'{node}' was not found in the dependency graph.\n"
|
|
316
|
+
"If it is an external library, ensure "
|
|
317
|
+
"'exclude_external_libraries=False' is passed to "
|
|
318
|
+
"'get_evaluable_architecture()'.\n"
|
|
319
|
+
"If this flag is already set, the module may not be "
|
|
320
|
+
"imported by any code in the scanned source path.\n"
|
|
321
|
+
"Verify the module name is correct as it would appear "
|
|
322
|
+
"in a Python import statement."
|
|
323
|
+
)
|
|
324
|
+
|
|
298
325
|
def _flatten_graph_node(self, node: Node) -> Node:
|
|
299
326
|
"""Limits the depth of the graph by aggregating sub modules above a certain limit.
|
|
300
327
|
Args:
|
|
@@ -10,8 +10,12 @@ from pytestarch.eval_structure.evaluable_architecture import (
|
|
|
10
10
|
from pytestarch.eval_structure.evaluable_structures import AbstractNode
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def to_module(node: AbstractNode) -> Module:
|
|
14
|
+
return Module(identifier=node)
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
def to_modules(nodes: list[AbstractNode]) -> list[Module]:
|
|
14
|
-
return list(map(
|
|
18
|
+
return list(map(to_module, nodes))
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
def filter_to_module(filter: ModuleFilter) -> Module:
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/parser.py
RENAMED
|
@@ -51,8 +51,7 @@ class Parser:
|
|
|
51
51
|
"""Converts a given python file to an ast module and its name."""
|
|
52
52
|
absolute_path = path.resolve()
|
|
53
53
|
if self._file_should_be_parsed(absolute_path):
|
|
54
|
-
|
|
55
|
-
code = file.read()
|
|
54
|
+
code = absolute_path.read_bytes()
|
|
56
55
|
|
|
57
56
|
module_name = self._get_module_name(path)
|
|
58
57
|
self._all_modules.append(module_name)
|
|
@@ -22,8 +22,8 @@ DEFAULT_EXCLUSIONS = ("*__pycache__*",)
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def get_evaluable_architecture(
|
|
25
|
-
root_path: str,
|
|
26
|
-
module_path: str,
|
|
25
|
+
root_path: str | Path,
|
|
26
|
+
module_path: str | Path,
|
|
27
27
|
exclusions: tuple[str, ...] = DEFAULT_EXCLUSIONS,
|
|
28
28
|
exclude_external_libraries: bool = True,
|
|
29
29
|
level_limit: int | None = None,
|
|
@@ -44,7 +44,7 @@ def get_evaluable_architecture(
|
|
|
44
44
|
modules one level below the module path to be added as nodes. Note that this only applies to the final graph;
|
|
45
45
|
all modules will be parsed, the graph will simply be flattened: if a submodule of X imports Y, this import is
|
|
46
46
|
then assigned to X instead, if Y is above the level limit.
|
|
47
|
-
regex_exclusions: Proper regex version of 'exclusions'. Can only be specified if
|
|
47
|
+
regex_exclusions: Proper regex version of 'exclusions'. Can only be specified if exclusions is not specified.
|
|
48
48
|
regex_external_exclusions: Proper regex version of 'external_exclusions' to exclude certain external dependencies from being integrated into the evaluable. Can only be specified if exclude_external_libraries is False and external_exclusions is not specified. If a parent module (e.g. 'logging') is excluded, so will be child modules (e.g. 'logging.handlers').
|
|
49
49
|
"""
|
|
50
50
|
if regex_exclusions and exclusions:
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Generic, TypeVar
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
6
|
|
|
7
7
|
from pytestarch.eval_structure.evaluable_architecture import EvaluableArchitecture
|
|
8
8
|
from pytestarch.utils.decorators import deprecated
|
|
@@ -247,7 +247,7 @@ LayerSpecificationSuccessor = TypeVar(
|
|
|
247
247
|
"LayerSpecificationSuccessor", LayerBehaviorSpecification, RuleApplier
|
|
248
248
|
)
|
|
249
249
|
|
|
250
|
-
InputTypes = TypeVar("InputTypes", str,
|
|
250
|
+
InputTypes = TypeVar("InputTypes", str, str | list[str])
|
|
251
251
|
|
|
252
252
|
|
|
253
253
|
class LayerSpecification(Generic[LayerSpecificationSuccessor, InputTypes], ABC):
|
|
@@ -258,7 +258,7 @@ class LayerSpecification(Generic[LayerSpecificationSuccessor, InputTypes], ABC):
|
|
|
258
258
|
pass
|
|
259
259
|
|
|
260
260
|
|
|
261
|
-
class LayerRuleObject(LayerSpecification[RuleApplier,
|
|
261
|
+
class LayerRuleObject(LayerSpecification[RuleApplier, str | list[str]], ABC):
|
|
262
262
|
pass
|
|
263
263
|
|
|
264
264
|
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/layered_architecture_rule.py
RENAMED
|
@@ -54,7 +54,7 @@ class LayeredArchitecture(BaseLayeredArchitecture, LayerName, LayerDefinition):
|
|
|
54
54
|
|
|
55
55
|
if underspecified_layers:
|
|
56
56
|
raise ImproperlyConfigured(
|
|
57
|
-
f
|
|
57
|
+
f"Specify the modules of layer(s) {', '.join(underspecified_layers)} first."
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
if name in self._modules_by_layer_name:
|
|
@@ -94,7 +94,7 @@ class LayeredArchitecture(BaseLayeredArchitecture, LayerName, LayerDefinition):
|
|
|
94
94
|
|
|
95
95
|
if duplicates:
|
|
96
96
|
raise ImproperlyConfigured(
|
|
97
|
-
f
|
|
97
|
+
f"Module(s) {', '.join(duplicates)} already assigned to a layer."
|
|
98
98
|
)
|
|
99
99
|
|
|
100
100
|
self._modules_by_layer_name[layers_without_modules[0]] = (
|
|
@@ -124,7 +124,7 @@ class LayeredArchitecture(BaseLayeredArchitecture, LayerName, LayerDefinition):
|
|
|
124
124
|
f"Layer {layer}: [{', '.join(map(lambda m: m.identifier, modules))}]"
|
|
125
125
|
for layer, modules in self._modules_by_layer_name.items()
|
|
126
126
|
]
|
|
127
|
-
return f
|
|
127
|
+
return f"Layered Architecture: {'; '.join(layers)}"
|
|
128
128
|
|
|
129
129
|
def __getitem__(self, layer: str) -> Sequence[ModuleFilter]:
|
|
130
130
|
return self._modules_by_layer_name[layer]
|
|
@@ -146,8 +146,8 @@ class LayerRule(
|
|
|
146
146
|
LayerSpecification,
|
|
147
147
|
LayerBase,
|
|
148
148
|
):
|
|
149
|
-
"""Represents an architectural rule of the form
|
|
150
|
-
Layer X [verb, such as 'should'] [access type, such as 'be accessed by'] Layer Y
|
|
149
|
+
r"""Represents an architectural rule of the form
|
|
150
|
+
Layer X \[verb, such as 'should'\] \[access type, such as 'be accessed by'\] Layer Y
|
|
151
151
|
The modules that each layer contains are to be specified via a LayeredArchitecture object.
|
|
152
152
|
"""
|
|
153
153
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Sequence
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
4
|
from dataclasses import dataclass, replace
|
|
5
|
-
from typing import Callable
|
|
6
5
|
|
|
7
6
|
from pytestarch.eval_structure.evaluable_architecture import (
|
|
8
7
|
EvaluableArchitecture,
|
|
@@ -54,8 +53,8 @@ class Rule(
|
|
|
54
53
|
RuleSubject,
|
|
55
54
|
RuleApplier,
|
|
56
55
|
):
|
|
57
|
-
"""Represents an architectural rule of the form
|
|
58
|
-
Module1 [verb, such as 'should'] [import type, such as 'import'] Module2
|
|
56
|
+
r"""Represents an architectural rule of the form
|
|
57
|
+
Module1 \[verb, such as 'should'\] \[import type, such as 'import'\] Module2
|
|
59
58
|
"""
|
|
60
59
|
|
|
61
60
|
def __init__(
|
|
@@ -85,6 +84,18 @@ class Rule(
|
|
|
85
84
|
return self
|
|
86
85
|
|
|
87
86
|
def are_named(self, names: str | Sequence[str]) -> BehaviorSpecification: # type: ignore
|
|
87
|
+
# Check for wildcard usage and suggest alternative
|
|
88
|
+
if isinstance(names, str):
|
|
89
|
+
if "*" in names:
|
|
90
|
+
raise ImproperlyConfigured(
|
|
91
|
+
f'Wildcard "*" detected in module name "{names}". Use "have_name_matching" with a regex pattern instead.'
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
for name in names:
|
|
95
|
+
if "*" in name:
|
|
96
|
+
raise ImproperlyConfigured(
|
|
97
|
+
f'Wildcard "*" detected in module name "{name}". Use "have_name_matching" with a regex pattern instead.'
|
|
98
|
+
)
|
|
88
99
|
self._set_modules(names, lambda name: ModuleNameFilter(name=name))
|
|
89
100
|
return self
|
|
90
101
|
|
|
@@ -133,11 +144,7 @@ class Rule(
|
|
|
133
144
|
for module, name_is_regex in modules:
|
|
134
145
|
module_names.append(module)
|
|
135
146
|
module_creation_fn.append(
|
|
136
|
-
|
|
137
|
-
ModuleNameFilter(name=name)
|
|
138
|
-
if not name_is_regex
|
|
139
|
-
else ModuleNameRegexFilter(name=name)
|
|
140
|
-
)
|
|
147
|
+
ModuleNameRegexFilter if name_is_regex else ModuleNameFilter
|
|
141
148
|
)
|
|
142
149
|
|
|
143
150
|
self._append_modules(module_names, module_creation_fn)
|
|
@@ -229,7 +236,7 @@ class Rule(
|
|
|
229
236
|
def __str__(self) -> str:
|
|
230
237
|
self._assert_required_configuration_present()
|
|
231
238
|
|
|
232
|
-
method_name = f'
|
|
239
|
+
method_name = f"{'should' if self._configuration.should else 'should only' if self._configuration.should_only else 'should not'}"
|
|
233
240
|
|
|
234
241
|
subject_prefix = (
|
|
235
242
|
"Sub modules of "
|
|
@@ -239,7 +246,7 @@ class Rule(
|
|
|
239
246
|
if self._configuration.rule_object_anything:
|
|
240
247
|
object_message = "anything "
|
|
241
248
|
else:
|
|
242
|
-
object_message = f
|
|
249
|
+
object_message = f"modules that are {'sub modules of' if self._configuration.modules_to_check_against[0].identifier_is_parent_module else 'named'} " # type: ignore
|
|
243
250
|
|
|
244
251
|
combined_rule_subjects = self._combine_names(
|
|
245
252
|
self._configuration.modules_to_check # type: ignore
|
|
@@ -250,8 +257,8 @@ class Rule(
|
|
|
250
257
|
return (
|
|
251
258
|
f'{subject_prefix}"{combined_rule_subjects}" '
|
|
252
259
|
f"{method_name} "
|
|
253
|
-
f'
|
|
254
|
-
f'
|
|
260
|
+
f"{'import' if self._configuration.import_ else 'be imported by'} "
|
|
261
|
+
f"{'modules except ' if self._configuration.except_present else ''}"
|
|
255
262
|
f"{object_message}"
|
|
256
263
|
f'"{combined_rule_objects}".'
|
|
257
264
|
)
|
|
@@ -288,9 +295,7 @@ class Rule(
|
|
|
288
295
|
object_message,
|
|
289
296
|
]
|
|
290
297
|
|
|
291
|
-
error_message = (
|
|
292
|
-
f"Specify {', '.join(filter(lambda m: len(m) > 0, messages))}."
|
|
293
|
-
)
|
|
298
|
+
error_message = f"Specify {', '.join(filter(None, messages))}."
|
|
294
299
|
|
|
295
300
|
raise ImproperlyConfigured(error_message)
|
|
296
301
|
|
|
@@ -219,14 +219,6 @@ class RuleViolationMessageGenerator(RuleViolationMessageBaseGenerator):
|
|
|
219
219
|
|
|
220
220
|
return rule_objects_for_rule_subject, violating_rule_subjects
|
|
221
221
|
|
|
222
|
-
def _convert_to_names(
|
|
223
|
-
self, violating_dependencies: list[Dependency]
|
|
224
|
-
) -> list[tuple[str, str]]:
|
|
225
|
-
return [
|
|
226
|
-
(dependency[0].identifier, dependency[1].identifier)
|
|
227
|
-
for dependency in violating_dependencies
|
|
228
|
-
]
|
|
229
|
-
|
|
230
222
|
def _create_should_only_import_violated_messages(
|
|
231
223
|
self, rule_violations: RuleViolations
|
|
232
224
|
) -> list[RuleViolatedMessage]:
|
|
@@ -241,10 +241,16 @@ class RuleViolationDetector(RuleViolationBaseDetector):
|
|
|
241
241
|
this class decides whether any of the requirements have been violated and how.
|
|
242
242
|
|
|
243
243
|
ExplicitlyRequestedDependenciesByBaseModules follow this structure:
|
|
244
|
+
|
|
245
|
+
```
|
|
244
246
|
{(requested dependency, e.g. from A to B): [list of dependencies found that qualifiy as the requested dependency, e.g. A.a1 imports B, A.a2 imports B]}
|
|
247
|
+
```
|
|
245
248
|
|
|
246
249
|
NotExplicitlyRequestedDependenciesByBaseModule on the other hand are structured like this:
|
|
247
|
-
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
{Module for which not explicitly requested dependencies (either from or to this module) were found: [list of such dependencies]}
|
|
253
|
+
```
|
|
248
254
|
|
|
249
255
|
The output is a rule violation object which for each type of rule contains a list of (rule subject, rule object) modules which violate the rule. These are ordered according
|
|
250
256
|
to the order the user used when specifying the rule, i.e. the rule subject is the original rule subject no matter if the rule is an import
|
pytestarch-4.0.0/PKG-INFO
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: PyTestArch
|
|
3
|
-
Version: 4.0.0
|
|
4
|
-
Summary: Test framework for software architecture based on imports between modules
|
|
5
|
-
License: Apache-2.0
|
|
6
|
-
Keywords: architecture,test
|
|
7
|
-
Author: zyskarch
|
|
8
|
-
Author-email: zyskarch@gmail.com
|
|
9
|
-
Maintainer: zyskarch
|
|
10
|
-
Maintainer-email: zyskarch@gmail.com
|
|
11
|
-
Requires-Python: >=3.9
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
|
-
Classifier: Operating System :: OS Independent
|
|
19
|
-
Classifier: Intended Audience :: Developers
|
|
20
|
-
Classifier: Topic :: Software Development :: Testing
|
|
21
|
-
Classifier: Topic :: Software Development :: Testing :: Unit
|
|
22
|
-
Provides-Extra: visualization
|
|
23
|
-
Requires-Dist: matplotlib (>=3.9) ; extra == "visualization"
|
|
24
|
-
Requires-Dist: networkx (>=3.2)
|
|
25
|
-
Project-URL: Documentation, https://zyskarch.github.io/pytestarch/latest
|
|
26
|
-
Project-URL: Repository, https://github.com/zyskarch/pytestarch
|
|
27
|
-
Project-URL: issues, https://github.com/zyskarch/pytestarch/issues
|
|
28
|
-
Description-Content-Type: text/markdown
|
|
29
|
-
|
|
30
|
-
# Welcome to PyTestArch
|
|
31
|
-
|
|
32
|
-
PyTestArch is an open source library that allows users to define architectural rules and test their code against them. It is
|
|
33
|
-
generally inspired by [ArchUnit](https://www.archunit.org/).
|
|
34
|
-
|
|
35
|
-
## Installation Guide
|
|
36
|
-
PyTestArch is available via [PyPI](https://pypi.org/project/pytestarch/) and can be installed e.g. via pip: `pip install pytestarch`. To also install the
|
|
37
|
-
optional dependency matplotlib, which is required to draw the created dependency graphs, install `pytestarch[visualization]`
|
|
38
|
-
|
|
39
|
-
## Usage Guide
|
|
40
|
-
Three steps are required to test an architectural rule:
|
|
41
|
-
|
|
42
|
-
1) Create an evaluable representation of the source code you want to test
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
from pytestarch import get_evaluable_architecture
|
|
46
|
-
|
|
47
|
-
evaluable = get_evaluable_architecture("/home/dummy/project", "/home/dummy/project/src")
|
|
48
|
-
```
|
|
49
|
-
This will scan all python files under /home/dummy/project/src for imports and build an internal representation that can
|
|
50
|
-
later be queried. The first parameter /home/dummy/project helps PyTestArch to differentiate between internal and external
|
|
51
|
-
dependencies. This evaluable can be used for multiple architectural rule checks; if you are using [pytest](https://docs.pytest.org/en/7.1.x/),
|
|
52
|
-
you could use a fixture for this evaluable object.
|
|
53
|
-
|
|
54
|
-
2) Define an architectural rule
|
|
55
|
-
```
|
|
56
|
-
from pytestarch import Rule
|
|
57
|
-
|
|
58
|
-
rule = (
|
|
59
|
-
Rule()
|
|
60
|
-
.modules_that()
|
|
61
|
-
.are_named("project.src.moduleB")
|
|
62
|
-
.should_not()
|
|
63
|
-
.be_imported_by_modules_that()
|
|
64
|
-
.are_sub_modules_of("project.src.moduleA")
|
|
65
|
-
)
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
This rule represents the architectural requirements that a module named "project.src.moduleB" should not be imported by any module
|
|
69
|
-
that is a submodule of "project.src.moduleA", excluding "project.src.moduleA" itself.
|
|
70
|
-
|
|
71
|
-
3) Evaluate your code against this rule
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
rule.assert_applies(evaluable)
|
|
75
|
-
```
|
|
76
|
-
That's it!
|
|
77
|
-
|
pytestarch-4.0.0/README.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# Welcome to PyTestArch
|
|
2
|
-
|
|
3
|
-
PyTestArch is an open source library that allows users to define architectural rules and test their code against them. It is
|
|
4
|
-
generally inspired by [ArchUnit](https://www.archunit.org/).
|
|
5
|
-
|
|
6
|
-
## Installation Guide
|
|
7
|
-
PyTestArch is available via [PyPI](https://pypi.org/project/pytestarch/) and can be installed e.g. via pip: `pip install pytestarch`. To also install the
|
|
8
|
-
optional dependency matplotlib, which is required to draw the created dependency graphs, install `pytestarch[visualization]`
|
|
9
|
-
|
|
10
|
-
## Usage Guide
|
|
11
|
-
Three steps are required to test an architectural rule:
|
|
12
|
-
|
|
13
|
-
1) Create an evaluable representation of the source code you want to test
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
from pytestarch import get_evaluable_architecture
|
|
17
|
-
|
|
18
|
-
evaluable = get_evaluable_architecture("/home/dummy/project", "/home/dummy/project/src")
|
|
19
|
-
```
|
|
20
|
-
This will scan all python files under /home/dummy/project/src for imports and build an internal representation that can
|
|
21
|
-
later be queried. The first parameter /home/dummy/project helps PyTestArch to differentiate between internal and external
|
|
22
|
-
dependencies. This evaluable can be used for multiple architectural rule checks; if you are using [pytest](https://docs.pytest.org/en/7.1.x/),
|
|
23
|
-
you could use a fixture for this evaluable object.
|
|
24
|
-
|
|
25
|
-
2) Define an architectural rule
|
|
26
|
-
```
|
|
27
|
-
from pytestarch import Rule
|
|
28
|
-
|
|
29
|
-
rule = (
|
|
30
|
-
Rule()
|
|
31
|
-
.modules_that()
|
|
32
|
-
.are_named("project.src.moduleB")
|
|
33
|
-
.should_not()
|
|
34
|
-
.be_imported_by_modules_that()
|
|
35
|
-
.are_sub_modules_of("project.src.moduleA")
|
|
36
|
-
)
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
This rule represents the architectural requirements that a module named "project.src.moduleB" should not be imported by any module
|
|
40
|
-
that is a submodule of "project.src.moduleA", excluding "project.src.moduleA" itself.
|
|
41
|
-
|
|
42
|
-
3) Evaluate your code against this rule
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
rule.assert_applies(evaluable)
|
|
46
|
-
```
|
|
47
|
-
That's it!
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/parsed_dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/multiple_rule_applier.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/error_message/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/rule_violations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/utils/partial_match_to_regex_converter.py
RENAMED
|
File without changes
|