Как определить импорт вложенных подпакетов для проверки архитектурных правил связывания - PullRequest
1 голос
/ 15 апреля 2020

Структура каталогов проекта:

У меня есть проект 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

...