pytest-fsd 0.2.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 (48) hide show
  1. pytest_fsd-0.2.0/PKG-INFO +171 -0
  2. pytest_fsd-0.2.0/README.md +160 -0
  3. pytest_fsd-0.2.0/pyproject.toml +25 -0
  4. pytest_fsd-0.2.0/src/pytest_fsd/__init__.py +84 -0
  5. pytest_fsd-0.2.0/src/pytest_fsd/_lib/__init__.py +1 -0
  6. pytest_fsd-0.2.0/src/pytest_fsd/_lib/ast_utils.py +54 -0
  7. pytest_fsd-0.2.0/src/pytest_fsd/_lib/fs_utils.py +31 -0
  8. pytest_fsd-0.2.0/src/pytest_fsd/_lib/violations.py +22 -0
  9. pytest_fsd-0.2.0/src/pytest_fsd/config.py +62 -0
  10. pytest_fsd-0.2.0/src/pytest_fsd/py.typed +0 -0
  11. pytest_fsd-0.2.0/src/pytest_fsd/rules/__init__.py +1 -0
  12. pytest_fsd-0.2.0/src/pytest_fsd/rules/ambiguous_slice_names/README.md +29 -0
  13. pytest_fsd-0.2.0/src/pytest_fsd/rules/ambiguous_slice_names/__init__.py +70 -0
  14. pytest_fsd-0.2.0/src/pytest_fsd/rules/excessive_slicing/README.md +41 -0
  15. pytest_fsd-0.2.0/src/pytest_fsd/rules/excessive_slicing/__init__.py +47 -0
  16. pytest_fsd-0.2.0/src/pytest_fsd/rules/forbidden_imports/README.md +37 -0
  17. pytest_fsd-0.2.0/src/pytest_fsd/rules/forbidden_imports/__init__.py +61 -0
  18. pytest_fsd-0.2.0/src/pytest_fsd/rules/import_locality/README.md +44 -0
  19. pytest_fsd-0.2.0/src/pytest_fsd/rules/inconsistent_naming/README.md +34 -0
  20. pytest_fsd-0.2.0/src/pytest_fsd/rules/insignificant_slice/README.md +26 -0
  21. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_cross_imports/README.md +30 -0
  22. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_cross_imports/__init__.py +57 -0
  23. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_file_segments/README.md +39 -0
  24. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_file_segments/__init__.py +72 -0
  25. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_layer_public_api/README.md +30 -0
  26. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_layer_public_api/__init__.py +41 -0
  27. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_processes/README.md +24 -0
  28. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_public_api_sidestep/README.md +31 -0
  29. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_public_api_sidestep/__init__.py +154 -0
  30. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_reserved_folder_names/README.md +37 -0
  31. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_reserved_folder_names/__init__.py +71 -0
  32. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_segmentless_slices/README.md +32 -0
  33. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_segmentless_slices/__init__.py +62 -0
  34. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_segments_on_sliced_layers/README.md +29 -0
  35. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_segments_on_sliced_layers/__init__.py +46 -0
  36. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_ui_in_app/README.md +38 -0
  37. pytest_fsd-0.2.0/src/pytest_fsd/rules/no_ui_in_app/__init__.py +87 -0
  38. pytest_fsd-0.2.0/src/pytest_fsd/rules/public_api/README.md +34 -0
  39. pytest_fsd-0.2.0/src/pytest_fsd/rules/public_api/__init__.py +65 -0
  40. pytest_fsd-0.2.0/src/pytest_fsd/rules/repetitive_naming/README.md +31 -0
  41. pytest_fsd-0.2.0/src/pytest_fsd/rules/repetitive_naming/__init__.py +57 -0
  42. pytest_fsd-0.2.0/src/pytest_fsd/rules/segments_by_purpose/README.md +38 -0
  43. pytest_fsd-0.2.0/src/pytest_fsd/rules/segments_by_purpose/__init__.py +112 -0
  44. pytest_fsd-0.2.0/src/pytest_fsd/rules/shared_lib_grouping/README.md +46 -0
  45. pytest_fsd-0.2.0/src/pytest_fsd/rules/shared_lib_grouping/__init__.py +47 -0
  46. pytest_fsd-0.2.0/src/pytest_fsd/rules/typo_in_layer_name/README.md +29 -0
  47. pytest_fsd-0.2.0/src/pytest_fsd/rules/unknown_folders_in_root/README.md +35 -0
  48. pytest_fsd-0.2.0/src/pytest_fsd/rules/unknown_folders_in_root/__init__.py +50 -0
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.3
2
+ Name: pytest-fsd
3
+ Version: 0.2.0
4
+ Summary: Add your description here
5
+ Author: Lebedev Nikita
6
+ Author-email: Lebedev Nikita <rachet337@gmail.com>
7
+ Requires-Dist: pytest-archon>=0.0.7
8
+ Requires-Dist: tomli>=2.0.1 ; python_full_version < '3.11'
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+
12
+ # pytest-fsd
13
+
14
+ **FSD Architecture Validation for Python Projects.**
15
+
16
+ `pytest-fsd` автоматически проверяет архитектуру вашего Python-проекта на соответствие методологии [Feature-Sliced Design](https://feature-sliced.design/).
17
+
18
+ Использует гибридный подход: динамические проверки через [pytest-archon](https://pypi.org/project/pytest-archon/) + статический AST-анализ + проверка файловой структуры.
19
+
20
+ ## Установка
21
+
22
+ ```bash
23
+ pip install pytest-fsd
24
+ # или
25
+ uv add --dev pytest-fsd
26
+ ```
27
+
28
+ ## Использование
29
+
30
+ ### 1. Настройка `pyproject.toml`
31
+
32
+ ```toml
33
+ [tool.pytest_fsd]
34
+ base_path = "src"
35
+ layers = ["app", "windows", "widgets", "features", "entities", "shared"]
36
+ ```
37
+
38
+ ### 2. Создание теста
39
+
40
+ ```python
41
+ # tests/test_architecture.py
42
+ from pytest_fsd import validate_fsd_architecture
43
+
44
+ def test_project_architecture():
45
+ validate_fsd_architecture()
46
+ ```
47
+
48
+ ### 3. Запуск
49
+
50
+ ```bash
51
+ pytest tests/test_architecture.py -vv
52
+ ```
53
+
54
+ ## Архитектура библиотеки
55
+
56
+ Каждое правило — это папка внутри `src/pytest_fsd/rules/<rule_name>/` с:
57
+
58
+ - `__init__.py` — логика проверки (функция `check(config, project_root) -> List[Violation]`)
59
+ - `README.md` — описание правила, примеры, rationale
60
+
61
+ ```
62
+ src/pytest_fsd/
63
+ __init__.py # Фасад: validate_fsd_architecture()
64
+ config.py # Чтение [tool.pytest_fsd] из pyproject.toml
65
+ _lib/ # Общие утилиты
66
+ violations.py # Единый Violation dataclass
67
+ ast_utils.py # AST-парсинг импортов
68
+ fs_utils.py # Файловые утилиты, константы сегментов
69
+ rules/
70
+ forbidden_imports/ # pytest-archon: слои импортируют только нижележащие
71
+ no_cross_imports/ # pytest-archon: слайсы в одном слое независимы
72
+ no_public_api_sidestep/ # AST: импорт только через __init__.py слайса
73
+ no_layer_public_api/ # FS: слоевые папки без __init__.py
74
+ no_ui_in_app/ # AST: запрет UI-фреймворков в app
75
+ repetitive_naming/ # FS: файлы не дублируют имя слайса
76
+ no_segmentless_slices/ # FS: слайс содержит хотя бы один сегмент
77
+ segments_by_purpose/ # FS: запрет utils/helpers/components/hooks
78
+ ambiguous_slice_names/ # FS: имена слайсов ≠ имена сегментов shared
79
+ no_segments_on_sliced_layers/ # FS: в слайсовых слоях не должно быть сегментов напрямую
80
+ public_api/ # FS: каждый слайс должен иметь __init__.py
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Матрица покрытия правил Steiger
86
+
87
+ Полный список правил из [Steiger FSD Plugin](https://github.com/feature-sliced/steiger) и их статус в `pytest-fsd`:
88
+
89
+ | # | Steiger Rule | pytest-fsd Status | Описание |
90
+ | --- | ----------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
91
+ | 1 | `forbidden-imports` / `no-higher-level-imports` | ✅ **Полностью** | Слои импортируют только нижележащие слои |
92
+ | 2 | `no-cross-imports` | ✅ **Полностью** | Слайсы в одном слое независимы друг от друга |
93
+ | 3 | `no-public-api-sidestep` | ✅ **Полностью** | Импорт из чужого слайса только через `__init__.py` |
94
+ | 4 | `public-api` | ✅ **Полностью** | Каждый слайс и сегмент shared обязан иметь `__init__.py` |
95
+ | 5 | `no-layer-public-api` | ✅ **Полностью** | Папки слоев (`features/`, `entities/`) не должны содержать `__init__.py` |
96
+ | 6 | `segments-by-purpose` | ✅ **Полностью** | Запрет `utils`, `helpers`, `hooks`, `components`, `modals`, `types`, `constants` и др. |
97
+ | 7 | `no-segmentless-slices` | ✅ **Полностью** | Слайс обязан содержать хотя бы один стандартный сегмент |
98
+ | 8 | `repetitive-naming` | ✅ **Полностью** | Файлы не дублируют имя слайса (`user/user_model.py` → `user/model.py`) |
99
+ | 9 | `no-ui-in-app` | ✅ **Полностью** | Слой `app` не должен импортировать UI-фреймворки |
100
+ | 10 | `ambiguous-slice-names` | ✅ **Полностью** | Имена слайсов не совпадают с сегментами `shared/` |
101
+ | 11 | `no-segments-on-sliced-layers` | ✅ **Полностью** | В слайсовых слоях нет прямых папок-сегментов |
102
+ | 12 | `inconsistent-naming` | 🔶 **Ruff** | Обеспечивается плагином `N` (pep8-naming) в `Ruff` |
103
+ | 13 | `import-locality` | 🔶 **Ruff** | Обеспечивается плагином `TID` (flake8-tidy-imports) в `Ruff` |
104
+ | 14 | `typo-in-layer-name` | 🔶 **Конфигурация** | Покрывается блоком `[tool.pytest_fsd].layers` в `pyproject.toml` |
105
+ | 15 | `no-processes` | 🔶 **Конфигурация** | Слой `processes` deprecated; просто не включайте его в `layers` |
106
+ | 16 | `excessive-slicing` | ⚡ **Опционально** | Более 20 слайсов в одном слое (порог: 20) |
107
+ | 17 | `insignificant-slice` | 🟡 **Ручная проверка** | Требует анализа графа импортов для определения "незначительных" слайсов |
108
+ | 18 | `no-file-segments` | ⚡ **Опционально** | Сегмент как файл (`model.py`) вместо папки (`model/`) |
109
+ | 19 | `shared-lib-grouping` | ⚡ **Опционально** | Более 15 файлов в `shared/lib` без группировки |
110
+ | 20 | `no-reserved-folder-names` | ⚡ **Опционально** | Подпапки в сегментах не должны совпадать с именами сегментов |
111
+
112
+ ### Легенда
113
+
114
+ | Статус | Значение |
115
+ | -------------------------- | --------------------------------------------------------------------------------------------------- |
116
+ | ✅ **Полностью** | Правило полностью автоматизировано и выполняется при каждом запуске `pytest` |
117
+ | ⚡ **Опционально** | Правило автоматизировано, но включается через `extra_rules` в `pyproject.toml` |
118
+ | 🔶 **Ruff / Конфигурация** | Покрывается внешними инструментами (`Ruff`) или конфигурацией `pyproject.toml` |
119
+ | 🟡 **Ручная проверка** | Требует субъективной оценки или сложного анализа, который лучше производить вручную при code review |
120
+
121
+ ---
122
+
123
+ ## Включение дополнительных правил
124
+
125
+ Добавьте в `pyproject.toml`:
126
+
127
+ ```toml
128
+ [tool.pytest_fsd]
129
+ base_path = "src"
130
+ layers = ["app", "windows", "widgets", "features", "entities", "shared"]
131
+ extra_rules = [
132
+ "excessive-slicing", # ≤ 20 слайсов на слой
133
+ "shared-lib-grouping", # ≤ 15 файлов в shared/lib
134
+ "no-file-segments", # Сегменты должны быть папками, не файлами
135
+ "no-reserved-folder-names" # Подпапки сегментов не могут называться ui/model/api/lib/config
136
+ ]
137
+ ```
138
+
139
+ Каждое правило подробно описано в `src/pytest_fsd/rules/<rule_name>/README.md`.
140
+
141
+ ---
142
+
143
+ ## Настройка Ruff для смежных правил
144
+
145
+ Для полного покрытия FSD-правил, которые Steiger проверяет на уровне линтинга (и которые `pytest-fsd` не дублирует), добавьте в `pyproject.toml`:
146
+
147
+ ```toml
148
+ [tool.ruff.lint]
149
+ select = ["N", "TID"]
150
+
151
+ [tool.ruff.lint.flake8-tidy-imports]
152
+ ban-relative-imports = "parents"
153
+ ```
154
+
155
+ | Ruff Plugin | Steiger Rule | Что проверяет |
156
+ | --------------------------- | --------------------- | ----------------------------------------------- |
157
+ | `N` (pep8-naming) | `inconsistent-naming` | `snake_case` для модулей, переменных, функций |
158
+ | `TID` (flake8-tidy-imports) | `import-locality` | Запрет relative imports из родительских пакетов |
159
+
160
+ Подробные описания и примеры конфигурации: `src/pytest_fsd/rules/inconsistent_naming/README.md` и `src/pytest_fsd/rules/import_locality/README.md`.
161
+
162
+ ## Известные ограничения (Known Limitations)
163
+
164
+ - **`TYPE_CHECKING` импорты**: Правила, использующие `pytest-archon` (например, `forbidden-imports` и `no-cross-imports`), работают на базе динамического анализа графа импортов в рантайме. Импорты, находящиеся внутри блоков `if TYPE_CHECKING:`, не выполняются при загрузке модуля и, следовательно, **не видны для этих правил**.
165
+ - **Относительные импорты**: В модуле `ast_utils.py` добавлена поддержка относительных путей для проверки `no-public-api-sidestep`, однако `Ruff` (плагин `TID`) всё равно лучше справляется с контролем относительных импортов за пределами слайсов.
166
+ - **Динамический `__all__`**: Правило `no-public-api-sidestep` использует статический AST-анализ для извлечения `__all__` из файла `__init__.py`. Если список экспортов формируется динамически (например, `__all__ = a + b`), статический анализатор не сможет его прочитать, и инструмент может выдать ложноположительные нарушения. Экспорты в `__all__` должны быть заданы как явный список или кортеж.
167
+ - **Минимальная версия Python**: Библиотека поддерживает Python **3.8+**. Для Python `<3.11` используется обратная совместимость через пакет `tomli`, а на Python `3.11+` — встроенный `tomllib`.
168
+
169
+ ## Лицензия
170
+
171
+ MIT
@@ -0,0 +1,160 @@
1
+ # pytest-fsd
2
+
3
+ **FSD Architecture Validation for Python Projects.**
4
+
5
+ `pytest-fsd` автоматически проверяет архитектуру вашего Python-проекта на соответствие методологии [Feature-Sliced Design](https://feature-sliced.design/).
6
+
7
+ Использует гибридный подход: динамические проверки через [pytest-archon](https://pypi.org/project/pytest-archon/) + статический AST-анализ + проверка файловой структуры.
8
+
9
+ ## Установка
10
+
11
+ ```bash
12
+ pip install pytest-fsd
13
+ # или
14
+ uv add --dev pytest-fsd
15
+ ```
16
+
17
+ ## Использование
18
+
19
+ ### 1. Настройка `pyproject.toml`
20
+
21
+ ```toml
22
+ [tool.pytest_fsd]
23
+ base_path = "src"
24
+ layers = ["app", "windows", "widgets", "features", "entities", "shared"]
25
+ ```
26
+
27
+ ### 2. Создание теста
28
+
29
+ ```python
30
+ # tests/test_architecture.py
31
+ from pytest_fsd import validate_fsd_architecture
32
+
33
+ def test_project_architecture():
34
+ validate_fsd_architecture()
35
+ ```
36
+
37
+ ### 3. Запуск
38
+
39
+ ```bash
40
+ pytest tests/test_architecture.py -vv
41
+ ```
42
+
43
+ ## Архитектура библиотеки
44
+
45
+ Каждое правило — это папка внутри `src/pytest_fsd/rules/<rule_name>/` с:
46
+
47
+ - `__init__.py` — логика проверки (функция `check(config, project_root) -> List[Violation]`)
48
+ - `README.md` — описание правила, примеры, rationale
49
+
50
+ ```
51
+ src/pytest_fsd/
52
+ __init__.py # Фасад: validate_fsd_architecture()
53
+ config.py # Чтение [tool.pytest_fsd] из pyproject.toml
54
+ _lib/ # Общие утилиты
55
+ violations.py # Единый Violation dataclass
56
+ ast_utils.py # AST-парсинг импортов
57
+ fs_utils.py # Файловые утилиты, константы сегментов
58
+ rules/
59
+ forbidden_imports/ # pytest-archon: слои импортируют только нижележащие
60
+ no_cross_imports/ # pytest-archon: слайсы в одном слое независимы
61
+ no_public_api_sidestep/ # AST: импорт только через __init__.py слайса
62
+ no_layer_public_api/ # FS: слоевые папки без __init__.py
63
+ no_ui_in_app/ # AST: запрет UI-фреймворков в app
64
+ repetitive_naming/ # FS: файлы не дублируют имя слайса
65
+ no_segmentless_slices/ # FS: слайс содержит хотя бы один сегмент
66
+ segments_by_purpose/ # FS: запрет utils/helpers/components/hooks
67
+ ambiguous_slice_names/ # FS: имена слайсов ≠ имена сегментов shared
68
+ no_segments_on_sliced_layers/ # FS: в слайсовых слоях не должно быть сегментов напрямую
69
+ public_api/ # FS: каждый слайс должен иметь __init__.py
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Матрица покрытия правил Steiger
75
+
76
+ Полный список правил из [Steiger FSD Plugin](https://github.com/feature-sliced/steiger) и их статус в `pytest-fsd`:
77
+
78
+ | # | Steiger Rule | pytest-fsd Status | Описание |
79
+ | --- | ----------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
80
+ | 1 | `forbidden-imports` / `no-higher-level-imports` | ✅ **Полностью** | Слои импортируют только нижележащие слои |
81
+ | 2 | `no-cross-imports` | ✅ **Полностью** | Слайсы в одном слое независимы друг от друга |
82
+ | 3 | `no-public-api-sidestep` | ✅ **Полностью** | Импорт из чужого слайса только через `__init__.py` |
83
+ | 4 | `public-api` | ✅ **Полностью** | Каждый слайс и сегмент shared обязан иметь `__init__.py` |
84
+ | 5 | `no-layer-public-api` | ✅ **Полностью** | Папки слоев (`features/`, `entities/`) не должны содержать `__init__.py` |
85
+ | 6 | `segments-by-purpose` | ✅ **Полностью** | Запрет `utils`, `helpers`, `hooks`, `components`, `modals`, `types`, `constants` и др. |
86
+ | 7 | `no-segmentless-slices` | ✅ **Полностью** | Слайс обязан содержать хотя бы один стандартный сегмент |
87
+ | 8 | `repetitive-naming` | ✅ **Полностью** | Файлы не дублируют имя слайса (`user/user_model.py` → `user/model.py`) |
88
+ | 9 | `no-ui-in-app` | ✅ **Полностью** | Слой `app` не должен импортировать UI-фреймворки |
89
+ | 10 | `ambiguous-slice-names` | ✅ **Полностью** | Имена слайсов не совпадают с сегментами `shared/` |
90
+ | 11 | `no-segments-on-sliced-layers` | ✅ **Полностью** | В слайсовых слоях нет прямых папок-сегментов |
91
+ | 12 | `inconsistent-naming` | 🔶 **Ruff** | Обеспечивается плагином `N` (pep8-naming) в `Ruff` |
92
+ | 13 | `import-locality` | 🔶 **Ruff** | Обеспечивается плагином `TID` (flake8-tidy-imports) в `Ruff` |
93
+ | 14 | `typo-in-layer-name` | 🔶 **Конфигурация** | Покрывается блоком `[tool.pytest_fsd].layers` в `pyproject.toml` |
94
+ | 15 | `no-processes` | 🔶 **Конфигурация** | Слой `processes` deprecated; просто не включайте его в `layers` |
95
+ | 16 | `excessive-slicing` | ⚡ **Опционально** | Более 20 слайсов в одном слое (порог: 20) |
96
+ | 17 | `insignificant-slice` | 🟡 **Ручная проверка** | Требует анализа графа импортов для определения "незначительных" слайсов |
97
+ | 18 | `no-file-segments` | ⚡ **Опционально** | Сегмент как файл (`model.py`) вместо папки (`model/`) |
98
+ | 19 | `shared-lib-grouping` | ⚡ **Опционально** | Более 15 файлов в `shared/lib` без группировки |
99
+ | 20 | `no-reserved-folder-names` | ⚡ **Опционально** | Подпапки в сегментах не должны совпадать с именами сегментов |
100
+
101
+ ### Легенда
102
+
103
+ | Статус | Значение |
104
+ | -------------------------- | --------------------------------------------------------------------------------------------------- |
105
+ | ✅ **Полностью** | Правило полностью автоматизировано и выполняется при каждом запуске `pytest` |
106
+ | ⚡ **Опционально** | Правило автоматизировано, но включается через `extra_rules` в `pyproject.toml` |
107
+ | 🔶 **Ruff / Конфигурация** | Покрывается внешними инструментами (`Ruff`) или конфигурацией `pyproject.toml` |
108
+ | 🟡 **Ручная проверка** | Требует субъективной оценки или сложного анализа, который лучше производить вручную при code review |
109
+
110
+ ---
111
+
112
+ ## Включение дополнительных правил
113
+
114
+ Добавьте в `pyproject.toml`:
115
+
116
+ ```toml
117
+ [tool.pytest_fsd]
118
+ base_path = "src"
119
+ layers = ["app", "windows", "widgets", "features", "entities", "shared"]
120
+ extra_rules = [
121
+ "excessive-slicing", # ≤ 20 слайсов на слой
122
+ "shared-lib-grouping", # ≤ 15 файлов в shared/lib
123
+ "no-file-segments", # Сегменты должны быть папками, не файлами
124
+ "no-reserved-folder-names" # Подпапки сегментов не могут называться ui/model/api/lib/config
125
+ ]
126
+ ```
127
+
128
+ Каждое правило подробно описано в `src/pytest_fsd/rules/<rule_name>/README.md`.
129
+
130
+ ---
131
+
132
+ ## Настройка Ruff для смежных правил
133
+
134
+ Для полного покрытия FSD-правил, которые Steiger проверяет на уровне линтинга (и которые `pytest-fsd` не дублирует), добавьте в `pyproject.toml`:
135
+
136
+ ```toml
137
+ [tool.ruff.lint]
138
+ select = ["N", "TID"]
139
+
140
+ [tool.ruff.lint.flake8-tidy-imports]
141
+ ban-relative-imports = "parents"
142
+ ```
143
+
144
+ | Ruff Plugin | Steiger Rule | Что проверяет |
145
+ | --------------------------- | --------------------- | ----------------------------------------------- |
146
+ | `N` (pep8-naming) | `inconsistent-naming` | `snake_case` для модулей, переменных, функций |
147
+ | `TID` (flake8-tidy-imports) | `import-locality` | Запрет relative imports из родительских пакетов |
148
+
149
+ Подробные описания и примеры конфигурации: `src/pytest_fsd/rules/inconsistent_naming/README.md` и `src/pytest_fsd/rules/import_locality/README.md`.
150
+
151
+ ## Известные ограничения (Known Limitations)
152
+
153
+ - **`TYPE_CHECKING` импорты**: Правила, использующие `pytest-archon` (например, `forbidden-imports` и `no-cross-imports`), работают на базе динамического анализа графа импортов в рантайме. Импорты, находящиеся внутри блоков `if TYPE_CHECKING:`, не выполняются при загрузке модуля и, следовательно, **не видны для этих правил**.
154
+ - **Относительные импорты**: В модуле `ast_utils.py` добавлена поддержка относительных путей для проверки `no-public-api-sidestep`, однако `Ruff` (плагин `TID`) всё равно лучше справляется с контролем относительных импортов за пределами слайсов.
155
+ - **Динамический `__all__`**: Правило `no-public-api-sidestep` использует статический AST-анализ для извлечения `__all__` из файла `__init__.py`. Если список экспортов формируется динамически (например, `__all__ = a + b`), статический анализатор не сможет его прочитать, и инструмент может выдать ложноположительные нарушения. Экспорты в `__all__` должны быть заданы как явный список или кортеж.
156
+ - **Минимальная версия Python**: Библиотека поддерживает Python **3.8+**. Для Python `<3.11` используется обратная совместимость через пакет `tomli`, а на Python `3.11+` — встроенный `tomllib`.
157
+
158
+ ## Лицензия
159
+
160
+ MIT
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "pytest-fsd"
3
+ version = "0.2.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Lebedev Nikita", email = "rachet337@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.8"
10
+ dependencies = [
11
+ "pytest-archon>=0.0.7",
12
+ "tomli>=2.0.1; python_version < '3.11'",
13
+ ]
14
+
15
+ [dependency-groups]
16
+ dev = [
17
+ "pytest>=8.3.5,<9",
18
+ ]
19
+
20
+ [tool.pytest.ini_options]
21
+ pythonpath = ["src"]
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.9.27,<0.10.0"]
25
+ build-backend = "uv_build"
@@ -0,0 +1,84 @@
1
+ # src/pytest_fsd/__init__.py
2
+ """
3
+ pytest-fsd: FSD Architecture Validation for Python Projects.
4
+
5
+ Provides validate_fsd_architecture() — a single facade function
6
+ that runs all FSD rules configured in pyproject.toml.
7
+ """
8
+
9
+ from typing import List
10
+
11
+ from ._lib.violations import Violation
12
+ from .config import load_config
13
+ from .rules import (
14
+ ambiguous_slice_names,
15
+ forbidden_imports,
16
+ no_cross_imports,
17
+ no_layer_public_api,
18
+ no_public_api_sidestep,
19
+ no_segmentless_slices,
20
+ no_segments_on_sliced_layers,
21
+ no_ui_in_app,
22
+ public_api,
23
+ repetitive_naming,
24
+ segments_by_purpose,
25
+ unknown_folders_in_root,
26
+ )
27
+ from .rules import (
28
+ excessive_slicing,
29
+ no_file_segments,
30
+ no_reserved_folder_names,
31
+ shared_lib_grouping,
32
+ )
33
+
34
+ # Базовые правила — всегда активны
35
+ CORE_RULES = [
36
+ forbidden_imports,
37
+ no_cross_imports,
38
+ no_public_api_sidestep,
39
+ no_layer_public_api,
40
+ no_ui_in_app,
41
+ repetitive_naming,
42
+ no_segmentless_slices,
43
+ segments_by_purpose,
44
+ ambiguous_slice_names,
45
+ no_segments_on_sliced_layers,
46
+ public_api,
47
+ unknown_folders_in_root,
48
+ ]
49
+
50
+ # Дополнительные правила — включаются через extra_rules в pyproject.toml
51
+ EXTRA_RULES = {
52
+ "excessive-slicing": excessive_slicing,
53
+ "shared-lib-grouping": shared_lib_grouping,
54
+ "no-file-segments": no_file_segments,
55
+ "no-reserved-folder-names": no_reserved_folder_names,
56
+ }
57
+
58
+
59
+ def validate_fsd_architecture(project_root: str = ".") -> None:
60
+ """
61
+ Validate the project architecture against FSD rules.
62
+
63
+ Reads configuration from pyproject.toml -> [tool.pytest_fsd].
64
+ Runs all core rules + any extra_rules specified in config.
65
+ Raises AssertionError if any violations are found.
66
+ """
67
+ config = load_config(project_root)
68
+ all_violations: List[Violation] = []
69
+
70
+ # Запускаем базовые правила
71
+ for rule_module in CORE_RULES:
72
+ all_violations.extend(rule_module.check(config, project_root))
73
+
74
+ # Запускаем дополнительные правила, если они включены в конфиге
75
+ for extra_rule_name in config.extra_rules:
76
+ rule_module = EXTRA_RULES.get(extra_rule_name)
77
+ if rule_module is not None:
78
+ all_violations.extend(rule_module.check(config, project_root))
79
+
80
+ if all_violations:
81
+ error_msg = "FSD Architecture Validation failed:\n"
82
+ for v in all_violations:
83
+ error_msg += v.format() + "\n"
84
+ raise AssertionError(error_msg)
@@ -0,0 +1 @@
1
+ """Shared utilities for pytest-fsd rules."""
@@ -0,0 +1,54 @@
1
+ # src/pytest_fsd/_lib/ast_utils.py
2
+ """Shared AST parsing utilities used by multiple rules."""
3
+
4
+ import ast
5
+ from typing import List, Tuple, Optional
6
+
7
+
8
+ def get_imports_from_file(
9
+ filepath: str,
10
+ ) -> List[Tuple[int, Optional[str], int, List[str]]]:
11
+ """Return list of (line_number, full_module_path, level, imported_names).
12
+
13
+ Level represents relative imports:
14
+ 0 = absolute import (e.g. `import foo` or `from foo import bar`)
15
+ 1 = current directory (`from . import bar` -> module=None, level=1)
16
+ 2 = parent directory (`from ..foo import bar` -> module='foo', level=2)
17
+ """
18
+ imports = []
19
+ try:
20
+ with open(filepath, "r", encoding="utf-8") as f:
21
+ tree = ast.parse(f.read(), filename=filepath)
22
+ except Exception:
23
+ return []
24
+
25
+ for node in ast.walk(tree):
26
+ if isinstance(node, ast.Import):
27
+ for alias in node.names:
28
+ imports.append((node.lineno, alias.name, 0, [alias.name]))
29
+ elif isinstance(node, ast.ImportFrom):
30
+ names = [alias.name for alias in node.names]
31
+ imports.append((node.lineno, node.module, node.level, names))
32
+
33
+ return imports
34
+
35
+
36
+ def get_exported_names(filepath: str) -> Optional[List[str]]:
37
+ """Return the list of names exported in __all__ if defined, or None if not defined."""
38
+ try:
39
+ with open(filepath, "r", encoding="utf-8") as f:
40
+ tree = ast.parse(f.read(), filename=filepath)
41
+ except Exception:
42
+ return None
43
+
44
+ for node in tree.body:
45
+ if isinstance(node, ast.Assign):
46
+ for target in node.targets:
47
+ if isinstance(target, ast.Name) and target.id == "__all__":
48
+ if isinstance(node.value, (ast.List, ast.Tuple)):
49
+ return [
50
+ elt.value
51
+ for elt in node.value.elts
52
+ if getattr(elt, "value", None) is not None
53
+ ]
54
+ return None
@@ -0,0 +1,31 @@
1
+ # src/pytest_fsd/_lib/fs_utils.py
2
+ """Shared filesystem utilities used by multiple rules."""
3
+
4
+ import os
5
+ from typing import List
6
+
7
+ from ..config import FsdConfig
8
+
9
+ # Стандартные сегменты FSD для Python-проектов
10
+ STANDARD_SEGMENTS = ("ui", "model", "api", "lib", "config")
11
+
12
+
13
+ def get_sliced_layers(config: FsdConfig) -> List[str]:
14
+ """Return layers that contain slices (excluding app and shared)."""
15
+ return [
16
+ layer_name
17
+ for layer_name in config.layers
18
+ if layer_name not in ("app", "shared")
19
+ ]
20
+
21
+
22
+ def get_slices(layer: str, base_path: str) -> List[str]:
23
+ """Return list of slice directory names inside a given layer."""
24
+ path = os.path.join(base_path, layer)
25
+ if not os.path.isdir(path):
26
+ return []
27
+ return [
28
+ d
29
+ for d in os.listdir(path)
30
+ if os.path.isdir(os.path.join(path, d)) and not d.startswith("_")
31
+ ]
@@ -0,0 +1,22 @@
1
+ # src/pytest_fsd/_lib/violations.py
2
+ """Unified violation dataclass for all FSD rules."""
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass
9
+ class Violation:
10
+ """Represents a single FSD architecture violation."""
11
+
12
+ rule: str
13
+ file_path: str
14
+ message: str
15
+ line_number: Optional[int] = None
16
+
17
+ def format(self) -> str:
18
+ """Format the violation as a human-readable string."""
19
+ location = self.file_path
20
+ if self.line_number is not None:
21
+ location += f":{self.line_number}"
22
+ return f" - [{self.rule}] {location} -> {self.message}"
@@ -0,0 +1,62 @@
1
+ import os
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import List
5
+
6
+ try:
7
+ import tomllib
8
+ except ModuleNotFoundError:
9
+ import tomli as tomllib
10
+
11
+
12
+ @dataclass
13
+ class FsdConfig:
14
+ """Configuration for pytest-fsd, read from [tool.pytest_fsd] in pyproject.toml."""
15
+
16
+ base_path: str = "src"
17
+ layers: List[str] = field(
18
+ default_factory=lambda: [
19
+ "app",
20
+ "pages",
21
+ "widgets",
22
+ "features",
23
+ "entities",
24
+ "shared",
25
+ ]
26
+ )
27
+ ignore_paths: List[str] = field(default_factory=list)
28
+ extra_rules: List[str] = field(default_factory=list)
29
+
30
+
31
+ # Доступные дополнительные правила
32
+ AVAILABLE_EXTRA_RULES = {
33
+ "excessive-slicing",
34
+ "shared-lib-grouping",
35
+ "no-file-segments",
36
+ "no-reserved-folder-names",
37
+ }
38
+
39
+
40
+ def load_config(root_dir: str) -> FsdConfig:
41
+ """Загружает настройки [tool.pytest_fsd] из pyproject.toml."""
42
+ pyproject_path = os.path.join(root_dir, "pyproject.toml")
43
+
44
+ if not os.path.exists(pyproject_path):
45
+ return FsdConfig()
46
+
47
+ with open(pyproject_path, "rb") as f:
48
+ data = tomllib.load(f)
49
+
50
+ tool_config = data.get("tool", {}).get("pytest_fsd", {})
51
+
52
+ config = FsdConfig()
53
+ if "base_path" in tool_config:
54
+ config.base_path = tool_config["base_path"]
55
+ if "layers" in tool_config:
56
+ config.layers = tool_config["layers"]
57
+ if "ignore_paths" in tool_config:
58
+ config.ignore_paths = tool_config["ignore_paths"]
59
+ if "extra_rules" in tool_config:
60
+ config.extra_rules = tool_config["extra_rules"]
61
+
62
+ return config
File without changes
@@ -0,0 +1 @@
1
+ """Rules registry for pytest-fsd."""
@@ -0,0 +1,29 @@
1
+ # `ambiguous-slice-names`
2
+
3
+ Forbid slice names that match segment names in the `shared` layer. For example, if `shared/i18n` exists, having `features/i18n` is confusing.
4
+
5
+ ## Examples
6
+
7
+ ✅ Pass:
8
+
9
+ ```
10
+ 📂 shared/
11
+ 📂 ui/
12
+ 📂 i18n/
13
+ 📂 features/
14
+ 📂 auth/ # OK: "auth" is not a shared segment
15
+ ```
16
+
17
+ ❌ Fail:
18
+
19
+ ```
20
+ 📂 shared/
21
+ 📂 ui/
22
+ 📂 i18n/
23
+ 📂 features/
24
+ 📂 i18n/ # ❌ same name as shared/i18n
25
+ ```
26
+
27
+ ## Rationale
28
+
29
+ When there is a segment in `shared` with the same name as a slice in another layer, it becomes ambiguous where new code should be written and obscures the search for existing code.