Структура каталогов проекта:
У меня есть проект Python 3 со следующей структурой каталогов (пакет sr c и тестовый пакет с исходным кодом):
root/
├── readme.md
├── setup.py
├── setup.cfg
├── src
│ ├── common
│ │ ├── __init__.py
│ │ └── helpers.py
│ ├── a
│ │ ├── __init__.py
│ │ └── decode_a.py
│ ├── b
│ │ ├── __init__.py
│ │ └── decode_b.py
│ └── coupling.bat
└── test
├── __init__.py
├── sibbling_coupling.py
└── test_coupling.py
Исходная папка разделена на несколько дочерних подпакетов: common с общими помощниками и a и b вложенных папок.
Следующие правила проектирования архитектуры были определены:
- общий подпакет: любая дочерняя подпапка (a, b) может импортировать из общего
- a подпакет: нет дочерней подпапки (обычный, b) может импортировать из подпакета
- b : нет вложенной папки (общий, a) можно импортировать из b
Это упрощенный пример , но в реальном проекте нет ничего странного в том, что некоторые операции импорта нарушают эти правила. Например: Содержимое src / decode_b.py
from a.decode_a import decode_a << import breaking coupling rules: From b imports a
from common.helpers import to_level
def decode_b(string):
level = to_level(string)
result = decode_a(level)
return result
Итак, необходим механизм для проверки правил архитектурной связи:
Сначала я подумал, если возможно, обнаружить внутри. init источник импорта.
Но я не нашел ничего, что позволило бы мне вызвать исключение или записать ошибку.
Во-вторых, я сделал windows летучую мышь, чтобы обнаружить ее ( src / coup.bat):
findstr /b /c:"from a." /s /d:common;b *.py
findstr /b /c:"from b." /s /d:common;a *.py
findstr /b /c:"import b." /s /d:common;a *.py
findstr /b /c:"import a." /s /d:common;b *.py
- Плюсы: это быстрое решение и обнаружение критического импорта (см. следующий вывод)
- Минусы: это windows
b_decode.py:from a.a_decode import decode_a
И в-третьих, я сделал эквивалентное решение python, используя glob (test / test_coupling.py):
def test_options_file():
subpackage_coupling = SibblingCoupling()
forbidden_imports = subpackage_coupling.check()
assert forbidden_imports == []
С файлом sibbling_coupling.py, чтобы сделать проверка:
class SibblingCoupling:
PYTHON_FILES = "*.py"
SRC_PATH = "src"
# Key = subpackage_name, value = list of allowed subpackages to be imported (apart from itself)
COUPLING_RULES = {
"common": [],
"a": ["common"],
"b": ["common"],
}
def __init__(self):
working_dir = pathlib.Path.cwd()
self.src_path = working_dir.parents[0] / self.SRC_PATH
self.subpackages = set(self.COUPLING_RULES.keys())
self.forbidden_imports = []
def check(self):
for name, allowed_subpackages in self.COUPLING_RULES.items():
self._check_subpackage(name, allowed_subpackages)
return self.forbidden_imports
def _check_subpackage(self, name, allowed_subpackages):
allowed_coupling = set(allowed_subpackages)
allowed_coupling.add(name)
forbidden_coupling = self.subpackages - allowed_coupling
for file in self._files_inside_subpackage(name):
self._check_file(file, name, forbidden_coupling)
def _files_inside_subpackage(self, name: str):
result = (self.src_path / name).rglob(self.PYTHON_FILES)
return result
def _check_file(self, file, name, forbidden_coupling):
for forbidden_subpackage in forbidden_coupling:
text = file.read_text()
forbidden_type_from = re.findall(
f"^from {forbidden_subpackage}.* import .*$", text, re.MULTILINE
)
forbidden_type_import = re.findall(
f"^import {forbidden_subpackage}.*$", text, re.MULTILINE
)
if forbidden_imports := (forbidden_type_from + forbidden_type_import):
self._add_forbidden_imports(forbidden_imports, name, forbidden_subpackage)
def _add_forbidden_imports(self, forbidden_imports, name, forbidden_subpackage):
forbidden_results = [
f"{name} imports {forbidden_subpackage}: {forbidden}"
for forbidden in forbidden_imports
]
self.forbidden_imports += forbidden_results
Когда я запускаю с помощью pytest, тест завершается неудачно, как и ожидалось, со следующей ошибкой утверждения:
Expected :[]
Actual :['b imports a: from a.decode_a import decode_a']
Вопрос:
Нет ли другого умнее / больше pythoni c / или не столь подробное решение для обеспечения соблюдения правил проектирования архитектуры (для любой операционной системы).
Примечание. Этот вопрос основан на python 3.8.2