deply 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
deply/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
deply/code_analyzer.py ADDED
@@ -0,0 +1,78 @@
1
+ import ast
2
+ from typing import Dict, Set, Tuple
3
+
4
+ from .models.code_element import CodeElement
5
+
6
+
7
+ class CodeAnalyzer:
8
+ def __init__(self, code_elements: Set[CodeElement]):
9
+ self.code_elements = code_elements
10
+ self.dependencies = {} # Dict[CodeElement, Set[Tuple[CodeElement, int]]]
11
+
12
+ def analyze(self):
13
+ name_to_element = self._build_name_to_element_map()
14
+ for code_element in self.code_elements:
15
+ dependencies = self._extract_dependencies(code_element, name_to_element)
16
+ self.dependencies[code_element] = dependencies
17
+
18
+ def _build_name_to_element_map(self) -> Dict[str, Set[CodeElement]]:
19
+ name_to_element = {}
20
+ for elem in self.code_elements:
21
+ name_to_element.setdefault(elem.name, set()).add(elem)
22
+ return name_to_element
23
+
24
+ def _extract_dependencies(self, code_element: CodeElement, name_to_element: Dict[str, Set[CodeElement]]) -> Set[
25
+ Tuple[CodeElement, int]]:
26
+ dependencies = set()
27
+ with open(code_element.file, 'r', encoding='utf-8') as f:
28
+ try:
29
+ tree = ast.parse(f.read(), filename=str(code_element.file))
30
+ except SyntaxError:
31
+ return dependencies # Skip files with syntax errors
32
+
33
+ class DependencyVisitor(ast.NodeVisitor):
34
+ def __init__(self, dependencies):
35
+ self.dependencies = dependencies
36
+
37
+ def visit_Import(self, node):
38
+ for alias in node.names:
39
+ name = alias.asname or alias.name.split('.')[0]
40
+ dep_elements = name_to_element.get(name, set())
41
+ for dep_element in dep_elements:
42
+ self.dependencies.add((dep_element, node.lineno))
43
+
44
+ def visit_ImportFrom(self, node):
45
+ module = node.module
46
+ for alias in node.names:
47
+ name = alias.asname or alias.name
48
+ full_name = f"{module}.{name}" if module else name
49
+ dep_elements = name_to_element.get(name, set())
50
+ for dep_element in dep_elements:
51
+ self.dependencies.add((dep_element, node.lineno))
52
+
53
+ def visit_Call(self, node):
54
+ if isinstance(node.func, ast.Name):
55
+ name = node.func.id
56
+ dep_elements = name_to_element.get(name, set())
57
+ for dep_element in dep_elements:
58
+ self.dependencies.add((dep_element, node.lineno))
59
+ self.generic_visit(node)
60
+
61
+ def visit_Attribute(self, node):
62
+ if isinstance(node.value, ast.Name):
63
+ name = f"{node.value.id}.{node.attr}"
64
+ dep_elements = name_to_element.get(name, set())
65
+ for dep_element in dep_elements:
66
+ self.dependencies.add((dep_element, node.lineno))
67
+ self.generic_visit(node)
68
+
69
+ def visit_Name(self, node):
70
+ if isinstance(node.ctx, ast.Load):
71
+ name = node.id
72
+ dep_elements = name_to_element.get(name, set())
73
+ for dep_element in dep_elements:
74
+ self.dependencies.add((dep_element, node.lineno))
75
+
76
+ visitor = DependencyVisitor(dependencies)
77
+ visitor.visit(tree)
78
+ return dependencies
@@ -0,0 +1,3 @@
1
+ from .base_collector import BaseCollector
2
+ from .file_regex_collector import FileRegexCollector
3
+ from .class_inherits_collector import ClassInheritsCollector
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+ from ..models.code_element import CodeElement
3
+
4
+
5
+ class BaseCollector(ABC):
6
+ @abstractmethod
7
+ def collect(self) -> set[CodeElement]:
8
+ pass
@@ -0,0 +1,44 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from typing import Set
4
+ from .base_collector import BaseCollector
5
+ from ..models.code_element import CodeElement
6
+ from ..utils.ast_utils import get_import_aliases, get_base_name
7
+
8
+
9
+ class ClassInheritsCollector(BaseCollector):
10
+ def __init__(self, config: dict, project_root: Path):
11
+ self.base_class = config.get("base_class", "")
12
+ self.project_root = project_root
13
+
14
+ def collect(self) -> Set[CodeElement]:
15
+ collected_elements = set()
16
+ for file_path in self.project_root.rglob("*.py"):
17
+ tree = self.parse_file(file_path)
18
+ classes = self.get_classes_inheriting(tree, file_path)
19
+ collected_elements.update(classes)
20
+
21
+ return collected_elements
22
+
23
+ def parse_file(self, file_path: Path):
24
+ try:
25
+ with open(file_path, 'r', encoding='utf-8') as f:
26
+ return ast.parse(f.read(), filename=str(file_path))
27
+ except SyntaxError:
28
+ return None
29
+
30
+ def get_classes_inheriting(self, tree, file_path: Path) -> set[CodeElement]:
31
+ import_aliases = get_import_aliases(tree)
32
+ classes = set()
33
+ for node in ast.walk(tree):
34
+ if isinstance(node, ast.ClassDef):
35
+ for base in node.bases:
36
+ base_name = get_base_name(base, import_aliases)
37
+ if base_name == self.base_class:
38
+ code_element = CodeElement(
39
+ file=file_path,
40
+ name=node.name,
41
+ element_type='class'
42
+ )
43
+ classes.add(code_element)
44
+ return classes
@@ -0,0 +1,17 @@
1
+ from typing import Dict, Any
2
+ from pathlib import Path
3
+ from .base_collector import BaseCollector
4
+ from .file_regex_collector import FileRegexCollector
5
+ from .class_inherits_collector import ClassInheritsCollector
6
+
7
+
8
+ class CollectorFactory:
9
+ @staticmethod
10
+ def create(config: Dict[str, Any], project_root: Path) -> BaseCollector:
11
+ collector_type = config.get("type")
12
+ if collector_type == "file_regex":
13
+ return FileRegexCollector(config, project_root)
14
+ elif collector_type == "class_inherits":
15
+ return ClassInheritsCollector(config, project_root)
16
+ else:
17
+ raise ValueError(f"Unknown collector type: {collector_type}")
@@ -0,0 +1,86 @@
1
+ import re
2
+ import ast
3
+ from pathlib import Path
4
+ from typing import Set
5
+ from .base_collector import BaseCollector
6
+ from ..models.code_element import CodeElement
7
+
8
+
9
+ class FileRegexCollector(BaseCollector):
10
+ def __init__(self, config: dict, project_root: Path):
11
+ self.regex_pattern = config.get("regex", "")
12
+ self.element_type = config.get("element_type", "") # 'class', 'function', 'variable'
13
+ self.regex = re.compile(self.regex_pattern)
14
+ self.project_root = project_root
15
+
16
+ def collect(self) -> Set[CodeElement]:
17
+ all_files = self.get_all_files()
18
+ collected_elements = set()
19
+ for file_path in all_files:
20
+ relative_path = str(file_path.relative_to(self.project_root))
21
+ if self.regex.match(relative_path):
22
+ elements = self.get_elements_in_file(file_path)
23
+ collected_elements.update(elements)
24
+ return collected_elements
25
+
26
+ def get_all_files(self):
27
+ return [f for f in self.project_root.rglob('*.py') if f.is_file()]
28
+
29
+ def get_elements_in_file(self, file_path: Path) -> Set[CodeElement]:
30
+ elements = set()
31
+ tree = self.parse_file(file_path)
32
+ if tree is None:
33
+ return elements
34
+
35
+ if not self.element_type or self.element_type == 'class':
36
+ elements.update(self.get_class_names(tree, file_path))
37
+ if not self.element_type or self.element_type == 'function':
38
+ elements.update(self.get_function_names(tree, file_path))
39
+ if not self.element_type or self.element_type == 'variable':
40
+ elements.update(self.get_variable_names(tree, file_path))
41
+ return elements
42
+
43
+ def parse_file(self, file_path: Path):
44
+ try:
45
+ with open(file_path, 'r', encoding='utf-8') as f:
46
+ return ast.parse(f.read(), filename=str(file_path))
47
+ except SyntaxError:
48
+ return None
49
+
50
+ def get_class_names(self, tree, file_path: Path) -> Set[CodeElement]:
51
+ classes = set()
52
+ for node in ast.walk(tree):
53
+ if isinstance(node, ast.ClassDef):
54
+ code_element = CodeElement(
55
+ file=file_path,
56
+ name=node.name,
57
+ element_type='class'
58
+ )
59
+ classes.add(code_element)
60
+ return classes
61
+
62
+ def get_function_names(self, tree, file_path: Path) -> Set[CodeElement]:
63
+ functions = set()
64
+ for node in ast.walk(tree):
65
+ if isinstance(node, ast.FunctionDef):
66
+ code_element = CodeElement(
67
+ file=file_path,
68
+ name=node.name,
69
+ element_type='function'
70
+ )
71
+ functions.add(code_element)
72
+ return functions
73
+
74
+ def get_variable_names(self, tree, file_path: Path) -> Set[CodeElement]:
75
+ variables = set()
76
+ for node in ast.walk(tree):
77
+ if isinstance(node, ast.Assign):
78
+ for target in node.targets:
79
+ if isinstance(target, ast.Name):
80
+ code_element = CodeElement(
81
+ file=file_path,
82
+ name=target.id,
83
+ element_type='variable'
84
+ )
85
+ variables.add(code_element)
86
+ return variables
deply/config_parser.py ADDED
@@ -0,0 +1,13 @@
1
+ from pathlib import Path
2
+ from typing import Any, Dict
3
+ import yaml
4
+
5
+
6
+ class ConfigParser:
7
+ def __init__(self, config_path: Path):
8
+ self.config_path = config_path
9
+
10
+ def parse(self) -> Dict[str, Any]:
11
+ with self.config_path.open("r") as f:
12
+ config = yaml.safe_load(f)
13
+ return config
@@ -0,0 +1,16 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import Set
4
+ from .base_collector import BaseCollector
5
+ from ..utils.file_utils import get_python_files
6
+
7
+
8
+ class FileRegexCollector(BaseCollector):
9
+ def __init__(self, config: dict, project_root: Path):
10
+ self.regex = re.compile(config.get("regex", ""))
11
+ self.project_root = project_root
12
+
13
+ def collect(self) -> Set[Path]:
14
+ files = get_python_files(self.project_root)
15
+ matched_files = {f for f in files if self.regex.match(str(f.relative_to(self.project_root)))}
16
+ return matched_files
deply/main.py ADDED
@@ -0,0 +1,69 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ from .config_parser import ConfigParser
4
+ from .collectors.collector_factory import CollectorFactory
5
+ from .code_analyzer import CodeAnalyzer
6
+ from .rules.dependency_rule import DependencyRule
7
+ from .reports.report_generator import ReportGenerator
8
+
9
+
10
+ def main():
11
+ # Parse command-line arguments
12
+ parser = argparse.ArgumentParser(prog="deply", description='Deply')
13
+ parser.add_argument("--config", required=True, type=str, help="Path to the configuration YAML file")
14
+ parser.add_argument("--project_root", required=True, type=str, help="Root directory of the project to analyze")
15
+ parser.add_argument("--report-format", type=str, choices=["text", "json", "html"], default="text",
16
+ help="Format of the output report")
17
+ parser.add_argument("--output", type=str, help="Output file for the report")
18
+ args = parser.parse_args()
19
+
20
+ config_path = Path(args.config)
21
+ project_root = Path(args.project_root)
22
+
23
+ # Parse configuration
24
+ config = ConfigParser(config_path).parse()
25
+
26
+ # Collect code elements
27
+ code_elements = set()
28
+ code_element_to_layer = {}
29
+ for layer in config['layers']:
30
+ layer_name = layer['name']
31
+ for collector_config in layer['collectors']:
32
+ collector = CollectorFactory.create(collector_config, project_root)
33
+ collected = collector.collect()
34
+ code_elements.update(collected)
35
+ for element in collected:
36
+ code_element_to_layer[element] = layer_name
37
+
38
+ # Analyze code to find dependencies
39
+ analyzer = CodeAnalyzer(code_elements)
40
+ analyzer.analyze()
41
+ code_element_dependencies = analyzer.dependencies
42
+
43
+ # Apply rules
44
+ rule = DependencyRule(config['ruleset'])
45
+ violations = rule.check(
46
+ code_element_to_layer=code_element_to_layer,
47
+ code_element_dependencies=code_element_dependencies
48
+ )
49
+
50
+ # Generate report
51
+ report_generator = ReportGenerator(violations)
52
+ report = report_generator.generate(format=args.report_format)
53
+
54
+ # Output the report
55
+ if args.output:
56
+ output_path = Path(args.output)
57
+ output_path.write_text(report)
58
+ else:
59
+ print(report)
60
+
61
+ # Exit with appropriate status
62
+ if violations:
63
+ exit(1)
64
+ else:
65
+ exit(0)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -0,0 +1,3 @@
1
+ from .base_rule import BaseRule
2
+ from .dependency_rule import DependencyRule
3
+ from .rule_factory import RuleFactory
@@ -0,0 +1,11 @@
1
+ from ..models.code_element import CodeElement
2
+ from ..models.violation import Violation
3
+
4
+
5
+ class BaseRule:
6
+ def check(
7
+ self,
8
+ code_element_to_layer: dict[CodeElement, str],
9
+ code_element_dependencies: dict[CodeElement, set[tuple[CodeElement, int]]],
10
+ ) -> list[Violation]:
11
+ raise NotImplementedError
@@ -0,0 +1,50 @@
1
+ # rules/dependency_rule.py
2
+
3
+ from .base_rule import BaseRule
4
+ from ..models.code_element import CodeElement
5
+ from ..models.violation import Violation
6
+
7
+
8
+ class DependencyRule(BaseRule):
9
+ def __init__(self, ruleset: dict[str, dict[str, list[str]]]):
10
+ self.ruleset = ruleset
11
+
12
+ def check(
13
+ self,
14
+ code_element_to_layer: dict[CodeElement, str],
15
+ code_element_dependencies: dict[CodeElement, set[tuple[CodeElement, int]]],
16
+ ) -> list[Violation]:
17
+ violations = []
18
+ for code_element, layer_name in code_element_to_layer.items():
19
+ layer_rules = self.ruleset.get(layer_name, {})
20
+ allowed_layers = set(layer_rules.get("allow", []))
21
+ disallowed_layers = set(layer_rules.get("disallow", []))
22
+ dependencies = code_element_dependencies.get(code_element, set())
23
+ for dep_element, line_number in dependencies:
24
+ dep_layer = code_element_to_layer.get(dep_element)
25
+
26
+ if dep_layer and dep_layer != layer_name:
27
+ if dep_layer in disallowed_layers:
28
+
29
+ message = f"Layer '{layer_name}' is not allowed to depend on layer '{dep_layer}'"
30
+ violations.append(
31
+ Violation(
32
+ file=code_element.file,
33
+ element_name=code_element.name,
34
+ element_type=code_element.element_type,
35
+ line=line_number,
36
+ message=message,
37
+ )
38
+ )
39
+ elif allowed_layers and dep_layer not in allowed_layers:
40
+ message = f"Layer '{layer_name}' depends on unallowed layer '{dep_layer}'"
41
+ violations.append(
42
+ Violation(
43
+ file=code_element.file,
44
+ element_name=code_element.name,
45
+ element_type=code_element.element_type,
46
+ line=line_number,
47
+ message=message,
48
+ )
49
+ )
50
+ return violations
@@ -0,0 +1,9 @@
1
+ from typing import Dict, Any, List
2
+ from .base_rule import BaseRule
3
+ from .dependency_rule import DependencyRule
4
+
5
+
6
+ class RuleFactory:
7
+ @staticmethod
8
+ def create_rules(ruleset: Dict[str, Any]) -> List[BaseRule]:
9
+ return [DependencyRule(ruleset)]
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Archil
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.1
2
+ Name: deply
3
+ Version: 0.1.0
4
+ Summary: A tool to enforce architectural patterns in python projects
5
+ Home-page: https://github.com/vashkatsi/deply
6
+ Author: Archil Abuladze
7
+ Author-email: armiworker@gmail.com
8
+ License: UNKNOWN
9
+ Project-URL: Bug Tracker, https://github.com/vashkatsi/deply/issues
10
+ Platform: UNKNOWN
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.6
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: PyYAML (>=5.1)
17
+
18
+ # Deply
19
+
20
+ **Deply** is a standalone Python tool for enforcing architectural patterns and dependencies in large
21
+ python projects. By analyzing code structure and dependencies, this tool ensures that architectural rules are followed,
22
+ promoting cleaner, more maintainable, and modular codebases.
23
+
24
+ Inspired by https://github.com/qossmic/deptrac
25
+
26
+ ## Features
27
+
28
+ - **Layer-Based Analysis**: Define project layers and restrict their dependencies to enforce modularity.
29
+ - **Dynamic Layer Configuration**: Easily configure collectors for each layer using file patterns and class inheritance.
30
+ - **Cross-Layer Dependency Rules** (TODO): Specify rules to disallow certain layers from accessing others.
31
+ - **Extensible and Configurable**: Customize layers and rules for any python project setup.
32
+
33
+ ## Installation
34
+
35
+ To install **Deply**, use `pip`:
36
+
37
+ ```bash
38
+ pip install deply
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ Before running the tool, create a configuration file (`config.yaml` or similar) that specifies the rules and target
44
+ files to enforce.
45
+
46
+ ### Example Configuration (`config.example.yaml`)
47
+
48
+ ```yaml
49
+ layers:
50
+ - name: models
51
+ collectors:
52
+ - type: class_inherits
53
+ base_class: "django.db.models.Model"
54
+
55
+ - name: services
56
+ collectors:
57
+ - type: file_regex
58
+ regex: "^.*/providers.py$"
59
+
60
+ - name: views
61
+ collectors:
62
+ - type: file_regex
63
+ regex: ".*/views_api.py"
64
+
65
+ ruleset:
66
+ views:
67
+ disallow:
68
+ - models # Disallows direct access to models in views
69
+
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ Run the tool from the command line by specifying the project root directory and configuration file:
75
+
76
+ ```bash
77
+ python deply.py --project_root='/path/to/your_project' --config=config.example.yaml
78
+ ```
79
+
80
+ ### Arguments
81
+
82
+ - `--project_root`: The path to the root of the project where apps are located.
83
+ - `--config`: Path to the configuration file that defines the rules and target files.
84
+
85
+ ## Sample Output
86
+
87
+ If violations are found, the tool will output a summary of architectural violations grouped by app, along with details
88
+ of each violation, such as the file, line number, and violation message.
89
+
90
+ ```plaintext
91
+ /path/to/your_project/your_project/app1/views_api.py:74 - Layer 'views' is not allowed to depend on layer 'models'
92
+ ```
93
+
94
+ ## Running Tests
95
+
96
+ To test the tool, use `unittest`:
97
+
98
+ ```bash
99
+ python -m unittest discover tests
100
+ ```
101
+
102
+ ## License
103
+
104
+ See the [LICENSE](LICENSE) file for details.
105
+
106
+
@@ -0,0 +1,22 @@
1
+ deply/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
2
+ deply/code_analyzer.py,sha256=bppc8CPanzF_apI0CYO8y-q4tUbprz5r0mX4lLTHK2s,3384
3
+ deply/config_parser.py,sha256=Y3lL4VjE9W9IuFIak0SfzGrxCGz2Yqw2ulsOo1qHMEo,317
4
+ deply/file_regex_collector.py,sha256=oEBEbEMhgjtmhmPkshTl4m0ebZVLnKxm55hvc71bVkw,567
5
+ deply/main.py,sha256=DWOUhKXu05sSw6mBbOJm0p5Yem7Ozf-OJM20XYoH5Tg,2336
6
+ deply/collectors/__init__.py,sha256=AjpFzRM4zyz9avFdnRsYDZd00nYY19gEf91aWr5pQzc,156
7
+ deply/collectors/base_collector.py,sha256=b_RKmnJVKR4detYVWr93yC6izj-MoMPCeCRZLt06IbE,186
8
+ deply/collectors/class_inherits_collector.py,sha256=P_54mhw5xU8SiFYCVOgDF0xc5uufhobFHeIqmPC-3nM,1671
9
+ deply/collectors/collector_factory.py,sha256=ykPYQfDs-HZsr2mI1SWXPotf5Qy9wQoevxsW1L-mlGs,679
10
+ deply/collectors/file_regex_collector.py,sha256=zAKhwctKeSZbKYzoTI0BFSDujh4ih4vEs99lTE8wgeA,3336
11
+ deply/rules/__init__.py,sha256=fuowBlrNBki39MUvD-T11ATZng-8cz_yOzhXQKFUuIo,114
12
+ deply/rules/base_rule.py,sha256=zAjWRTcx_jPMxaTFskNFt9NVaELQfY-CKBy5dBhdrow,345
13
+ deply/rules/dependency_rule.py,sha256=vlKjJlBI_1jgGgDXS-7JmDaJocYooYSfkFOBMX_VycA,2255
14
+ deply/rules/rule_factory.py,sha256=pejJQNoivlP53XXZpK1hR2bxzJOPvYnMB5URbiW_szs,256
15
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ tests/test.py,sha256=wEjN3qQdmuIH-jmVG6d4dX0SnD0pFSbDHhm3v1INyCM,3106
17
+ deply-0.1.0.dist-info/LICENSE,sha256=bVLuZzbcVnSRRlbO6G8kmv7sN4vLJPrhVvTHSc4LUYI,1493
18
+ deply-0.1.0.dist-info/METADATA,sha256=4WpxYbeXY3Gw4mwnnY1E30jes1j-wJ0t7NFmWckIVyk,2905
19
+ deply-0.1.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
20
+ deply-0.1.0.dist-info/entry_points.txt,sha256=8m_ILhOfLloKfK4IPzRBDw-4ha1oAVLptjLLqPUZswI,43
21
+ deply-0.1.0.dist-info/top_level.txt,sha256=dzbGa4EPLXcGenGwT9w1tLl_DXjbi73jl6suLa4ViXA,12
22
+ deply-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.37.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ deply = deply.main:main
3
+
@@ -0,0 +1,2 @@
1
+ deply
2
+ tests
tests/__init__.py ADDED
File without changes
tests/test.py ADDED
@@ -0,0 +1,110 @@
1
+ import unittest
2
+ import os
3
+ import shutil
4
+ from deply.main import main
5
+ from unittest.mock import patch
6
+ from io import StringIO
7
+ import sys
8
+
9
+
10
+ class TestArchCheck(unittest.TestCase):
11
+
12
+ def setUp(self):
13
+ # Set up a simulated project structure
14
+ self.test_project_root = os.path.abspath('tests/test_project')
15
+ self.app_path = os.path.join(self.test_project_root, 'app')
16
+ os.makedirs(self.app_path, exist_ok=True)
17
+
18
+ # create __init__.py in the test_project_root directory
19
+ with open(os.path.join(self.test_project_root, '__init__.py'), 'w') as f:
20
+ f.write("")
21
+
22
+ # Create config.yaml for the test
23
+ self.config_path = os.path.join(self.test_project_root, 'config.yaml')
24
+ with open(self.config_path, 'w') as f:
25
+ f.write("""
26
+ layers:
27
+ - name: models
28
+ collectors:
29
+ - type: class_inherits
30
+ base_class: "BaseModel"
31
+
32
+ - name: views
33
+ collectors:
34
+ - type: file_regex
35
+ regex: ".*/views.py"
36
+
37
+ ruleset:
38
+ views:
39
+ disallow:
40
+ - models
41
+ """)
42
+
43
+ # create __init__.py in the app directory
44
+ with open(os.path.join(self.app_path, '__init__.py'), 'w') as f:
45
+ f.write("")
46
+
47
+ # Create models.py with a class
48
+ with open(os.path.join(self.app_path, 'models.py'), 'w') as f:
49
+ f.write("""
50
+ class BaseModel:
51
+ pass
52
+
53
+
54
+ class MyModel(BaseModel):
55
+ pass
56
+ """)
57
+
58
+ def tearDown(self):
59
+ # Clean up the test project directory after each test
60
+ shutil.rmtree(self.test_project_root)
61
+
62
+ def test_class_dependency_violation(self):
63
+ # Create views.py that imports MyModel from models
64
+ with open(os.path.join(self.app_path, 'views.py'), 'w') as f:
65
+ f.write("""
66
+ from .models import MyModel
67
+
68
+ def view_function():
69
+ instance = MyModel()
70
+ """)
71
+ # Capture the output
72
+ with patch.object(sys, 'argv', [
73
+ 'archcheck',
74
+ '--project_root', self.test_project_root,
75
+ '--config', self.config_path
76
+ ]):
77
+ with patch('sys.stdout', new=StringIO()) as fake_out, patch('sys.stderr', new=StringIO()):
78
+ try:
79
+ main()
80
+ except SystemExit:
81
+ pass
82
+ output = fake_out.getvalue()
83
+
84
+ self.assertIn('views.py:2', output)
85
+
86
+ def test_no_violation(self):
87
+ # Create views.py that does not import from models
88
+ with open(os.path.join(self.app_path, 'views.py'), 'w') as f:
89
+ f.write("""
90
+ def view_function():
91
+ pass
92
+ """)
93
+ # Capture the output
94
+ with patch.object(sys, 'argv', [
95
+ 'archcheck',
96
+ '--project_root', self.test_project_root,
97
+ '--config', self.config_path
98
+ ]):
99
+ with patch('sys.stdout', new=StringIO()) as fake_out, patch('sys.stderr', new=StringIO()):
100
+ try:
101
+ main()
102
+ except SystemExit:
103
+ pass
104
+ output = fake_out.getvalue()
105
+
106
+ self.assertNotIn('views', output)
107
+
108
+
109
+ if __name__ == '__main__':
110
+ unittest.main()