A filesystem structure and symbol-placement linter for Python projects
Project description
python-structure-linter
A filesystem structure and symbol-placement linter for Python projects. Declare rules about how your source tree is laid out — which directories must exist, which components may live where, whether tests mirror source, how many symbols a module may define — and catch violations at commit time.
psl is the structural sibling of python-dependency-linter (import direction) and python-naming-linter (identifier names). It never inspects imports or names — only the shape of the tree and the population of each module.
Installation
pip install python-structure-linter
Or with uv:
uv add python-structure-linter
Quick Start
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
Rule Types
Each rule declares a type (the primitive) plus type-specific options. The engine
is neutral — your project's taxonomy (layer names, component directories) lives
entirely in the config values, never in the rule vocabulary.
directory-exists
Every directory matching within must contain the require directories.
- name: context-layers
type: directory-exists
within: src/contexts/{ctx}
require: [domain, application, adapters]
allowed-children
A scope directory may only contain the allowed child names. With
group_level: optional, a child directory that is not itself an allowed name is
treated as a one-level grouping tier (e.g. a subdomain) and passes only if all of
its own children are allowed.
- name: domain-components
type: allowed-children
within: src/contexts/{ctx}/domain
kind: dir # dir (default) | file | any
group_level: optional # none (default) | optional
allow: [entities, value_objects, events, services, errors]
ignore: [__init__.py]
mirror
The source and mirror trees must mirror each other at directory granularity.
direction selects which way the requirement runs:
mirror-to-source(default): every mirror directory must have a source counterpart, so the mirror tree invents no structure of its own (a test's location is determined by the source it covers).source-to-mirror: every source directory must have a mirror counterpart.both: both requirements hold.
mirror_extra_allowed exempts mirror-side directories (e.g. test scaffolding)
from the mirror-to-source check; source_extra_allowed exempts source-side
directories from the source-to-mirror check.
- name: tests-mirror-src
type: mirror
source: src
mirror: tests
direction: mirror-to-source
granularity: directory
ignore: [__pycache__]
mirror_extra_allowed: [doubles, fixtures, conftest]
symbol-count
Bound the number of top-level symbols (classes or functions) a module may define.
Nested definitions are not counted. visibility: public counts only names that
do not start with an underscore, so you can constrain a module's public surface
(e.g. exactly one public function) while leaving private helpers unrestricted.
- name: one-error-per-file
type: symbol-count
within: src/contexts/{ctx}/domain/**/errors/**
symbol: class
max: 1
ignore_files: [__init__.py]
- name: one-public-function-per-checker
type: symbol-count
within: src/checks/**
symbol: function
visibility: public # any (default) | public
min: 1
max: 1
Path Patterns
within, and the tree roots in mirror, are project-root-relative paths whose
segments may be:
| Token | Matches |
|---|---|
| literal | a segment with that exact name |
* |
exactly one segment |
** |
zero or more segments |
{name} |
one segment, captured as name |
File-targeting rules (symbol-count) match .py files; the others match
directories.
Inline Ignore
File-based rules honor an inline ignore comment:
# psl: ignore[one-error-per-file]
Use # psl: ignore (no brackets) to skip every rule for that file.
Include / Exclude
include restricts the scanned roots; exclude drops matching paths. Both take
project-root-relative glob patterns.
Configuration File
psl reads .python-structure-linter.yaml, or [tool.python-structure-linter]
in pyproject.toml, searching upward from the current directory.
Pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/heumsi/python-structure-linter
rev: v0.1.0
hooks:
- id: python-structure-linter
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 python_structure_linter-0.1.0.tar.gz.
File metadata
- Download URL: python_structure_linter-0.1.0.tar.gz
- Upload date:
- Size: 74.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
176237b34d72526448eb523ccd5e54a2c66d2e0ab3793aaf689953a7ace64876
|
|
| MD5 |
544aafa343a496ec7140eb8543d6fef9
|
|
| BLAKE2b-256 |
8a20244db31c099601d727250496abe6c0c880fe99dabb15839898a38da429c0
|
Provenance
The following attestation bundles were made for python_structure_linter-0.1.0.tar.gz:
Publisher:
publish.yaml on heumsi/python-structure-linter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_structure_linter-0.1.0.tar.gz -
Subject digest:
176237b34d72526448eb523ccd5e54a2c66d2e0ab3793aaf689953a7ace64876 - Sigstore transparency entry: 2189546549
- Sigstore integration time:
-
Permalink:
heumsi/python-structure-linter@95b18010a05315c2f97f9c3fd549e2b935487773 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/heumsi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@95b18010a05315c2f97f9c3fd549e2b935487773 -
Trigger Event:
push
-
Statement type:
File details
Details for the file python_structure_linter-0.1.0-py3-none-any.whl.
File metadata
- Download URL: python_structure_linter-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4fe672d563108c2816832fe907cc052a12e84fa69d81d9a8daa40c05c1fefd6
|
|
| MD5 |
2800eedd63e581ce1d878c5bc2049699
|
|
| BLAKE2b-256 |
8bc10a20089af20b0cbcfddf40a5980f287b6127378f14ddaff546a93b88c542
|
Provenance
The following attestation bundles were made for python_structure_linter-0.1.0-py3-none-any.whl:
Publisher:
publish.yaml on heumsi/python-structure-linter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_structure_linter-0.1.0-py3-none-any.whl -
Subject digest:
c4fe672d563108c2816832fe907cc052a12e84fa69d81d9a8daa40c05c1fefd6 - Sigstore transparency entry: 2189546612
- Sigstore integration time:
-
Permalink:
heumsi/python-structure-linter@95b18010a05315c2f97f9c3fd549e2b935487773 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/heumsi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@95b18010a05315c2f97f9c3fd549e2b935487773 -
Trigger Event:
push
-
Statement type: