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.
Files changed (59) hide show
  1. pytestarch-5.0.0/PKG-INFO +176 -0
  2. pytestarch-5.0.0/README.md +145 -0
  3. {pytestarch-4.0.0 → pytestarch-5.0.0}/pyproject.toml +14 -19
  4. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/diagram_parser.py +1 -1
  5. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/diagram_rule.py +1 -1
  6. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/breadth_first_searches.py +14 -10
  7. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/evaluable_architecture.py +8 -7
  8. pytestarch-5.0.0/src/pytestarch/eval_structure/exceptions.py +18 -0
  9. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/module_name_converter.py +4 -5
  10. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/networkxgraph.py +30 -3
  11. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/utils.py +5 -1
  12. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/parser.py +1 -2
  13. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/pytestarch.py +3 -3
  14. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/base_language.py +3 -3
  15. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/layered_architecture_rule.py +5 -5
  16. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/rule.py +21 -16
  17. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/error_message/message_generator.py +0 -8
  18. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/rule_matcher.py +1 -1
  19. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/rule_violation_detector.py +7 -1
  20. pytestarch-4.0.0/PKG-INFO +0 -77
  21. pytestarch-4.0.0/README.md +0 -47
  22. pytestarch-4.0.0/src/pytestarch/eval_structure/exceptions.py +0 -9
  23. {pytestarch-4.0.0 → pytestarch-5.0.0}/LICENSE +0 -0
  24. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/__init__.py +0 -0
  25. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/__init__.py +0 -0
  26. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/dependency_to_rule_converter.py +0 -0
  27. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/exceptions.py +0 -0
  28. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/diagram_extension/parsed_dependencies.py +0 -0
  29. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/__init__.py +0 -0
  30. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/evaluable_graph.py +0 -0
  31. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/evaluable_structures.py +0 -0
  32. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure/types.py +0 -0
  33. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/__init__.py +0 -0
  34. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/__init__.py +0 -0
  35. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/config.py +0 -0
  36. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/converter.py +0 -0
  37. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/exceptions.py +0 -0
  38. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/file_filter.py +0 -0
  39. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/import_filter.py +0 -0
  40. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/import_types.py +0 -0
  41. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/file_import/importee_module_calculator.py +0 -0
  42. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/graph_generation/__init__.py +0 -0
  43. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/eval_structure_generation/graph_generation/graph_generator.py +0 -0
  44. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/py.typed +0 -0
  45. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/LANGUAGE_DEFINTION.md +0 -0
  46. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/__init__.py +0 -0
  47. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/exceptions.py +0 -0
  48. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/query_language/multiple_rule_applier.py +0 -0
  49. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/__init__.py +0 -0
  50. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/error_message/__init__.py +0 -0
  51. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/exceptions.py +0 -0
  52. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/__init__.py +0 -0
  53. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/behavior_requirement.py +0 -0
  54. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/layer_rule_violation_detector.py +0 -0
  55. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/module_requirement.py +0 -0
  56. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/rule_assessment/rule_check/rule_violations.py +0 -0
  57. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/utils/__init__.py +0 -0
  58. {pytestarch-4.0.0 → pytestarch-5.0.0}/src/pytestarch/utils/decorators.py +0 -0
  59. {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.0.0"
4
- requires-python = ">=3.9"
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.2"]
25
+ dependencies = ["networkx>=3.6"]
26
26
 
27
27
  [project.optional-dependencies]
28
- visualization = ["matplotlib>=3.9"]
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
- mkdocs = "1.6.0"
37
- mkdocstrings-python = "1.10.3"
38
- mkdocs-material = "9.5.25"
39
- mkdocs-autorefs = "1.0.1"
40
- mkdocs-material-extensions = "1.3.1"
41
- mkdocstrings = "0.25.1"
42
- griffe = "0.45.2"
43
- nox = "2024.4.15"
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 -->, ->, <--, <-, -text->, or <-text-. The dependee is to be placed
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
- If False, 'should import' rules will be generated instead.
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
@@ -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, to_modules
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(tuple(to_modules([node, child])))
41
- return dependencies # type: ignore
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(tuple(to_modules([node, child])))
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 # type: ignore
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(tuple(to_modules([parent, node])))
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 # type: ignore
183
+ return nodes_fulfilling_criteria
180
184
 
181
185
 
182
186
  def get_all_submodules_of(
@@ -236,8 +236,8 @@ class EvaluableArchitecture(Protocol):
236
236
  matches.
237
237
 
238
238
  Args:
239
- dependent: Module(s)
240
- dependent_upon: Module(s)
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
- dependent: Module
264
- dependent_upon: Module
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
- dependent: Module
286
- dependent_upon: Module
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 (https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.spring_layout.html#networkx.drawing.layout.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
+ """
@@ -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
- - modules: list of modules, some of which may need converting since their names are regex patterns
31
- - arch: architecture that contains actual modules
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
- if module_to_match in never_matched:
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'No modules found that match: {", ".join(never_matched)}'
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, (str, tuple)):
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" can be
228
- used to draw the graph onto an existing matploblib Axis object.
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(lambda node: Module(identifier=node), nodes))
18
+ return list(map(to_module, nodes))
15
19
 
16
20
 
17
21
  def filter_to_module(filter: ModuleFilter) -> Module:
@@ -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
- with open(absolute_path) as file:
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 regex_exclusions is not specified.
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, Union
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, Union[str, list[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, Union[str, list[str]]], ABC):
261
+ class LayerRuleObject(LayerSpecification[RuleApplier, str | list[str]], ABC):
262
262
  pass
263
263
 
264
264
 
@@ -54,7 +54,7 @@ class LayeredArchitecture(BaseLayeredArchitecture, LayerName, LayerDefinition):
54
54
 
55
55
  if underspecified_layers:
56
56
  raise ImproperlyConfigured(
57
- f'Specify the modules of layer(s) {", ".join(underspecified_layers)} first.'
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'Module(s) {", ".join(duplicates)} already assigned to a layer.'
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'Layered Architecture: {"; ".join(layers)}'
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
- lambda name: (
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'{"should" if self._configuration.should else "should only" if self._configuration.should_only else "should not"}'
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'modules that are {"sub modules of" if self._configuration.modules_to_check_against[0].identifier_is_parent_module else "named"} ' # type: ignore
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'{"import" if self._configuration.import_ else "be imported by"} '
254
- f'{"modules except " if self._configuration.except_present else ""}'
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]:
@@ -256,6 +256,6 @@ class LayerRuleMatcher(RuleMatcher):
256
256
  )
257
257
 
258
258
  else:
259
- result = result + module_name_conversion_mapping[module.identifier]
259
+ result.extend(module_name_conversion_mapping.get(module.identifier, []))
260
260
 
261
261
  return result
@@ -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
- {Module for which not explicitly requested dependencies (either from or to this module) were found: [list of such dependencies]}.
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
-
@@ -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!
@@ -1,9 +0,0 @@
1
- from __future__ import annotations
2
-
3
-
4
- class ImpossibleMatch(Exception):
5
- pass
6
-
7
-
8
- class LayerMismatch(Exception):
9
- pass
File without changes