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 +1 -0
- deply/code_analyzer.py +78 -0
- deply/collectors/__init__.py +3 -0
- deply/collectors/base_collector.py +8 -0
- deply/collectors/class_inherits_collector.py +44 -0
- deply/collectors/collector_factory.py +17 -0
- deply/collectors/file_regex_collector.py +86 -0
- deply/config_parser.py +13 -0
- deply/file_regex_collector.py +16 -0
- deply/main.py +69 -0
- deply/rules/__init__.py +3 -0
- deply/rules/base_rule.py +11 -0
- deply/rules/dependency_rule.py +50 -0
- deply/rules/rule_factory.py +9 -0
- deply-0.1.0.dist-info/LICENSE +28 -0
- deply-0.1.0.dist-info/METADATA +106 -0
- deply-0.1.0.dist-info/RECORD +22 -0
- deply-0.1.0.dist-info/WHEEL +5 -0
- deply-0.1.0.dist-info/entry_points.txt +3 -0
- deply-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test.py +110 -0
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,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()
|
deply/rules/__init__.py
ADDED
deply/rules/base_rule.py
ADDED
|
@@ -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,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,,
|
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()
|