Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python_structure_linter-0.1.0.tar.gz (74.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_structure_linter-0.1.0-py3-none-any.whl (18.9 kB view details)

Uploaded Python 3

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

Hashes for python_structure_linter-0.1.0.tar.gz
Algorithm Hash digest
SHA256 176237b34d72526448eb523ccd5e54a2c66d2e0ab3793aaf689953a7ace64876
MD5 544aafa343a496ec7140eb8543d6fef9
BLAKE2b-256 8a20244db31c099601d727250496abe6c0c880fe99dabb15839898a38da429c0

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_structure_linter-0.1.0.tar.gz:

Publisher: publish.yaml on heumsi/python-structure-linter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file python_structure_linter-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for python_structure_linter-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c4fe672d563108c2816832fe907cc052a12e84fa69d81d9a8daa40c05c1fefd6
MD5 2800eedd63e581ce1d878c5bc2049699
BLAKE2b-256 8bc10a20089af20b0cbcfddf40a5980f287b6127378f14ddaff546a93b88c542

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page