Home
A filesystem structure linter for Python projects. Declare rules about how your source tree is laid out and enforce them with a single CLI command.
What It Does¶
- Require directories to exist, restrict which components may live where, mirror tests against source, and bound how many symbols a module defines
- Scope each rule to specific parts of the tree with path patterns
- Integrate into CI or pre-commit to keep your project structure consistent
For Python developers who want to enforce team-specific structural conventions that names and imports can't express. psl never inspects imports or names — only the shape of the tree and the population of each module.
Key Features¶
| Feature | Description |
|---|---|
| Rule Types | directory-exists, allowed-children, mirror, and symbol-count |
| Path Patterns | Scope rules with literal, *, **, and {name} segments |
| Include/Exclude | Restrict the scanned roots and drop paths with glob patterns |
| Inline Ignore | Suppress a file's symbol-count checks with # psl: ignore |
| Pre-commit | Drop-in integration with pre-commit hooks |
Quick Start¶
Install:
pip install python-structure-linter
Create .python-structure-linter.yaml in your project root:
include: [src]
exclude: ["**/__pycache__/**"]
rules:
- name: context-layers
type: directory-exists
description: Each context has domain, application, and adapters layers
within: src/contexts/{ctx}
require: [domain, application, adapters]
- name: one-class-per-domain-module
type: symbol-count
description: Domain modules define a single class
within: src/contexts/{ctx}/domain/**
symbol: class
max: 1
ignore_files: [__init__.py]
Run:
psl check
Output:
$ psl check
src/contexts/orders
[context-layers] Each context has domain, application, and adapters layers
missing required directory 'adapters'
src/contexts/orders/domain/entities/order.py:14
[one-class-per-domain-module] Domain modules define a single class
2 top-level class definitions found, at most 1 allowed
Found 2 violation(s).
More Examples¶
Mirror Tests Against Source¶
Require every test directory to have a matching source directory, so the tests tree invents no structure of its own:
rules:
- name: tests-mirror-src
type: mirror
description: The tests tree mirrors the src tree
source: src
mirror: tests
direction: mirror-to-source
mirror_extra_allowed: [doubles, fixtures, conftest]
This catches tests/contexts/orders/domain/entities when src/contexts/orders/domain/entities does not exist — the test tree drifted from the code it covers. Scaffolding directories like doubles, fixtures, and conftest are exempt.
Constrain a Module's Public Surface¶
Count only names that don't start with an underscore, so you can require exactly one public function per module while leaving private helpers unrestricted:
rules:
- name: one-public-function-per-checker
type: symbol-count
description: Each checker module exposes exactly one public function
within: src/checks/**
symbol: function
visibility: public
min: 1
max: 1
ignore_files: [__init__.py]
This catches a checker module that exports two public functions, or none, while _normalize and _walk beside them stay uncounted.