Feature-Sliced Design (FSD) architecture validation plugin for pytest
Project description
pytest-fsd
FSD Architecture Validation for Python Projects.
pytest-fsd automatically checks your Python project's architecture for compliance with the Feature-Sliced Design methodology.
It uses a hybrid approach: dynamic checks via pytest-archon + static AST analysis + file structure verification.
Installation
pip install pytest-fsd
# or
uv add --dev pytest-fsd
Usage
1. Configure pyproject.toml
[tool.pytest_fsd]
base_path = "src"
layers = ["app", "windows", "widgets", "features", "entities", "shared"]
2. Create a test
# tests/test_architecture.py
from pytest_fsd import validate_fsd_architecture
def test_project_architecture():
validate_fsd_architecture()
3. Run
pytest tests/test_architecture.py -vv
Library Architecture
Each rule is a folder inside src/pytest_fsd/rules/<rule_name>/ containing:
__init__.py— verification logic (the functioncheck(config, project_root) -> List[Violation])README.md— rule description, examples, and rationale
src/pytest_fsd/
__init__.py # Facade: validate_fsd_architecture()
config.py # Reads [tool.pytest_fsd] from pyproject.toml
_lib/ # Shared utilities
violations.py # Unified Violation dataclass
ast_utils.py # AST parsing for imports
fs_utils.py # File utilities, segment constants
rules/
forbidden_imports/ # pytest-archon: layers only import from layers below
no_cross_imports/ # pytest-archon: slices within the same layer are independent
no_public_api_sidestep/ # AST: importing from another slice is only allowed via its __init__.py
no_layer_public_api/ # FS: layer folders must not contain __init__.py
no_ui_in_app/ # AST: forbids importing UI frameworks directly into app
repetitive_naming/ # FS: files do not duplicate the slice name
no_segmentless_slices/ # FS: a slice must contain at least one standard segment
segments_by_purpose/ # FS: forbids utils/helpers/components/hooks
ambiguous_slice_names/ # FS: slice names must not match segment names in shared
no_segments_on_sliced_layers/ # FS: sliced layers must not contain segments directly
public_api/ # FS: every slice must have an __init__.py
Steiger Rules Coverage Matrix
Full list of rules from the Steiger FSD Plugin and their status in pytest-fsd:
| # | Steiger Rule | pytest-fsd Status | Description |
|---|---|---|---|
| 1 | forbidden-imports / no-higher-level-imports |
✅ Complete | Layers only import from layers below them |
| 2 | no-cross-imports |
✅ Complete | Slices within the same layer are independent of each other |
| 3 | no-public-api-sidestep |
✅ Complete | Importing from another slice is only allowed via __init__.py |
| 4 | public-api |
✅ Complete | Every slice and shared segment must have an __init__.py |
| 5 | no-layer-public-api |
✅ Complete | Layer folders (features/, entities/) must not contain __init__.py |
| 6 | segments-by-purpose |
✅ Complete | Forbids utils, helpers, hooks, components, modals, types, constants, etc. |
| 7 | no-segmentless-slices |
✅ Complete | A slice must contain at least one standard segment |
| 8 | repetitive-naming |
✅ Complete | Files do not duplicate the slice name (user/user_model.py → user/model.py) |
| 9 | no-ui-in-app |
✅ Complete | The app layer must not import UI frameworks |
| 10 | ambiguous-slice-names |
✅ Complete | Slice names must not match segment names in shared/ |
| 11 | no-segments-on-sliced-layers |
✅ Complete | Sliced layers do not have direct segment folders |
| 12 | inconsistent-naming |
🔶 Ruff | Enforced by the N (pep8-naming) plugin in Ruff |
| 13 | import-locality |
🔶 Ruff | Enforced by the TID (flake8-tidy-imports) plugin in Ruff |
| 14 | typo-in-layer-name |
✅ Complete | Forbids unknown layer folders (e.g., fietures instead of features) in the root |
| 15 | no-processes |
🔶 Configuration | The processes layer is deprecated; simply do not include it in layers |
| 16 | excessive-slicing |
⚡ Optional | More than 20 slices in one layer (threshold: 20) |
| 17 | insignificant-slice |
🟡 Manual check | Requires import graph analysis to determine "insignificant" slices |
| 18 | no-file-segments |
⚡ Optional | A segment as a file (model.py) instead of a folder (model/) |
| 19 | shared-lib-grouping |
⚡ Optional | More than 15 ungrouped files in shared/lib |
| 20 | no-reserved-folder-names |
⚡ Optional | Subfolders in segments must not match segment names |
Legend
| Status | Meaning |
|---|---|
| ✅ Complete | The rule is fully automated and runs every time pytest is executed |
| ⚡ Optional | The rule is automated but is enabled via extra_rules in pyproject.toml |
| 🔶 Ruff / Configuration | Covered by external tools (Ruff) or pyproject.toml configuration |
| 🟡 Manual check | Requires subjective evaluation or complex analysis better suited for manual code review |
Enabling Optional Rules
Add to pyproject.toml:
[tool.pytest_fsd]
base_path = "src"
layers = ["app", "windows", "widgets", "features", "entities", "shared"]
extra_rules = [
"excessive-slicing", # ≤ 20 slices per layer
"shared-lib-grouping", # ≤ 15 files in shared/lib
"no-file-segments", # Segments must be folders, not files
"no-reserved-folder-names" # Segment subfolders cannot be named ui/model/api/lib/config
]
Each rule is described in detail in src/pytest_fsd/rules/<rule_name>/README.md.
Configuring Ruff for Related Rules
For full coverage of FSD rules that Steiger checks at the linting level (which pytest-fsd does not duplicate), add to pyproject.toml:
[tool.ruff.lint]
select = ["N", "TID"]
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "parents"
| Ruff Plugin | Steiger Rule | What it checks |
|---|---|---|
N (pep8-naming) |
inconsistent-naming |
snake_case for modules, variables, functions |
TID (flake8-tidy-imports) |
import-locality |
Forbids relative imports from parent packages |
Detailed descriptions and configuration examples: src/pytest_fsd/rules/inconsistent_naming/README.md and src/pytest_fsd/rules/import_locality/README.md.
Known Limitations
TYPE_CHECKINGimports: Rules usingpytest-archon(e.g.,forbidden-importsandno-cross-imports) work based on dynamic import graph analysis at runtime. Imports insideif TYPE_CHECKING:blocks are not executed when the module loads and are therefore not visible to these rules.- Relative imports: The
ast_utils.pymodule supports relative paths for checkingno-public-api-sidestep, however,Ruff(theTIDplugin) is still better at controlling relative imports outside of slices. - Dynamic
__all__: Theno-public-api-sidesteprule uses static AST analysis to extract__all__from the__init__.pyfile. If the export list is formed dynamically (e.g.,__all__ = a + b), the static analyzer will not be able to read it, and the tool may yield false positive violations. Exports in__all__must be defined as an explicit list or tuple. - Minimum Python Version: The library supports Python 3.8+. For Python
<3.11, backward compatibility is provided via thetomlipackage, and on Python3.11+the built-intomllibis used.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_fsd-0.2.1.tar.gz.
File metadata
- Download URL: pytest_fsd-0.2.1.tar.gz
- Upload date:
- Size: 22.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05fbbe1a7492bc17b920c3f990e5eeb2b88567f26aaeaf4a08079e11c7aaf6db
|
|
| MD5 |
24a4d391af9d8875b1621960cd6299a0
|
|
| BLAKE2b-256 |
6740897c0281698662259dd32a9c02362bffff0d2ea44689f9c1812d300c0256
|
File details
Details for the file pytest_fsd-0.2.1-py3-none-any.whl.
File metadata
- Download URL: pytest_fsd-0.2.1-py3-none-any.whl
- Upload date:
- Size: 44.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f8fd5d1824312c6e9cc80bf81203aad906e433a7591a9169a69c04bd2a858e6
|
|
| MD5 |
c4841eaa8bbf31c8aa1e916a6538b0d4
|
|
| BLAKE2b-256 |
3978ee46b45f2ceecfbaa0bf94a1a97d489c380e6ca97b5993104680eecb6e78
|