Metadata-Version: 2.4
Name: python-structure-linter
Version: 0.1.0
Summary: A filesystem structure and symbol-placement linter for Python projects
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Requires-Dist: click>=8.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: tomli>=2.0; python_version < '3.11'
Provides-Extra: dev
Requires-Dist: pre-commit>=3.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: ty>=0.0.26; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-shadcn; extra == 'docs'
Requires-Dist: mkdocs>=1.6; extra == 'docs'
Description-Content-Type: text/markdown

# 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](https://github.com/heumsi/python-dependency-linter) (import direction) and [python-naming-linter](https://github.com/heumsi/python-naming-linter) (identifier names). It never inspects imports or names — only the shape of the tree and the population of each module.

## Installation

```bash
pip install python-structure-linter
```

Or with uv:

```bash
uv add python-structure-linter
```

## Quick Start

Create `.python-structure-linter.yaml` in your project root:

```yaml
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:

```bash
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.

```yaml
- 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.

```yaml
- 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.

```yaml
- 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.

```yaml
- 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:

```python
# 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

```yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/heumsi/python-structure-linter
    rev: v0.1.0
    hooks:
      - id: python-structure-linter
```

## License

MIT
