python-dependency-linter 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_dependency_linter-0.2.0/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- python_dependency_linter-0.2.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- python_dependency_linter-0.2.0/.github/ISSUE_TEMPLATE/feature_request.yml +37 -0
- python_dependency_linter-0.2.0/.github/pull_request_template.md +13 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.github/workflows/ci.yaml +8 -4
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.github/workflows/publish.yaml +4 -4
- python_dependency_linter-0.2.0/CHANGELOG.md +5 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/PKG-INFO +121 -2
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/README.md +120 -1
- python_dependency_linter-0.2.0/pyproject.toml +95 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/matcher.py +19 -7
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_matcher.py +32 -0
- python_dependency_linter-0.1.0/cliff.toml +0 -38
- python_dependency_linter-0.1.0/docs/superpowers/plans/2026-03-30-python-dependency-linter.md +0 -1343
- python_dependency_linter-0.1.0/docs/superpowers/specs/2026-03-30-python-dependency-linter-design.md +0 -235
- python_dependency_linter-0.1.0/pyproject.toml +0 -49
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.github/dependabot.yml +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.gitignore +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.pre-commit-config.yaml +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.pre-commit-hooks.yaml +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/LICENSE +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/checker.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/cli.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/config.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/parser.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/reporter.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/resolver.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_config.yaml +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/application/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/application/service.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/domain/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/domain/models.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/adapters/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/adapters/repository.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/application/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/application/service.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/domain/__init__.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/domain/models.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_pyproject.toml +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_checker.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_cli.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_config.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_parser.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_reporter.py +0 -0
- {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_resolver.py +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: Report a bug or unexpected behavior
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for reporting a bug! Please fill out the sections below.
|
|
9
|
+
|
|
10
|
+
- type: textarea
|
|
11
|
+
id: description
|
|
12
|
+
attributes:
|
|
13
|
+
label: Description
|
|
14
|
+
description: A clear description of what the bug is.
|
|
15
|
+
validations:
|
|
16
|
+
required: true
|
|
17
|
+
|
|
18
|
+
- type: textarea
|
|
19
|
+
id: reproduce
|
|
20
|
+
attributes:
|
|
21
|
+
label: Steps to reproduce
|
|
22
|
+
description: Steps to reproduce the behavior.
|
|
23
|
+
placeholder: |
|
|
24
|
+
1. Create a config file with ...
|
|
25
|
+
2. Run `pdl check`
|
|
26
|
+
3. See error ...
|
|
27
|
+
validations:
|
|
28
|
+
required: true
|
|
29
|
+
|
|
30
|
+
- type: textarea
|
|
31
|
+
id: expected
|
|
32
|
+
attributes:
|
|
33
|
+
label: Expected behavior
|
|
34
|
+
description: What you expected to happen.
|
|
35
|
+
validations:
|
|
36
|
+
required: true
|
|
37
|
+
|
|
38
|
+
- type: textarea
|
|
39
|
+
id: config
|
|
40
|
+
attributes:
|
|
41
|
+
label: Configuration
|
|
42
|
+
description: Your `.python-dependency-linter.yaml` (remove sensitive info).
|
|
43
|
+
render: yaml
|
|
44
|
+
|
|
45
|
+
- type: textarea
|
|
46
|
+
id: environment
|
|
47
|
+
attributes:
|
|
48
|
+
label: Environment
|
|
49
|
+
description: Please provide the following info.
|
|
50
|
+
value: |
|
|
51
|
+
- OS:
|
|
52
|
+
- Python version:
|
|
53
|
+
- python-dependency-linter version:
|
|
54
|
+
validations:
|
|
55
|
+
required: true
|
|
56
|
+
|
|
57
|
+
- type: textarea
|
|
58
|
+
id: logs
|
|
59
|
+
attributes:
|
|
60
|
+
label: Error output / logs
|
|
61
|
+
description: Paste any relevant CLI output.
|
|
62
|
+
render: shell
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Feature Request
|
|
2
|
+
description: Suggest a new feature or improvement
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for suggesting a feature! Please describe your idea below.
|
|
9
|
+
|
|
10
|
+
- type: textarea
|
|
11
|
+
id: problem
|
|
12
|
+
attributes:
|
|
13
|
+
label: Problem
|
|
14
|
+
description: What problem does this feature solve? What's missing?
|
|
15
|
+
placeholder: "e.g., I want to allow wildcard patterns in third_party rules, but currently ..."
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: solution
|
|
21
|
+
attributes:
|
|
22
|
+
label: Proposed solution
|
|
23
|
+
description: How do you think this should work?
|
|
24
|
+
validations:
|
|
25
|
+
required: true
|
|
26
|
+
|
|
27
|
+
- type: textarea
|
|
28
|
+
id: alternatives
|
|
29
|
+
attributes:
|
|
30
|
+
label: Alternatives considered
|
|
31
|
+
description: Any other approaches you've thought about.
|
|
32
|
+
|
|
33
|
+
- type: textarea
|
|
34
|
+
id: context
|
|
35
|
+
attributes:
|
|
36
|
+
label: Additional context
|
|
37
|
+
description: Any other context, examples, or config snippets.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## What does this PR do?
|
|
2
|
+
|
|
3
|
+
<!-- A brief description of the change. -->
|
|
4
|
+
|
|
5
|
+
## Related issue
|
|
6
|
+
|
|
7
|
+
<!-- Link to the issue this PR addresses, e.g., Closes #123 -->
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
11
|
+
- [ ] Tests added / updated
|
|
12
|
+
- [ ] `pdl check` runs without errors on the example config
|
|
13
|
+
- [ ] Documentation updated (if applicable)
|
|
@@ -3,14 +3,18 @@ name: CI
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
5
5
|
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "python_dependency_linter/**"
|
|
8
|
+
- "tests/**"
|
|
9
|
+
- "pyproject.toml"
|
|
6
10
|
|
|
7
11
|
jobs:
|
|
8
12
|
lint:
|
|
9
13
|
runs-on: ubuntu-latest
|
|
10
14
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@v6
|
|
12
16
|
- uses: astral-sh/setup-uv@v4
|
|
13
|
-
- uses: actions/setup-python@
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
14
18
|
with:
|
|
15
19
|
python-version: "3.13"
|
|
16
20
|
- run: uv pip install --system -e ".[dev]"
|
|
@@ -23,9 +27,9 @@ jobs:
|
|
|
23
27
|
matrix:
|
|
24
28
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
25
29
|
steps:
|
|
26
|
-
- uses: actions/checkout@
|
|
30
|
+
- uses: actions/checkout@v6
|
|
27
31
|
- uses: astral-sh/setup-uv@v4
|
|
28
|
-
- uses: actions/setup-python@
|
|
32
|
+
- uses: actions/setup-python@v6
|
|
29
33
|
with:
|
|
30
34
|
python-version: ${{ matrix.python-version }}
|
|
31
35
|
- run: uv pip install --system -e ".[dev]"
|
{python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.github/workflows/publish.yaml
RENAMED
|
@@ -11,13 +11,13 @@ jobs:
|
|
|
11
11
|
permissions:
|
|
12
12
|
contents: write
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
15
|
with:
|
|
16
16
|
fetch-depth: 0
|
|
17
17
|
- name: Generate full changelog
|
|
18
18
|
uses: orhun/git-cliff-action@v4
|
|
19
19
|
with:
|
|
20
|
-
config:
|
|
20
|
+
config: pyproject.toml
|
|
21
21
|
args: --verbose
|
|
22
22
|
env:
|
|
23
23
|
OUTPUT: CHANGELOG.md
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
id: release_notes
|
|
34
34
|
uses: orhun/git-cliff-action@v4
|
|
35
35
|
with:
|
|
36
|
-
config:
|
|
36
|
+
config: pyproject.toml
|
|
37
37
|
args: --verbose --latest --strip header
|
|
38
38
|
env:
|
|
39
39
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -50,7 +50,7 @@ jobs:
|
|
|
50
50
|
permissions:
|
|
51
51
|
id-token: write
|
|
52
52
|
steps:
|
|
53
|
-
- uses: actions/checkout@
|
|
53
|
+
- uses: actions/checkout@v6
|
|
54
54
|
- uses: actions/setup-python@v5
|
|
55
55
|
with:
|
|
56
56
|
python-version: "3.13"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-dependency-linter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A dependency linter for Python projects
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -27,6 +27,14 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
|
|
28
28
|
A dependency linter for Python projects. Define rules for which modules can depend on what, and catch violations.
|
|
29
29
|
|
|
30
|
+
## What It Does
|
|
31
|
+
|
|
32
|
+
- Define dependency rules between modules using a simple YAML or TOML config
|
|
33
|
+
- Detect imports that violate your rules with a single CLI command
|
|
34
|
+
- Integrate into CI or pre-commit to keep your architecture consistent
|
|
35
|
+
|
|
36
|
+
For Python developers who care about module boundaries and dependency direction — whether you're applying Layered, Hexagonal, Clean Architecture, or your own conventions.
|
|
37
|
+
|
|
30
38
|
## Installation
|
|
31
39
|
|
|
32
40
|
```bash
|
|
@@ -80,6 +88,64 @@ contexts/boards/domain/models.py:9
|
|
|
80
88
|
Found 2 violation(s).
|
|
81
89
|
```
|
|
82
90
|
|
|
91
|
+
## Examples
|
|
92
|
+
|
|
93
|
+
### Layered Architecture
|
|
94
|
+
|
|
95
|
+
Enforce dependency direction: `presentation → application → domain`, where `domain` has no outward dependencies.
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
rules:
|
|
99
|
+
- name: domain-isolation
|
|
100
|
+
modules: my_app.domain
|
|
101
|
+
allow:
|
|
102
|
+
standard_library: ["*"]
|
|
103
|
+
third_party: []
|
|
104
|
+
local: [my_app.domain]
|
|
105
|
+
|
|
106
|
+
- name: application-layer
|
|
107
|
+
modules: my_app.application
|
|
108
|
+
allow:
|
|
109
|
+
standard_library: ["*"]
|
|
110
|
+
third_party: [pydantic]
|
|
111
|
+
local:
|
|
112
|
+
- my_app.application
|
|
113
|
+
- my_app.domain
|
|
114
|
+
|
|
115
|
+
- name: presentation-layer
|
|
116
|
+
modules: my_app.presentation
|
|
117
|
+
allow:
|
|
118
|
+
standard_library: ["*"]
|
|
119
|
+
third_party: [fastapi, pydantic]
|
|
120
|
+
local:
|
|
121
|
+
- my_app.presentation
|
|
122
|
+
- my_app.application
|
|
123
|
+
- my_app.domain
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Hexagonal Architecture
|
|
127
|
+
|
|
128
|
+
Isolate domain from infrastructure. Ports (interfaces) live in domain, adapters depend on domain but not vice versa.
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
rules:
|
|
132
|
+
- name: domain-no-infra
|
|
133
|
+
modules: contexts.*.domain
|
|
134
|
+
allow:
|
|
135
|
+
standard_library: [dataclasses, typing, abc]
|
|
136
|
+
third_party: []
|
|
137
|
+
local: [contexts.*.domain]
|
|
138
|
+
|
|
139
|
+
- name: adapters-depend-on-domain
|
|
140
|
+
modules: contexts.*.adapters
|
|
141
|
+
allow:
|
|
142
|
+
standard_library: ["*"]
|
|
143
|
+
third_party: ["*"]
|
|
144
|
+
local:
|
|
145
|
+
- contexts.*.adapters
|
|
146
|
+
- contexts.*.domain
|
|
147
|
+
```
|
|
148
|
+
|
|
83
149
|
## Configuration
|
|
84
150
|
|
|
85
151
|
### Rule Structure
|
|
@@ -117,7 +183,17 @@ Dependencies are classified into three categories (per PEP 8):
|
|
|
117
183
|
- **`allow` only** — Whitelist mode. Only listed dependencies are allowed
|
|
118
184
|
- **`deny` only** — Blacklist mode. Listed dependencies are denied, rest allowed
|
|
119
185
|
- **`allow` + `deny`** — Allow first, then deny removes exceptions
|
|
120
|
-
- If `allow` exists but a category is omitted, that category allows all
|
|
186
|
+
- If `allow` exists but a category is omitted, that category allows all. For example:
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
rules:
|
|
190
|
+
- name: domain-isolation
|
|
191
|
+
modules: contexts.*.domain
|
|
192
|
+
allow:
|
|
193
|
+
third_party: [pydantic]
|
|
194
|
+
local: [contexts.*.domain]
|
|
195
|
+
# standard_library is omitted → all standard library imports are allowed
|
|
196
|
+
```
|
|
121
197
|
|
|
122
198
|
Use `"*"` to allow all within a category:
|
|
123
199
|
|
|
@@ -134,6 +210,23 @@ allow:
|
|
|
134
210
|
modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ...
|
|
135
211
|
```
|
|
136
212
|
|
|
213
|
+
`**` matches one or more levels in dotted module paths:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Submodule Matching
|
|
220
|
+
|
|
221
|
+
When a pattern is used in `allow` or `deny`, it also matches submodules of the matched module. For example:
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
allow:
|
|
225
|
+
local: [contexts.*.domain]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
This allows imports of `contexts.boards.domain` as well as its submodules like `contexts.boards.domain.models` or `contexts.boards.domain.entities.metric`.
|
|
229
|
+
|
|
137
230
|
### Rule Merging
|
|
138
231
|
|
|
139
232
|
When multiple rules match a module, they are merged. Specific rules override wildcard rules per field:
|
|
@@ -164,6 +257,22 @@ modules = "contexts.*.domain"
|
|
|
164
257
|
standard_library = ["dataclasses", "typing"]
|
|
165
258
|
third_party = ["pydantic"]
|
|
166
259
|
local = ["contexts.*.domain"]
|
|
260
|
+
|
|
261
|
+
[[tool.python-dependency-linter.rules]]
|
|
262
|
+
name = "application-dependency"
|
|
263
|
+
modules = "contexts.*.application"
|
|
264
|
+
|
|
265
|
+
[tool.python-dependency-linter.rules.allow]
|
|
266
|
+
standard_library = ["*"]
|
|
267
|
+
third_party = ["pydantic"]
|
|
268
|
+
local = ["contexts.*.application", "contexts.*.domain"]
|
|
269
|
+
|
|
270
|
+
[[tool.python-dependency-linter.rules]]
|
|
271
|
+
name = "no-boto-in-domain"
|
|
272
|
+
modules = "contexts.*.domain"
|
|
273
|
+
|
|
274
|
+
[tool.python-dependency-linter.rules.deny]
|
|
275
|
+
third_party = ["boto3"]
|
|
167
276
|
```
|
|
168
277
|
|
|
169
278
|
## CLI
|
|
@@ -195,6 +304,16 @@ Add to `.pre-commit-config.yaml`:
|
|
|
195
304
|
- id: python-dependency-linter
|
|
196
305
|
```
|
|
197
306
|
|
|
307
|
+
To pass custom options (e.g., a different config file or project root):
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
- repo: https://github.com/heumsi/python-dependency-linter
|
|
311
|
+
rev: v0.1.0
|
|
312
|
+
hooks:
|
|
313
|
+
- id: python-dependency-linter
|
|
314
|
+
args: [--config, custom-config.yaml, --project-root, src]
|
|
315
|
+
```
|
|
316
|
+
|
|
198
317
|
## License
|
|
199
318
|
|
|
200
319
|
MIT
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
A dependency linter for Python projects. Define rules for which modules can depend on what, and catch violations.
|
|
4
4
|
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
- Define dependency rules between modules using a simple YAML or TOML config
|
|
8
|
+
- Detect imports that violate your rules with a single CLI command
|
|
9
|
+
- Integrate into CI or pre-commit to keep your architecture consistent
|
|
10
|
+
|
|
11
|
+
For Python developers who care about module boundaries and dependency direction — whether you're applying Layered, Hexagonal, Clean Architecture, or your own conventions.
|
|
12
|
+
|
|
5
13
|
## Installation
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -55,6 +63,64 @@ contexts/boards/domain/models.py:9
|
|
|
55
63
|
Found 2 violation(s).
|
|
56
64
|
```
|
|
57
65
|
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
### Layered Architecture
|
|
69
|
+
|
|
70
|
+
Enforce dependency direction: `presentation → application → domain`, where `domain` has no outward dependencies.
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
rules:
|
|
74
|
+
- name: domain-isolation
|
|
75
|
+
modules: my_app.domain
|
|
76
|
+
allow:
|
|
77
|
+
standard_library: ["*"]
|
|
78
|
+
third_party: []
|
|
79
|
+
local: [my_app.domain]
|
|
80
|
+
|
|
81
|
+
- name: application-layer
|
|
82
|
+
modules: my_app.application
|
|
83
|
+
allow:
|
|
84
|
+
standard_library: ["*"]
|
|
85
|
+
third_party: [pydantic]
|
|
86
|
+
local:
|
|
87
|
+
- my_app.application
|
|
88
|
+
- my_app.domain
|
|
89
|
+
|
|
90
|
+
- name: presentation-layer
|
|
91
|
+
modules: my_app.presentation
|
|
92
|
+
allow:
|
|
93
|
+
standard_library: ["*"]
|
|
94
|
+
third_party: [fastapi, pydantic]
|
|
95
|
+
local:
|
|
96
|
+
- my_app.presentation
|
|
97
|
+
- my_app.application
|
|
98
|
+
- my_app.domain
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Hexagonal Architecture
|
|
102
|
+
|
|
103
|
+
Isolate domain from infrastructure. Ports (interfaces) live in domain, adapters depend on domain but not vice versa.
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
rules:
|
|
107
|
+
- name: domain-no-infra
|
|
108
|
+
modules: contexts.*.domain
|
|
109
|
+
allow:
|
|
110
|
+
standard_library: [dataclasses, typing, abc]
|
|
111
|
+
third_party: []
|
|
112
|
+
local: [contexts.*.domain]
|
|
113
|
+
|
|
114
|
+
- name: adapters-depend-on-domain
|
|
115
|
+
modules: contexts.*.adapters
|
|
116
|
+
allow:
|
|
117
|
+
standard_library: ["*"]
|
|
118
|
+
third_party: ["*"]
|
|
119
|
+
local:
|
|
120
|
+
- contexts.*.adapters
|
|
121
|
+
- contexts.*.domain
|
|
122
|
+
```
|
|
123
|
+
|
|
58
124
|
## Configuration
|
|
59
125
|
|
|
60
126
|
### Rule Structure
|
|
@@ -92,7 +158,17 @@ Dependencies are classified into three categories (per PEP 8):
|
|
|
92
158
|
- **`allow` only** — Whitelist mode. Only listed dependencies are allowed
|
|
93
159
|
- **`deny` only** — Blacklist mode. Listed dependencies are denied, rest allowed
|
|
94
160
|
- **`allow` + `deny`** — Allow first, then deny removes exceptions
|
|
95
|
-
- If `allow` exists but a category is omitted, that category allows all
|
|
161
|
+
- If `allow` exists but a category is omitted, that category allows all. For example:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
rules:
|
|
165
|
+
- name: domain-isolation
|
|
166
|
+
modules: contexts.*.domain
|
|
167
|
+
allow:
|
|
168
|
+
third_party: [pydantic]
|
|
169
|
+
local: [contexts.*.domain]
|
|
170
|
+
# standard_library is omitted → all standard library imports are allowed
|
|
171
|
+
```
|
|
96
172
|
|
|
97
173
|
Use `"*"` to allow all within a category:
|
|
98
174
|
|
|
@@ -109,6 +185,23 @@ allow:
|
|
|
109
185
|
modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ...
|
|
110
186
|
```
|
|
111
187
|
|
|
188
|
+
`**` matches one or more levels in dotted module paths:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Submodule Matching
|
|
195
|
+
|
|
196
|
+
When a pattern is used in `allow` or `deny`, it also matches submodules of the matched module. For example:
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
allow:
|
|
200
|
+
local: [contexts.*.domain]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This allows imports of `contexts.boards.domain` as well as its submodules like `contexts.boards.domain.models` or `contexts.boards.domain.entities.metric`.
|
|
204
|
+
|
|
112
205
|
### Rule Merging
|
|
113
206
|
|
|
114
207
|
When multiple rules match a module, they are merged. Specific rules override wildcard rules per field:
|
|
@@ -139,6 +232,22 @@ modules = "contexts.*.domain"
|
|
|
139
232
|
standard_library = ["dataclasses", "typing"]
|
|
140
233
|
third_party = ["pydantic"]
|
|
141
234
|
local = ["contexts.*.domain"]
|
|
235
|
+
|
|
236
|
+
[[tool.python-dependency-linter.rules]]
|
|
237
|
+
name = "application-dependency"
|
|
238
|
+
modules = "contexts.*.application"
|
|
239
|
+
|
|
240
|
+
[tool.python-dependency-linter.rules.allow]
|
|
241
|
+
standard_library = ["*"]
|
|
242
|
+
third_party = ["pydantic"]
|
|
243
|
+
local = ["contexts.*.application", "contexts.*.domain"]
|
|
244
|
+
|
|
245
|
+
[[tool.python-dependency-linter.rules]]
|
|
246
|
+
name = "no-boto-in-domain"
|
|
247
|
+
modules = "contexts.*.domain"
|
|
248
|
+
|
|
249
|
+
[tool.python-dependency-linter.rules.deny]
|
|
250
|
+
third_party = ["boto3"]
|
|
142
251
|
```
|
|
143
252
|
|
|
144
253
|
## CLI
|
|
@@ -170,6 +279,16 @@ Add to `.pre-commit-config.yaml`:
|
|
|
170
279
|
- id: python-dependency-linter
|
|
171
280
|
```
|
|
172
281
|
|
|
282
|
+
To pass custom options (e.g., a different config file or project root):
|
|
283
|
+
|
|
284
|
+
```yaml
|
|
285
|
+
- repo: https://github.com/heumsi/python-dependency-linter
|
|
286
|
+
rev: v0.1.0
|
|
287
|
+
hooks:
|
|
288
|
+
- id: python-dependency-linter
|
|
289
|
+
args: [--config, custom-config.yaml, --project-root, src]
|
|
290
|
+
```
|
|
291
|
+
|
|
173
292
|
## License
|
|
174
293
|
|
|
175
294
|
MIT
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "python-dependency-linter"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A dependency linter for Python projects"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pyyaml>=6.0",
|
|
25
|
+
"click>=8.0",
|
|
26
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
pdl = "python_dependency_linter.cli:main"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=7.0",
|
|
35
|
+
"ruff>=0.4",
|
|
36
|
+
"pre-commit>=3.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.version]
|
|
40
|
+
source = "vcs"
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
target-version = "py310"
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "I"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.isort]
|
|
49
|
+
known-first-party = ["python_dependency_linter"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
|
|
54
|
+
[tool.git-cliff.changelog]
|
|
55
|
+
header = """# Changelog\n
|
|
56
|
+
All notable changes to this project will be documented in this file.\n
|
|
57
|
+
"""
|
|
58
|
+
body = """
|
|
59
|
+
{%- macro remote_url() -%}
|
|
60
|
+
https://github.com/heumsi/python-dependency-linter
|
|
61
|
+
{%- endmacro -%}
|
|
62
|
+
|
|
63
|
+
{% if version -%}
|
|
64
|
+
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
65
|
+
{% else -%}
|
|
66
|
+
## [Unreleased]
|
|
67
|
+
{% endif -%}
|
|
68
|
+
|
|
69
|
+
{% for group, commits in commits | group_by(attribute="group") %}
|
|
70
|
+
### {{ group | upper_first }}
|
|
71
|
+
{% for commit in commits %}
|
|
72
|
+
- {{ commit.message | split(pat=":") | last | trim }}\
|
|
73
|
+
{% endfor %}
|
|
74
|
+
{% endfor %}
|
|
75
|
+
"""
|
|
76
|
+
trim = true
|
|
77
|
+
|
|
78
|
+
[tool.git-cliff.git]
|
|
79
|
+
conventional_commits = true
|
|
80
|
+
filter_unconventional = true
|
|
81
|
+
commit_preprocessors = [
|
|
82
|
+
{ pattern = ' *(:\w+:|[\p{Emoji_Presentation}\p{Extended_Pictographic}]\u{FE0F}?\u{200D}?) *', replace = "" },
|
|
83
|
+
]
|
|
84
|
+
commit_parsers = [
|
|
85
|
+
{ message = "^.*Update CHANGELOG", skip = true },
|
|
86
|
+
{ message = "^.*feat", group = "Features" },
|
|
87
|
+
{ message = "^.*fix", group = "Bug Fixes" },
|
|
88
|
+
{ message = "^.*refactor", group = "Refactor" },
|
|
89
|
+
{ message = "^.*perf", group = "Performance" },
|
|
90
|
+
{ message = "^.*doc", group = "Documentation" },
|
|
91
|
+
{ message = "^.*test", group = "Testing" },
|
|
92
|
+
{ message = "^.*chore", group = "Miscellaneous" },
|
|
93
|
+
{ message = "^.*ci", group = "CI/CD" },
|
|
94
|
+
]
|
|
95
|
+
tag_pattern = "v[0-9].*"
|
|
@@ -6,17 +6,29 @@ from python_dependency_linter.config import AllowDeny, Rule
|
|
|
6
6
|
def matches_pattern(pattern: str, module: str) -> bool:
|
|
7
7
|
pattern_parts = pattern.split(".")
|
|
8
8
|
module_parts = module.split(".")
|
|
9
|
+
return _match(pattern_parts, module_parts)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
def _match(pattern_parts: list[str], module_parts: list[str]) -> bool:
|
|
13
|
+
if not pattern_parts and not module_parts:
|
|
14
|
+
return True
|
|
15
|
+
if not pattern_parts:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
if pattern_parts[0] == "**":
|
|
19
|
+
# "**" matches one or more parts
|
|
20
|
+
for i in range(1, len(module_parts) + 1):
|
|
21
|
+
if _match(pattern_parts[1:], module_parts[i:]):
|
|
22
|
+
return True
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
if not module_parts:
|
|
11
26
|
return False
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
continue
|
|
16
|
-
if p != m:
|
|
17
|
-
return False
|
|
28
|
+
if pattern_parts[0] == "*" or pattern_parts[0] == module_parts[0]:
|
|
29
|
+
return _match(pattern_parts[1:], module_parts[1:])
|
|
18
30
|
|
|
19
|
-
return
|
|
31
|
+
return False
|
|
20
32
|
|
|
21
33
|
|
|
22
34
|
def find_matching_rules(module: str, rules: list[Rule]) -> list[Rule]:
|
|
@@ -17,6 +17,38 @@ def test_matches_pattern_wildcard():
|
|
|
17
17
|
assert matches_pattern("contexts.*.domain", "contexts.boards.application") is False
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def test_matches_pattern_double_star():
|
|
21
|
+
# matches one level
|
|
22
|
+
assert matches_pattern("contexts.**.domain", "contexts.analytics.domain") is True
|
|
23
|
+
# matches multiple levels
|
|
24
|
+
assert (
|
|
25
|
+
matches_pattern("contexts.**.domain", "contexts.analytics.sub.domain") is True
|
|
26
|
+
)
|
|
27
|
+
# does not match zero levels (** requires one or more)
|
|
28
|
+
assert matches_pattern("contexts.**.domain", "contexts.domain") is False
|
|
29
|
+
# does not match wrong suffix
|
|
30
|
+
assert (
|
|
31
|
+
matches_pattern("contexts.**.domain", "contexts.analytics.application") is False
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_matches_pattern_double_star_at_end():
|
|
36
|
+
assert (
|
|
37
|
+
matches_pattern("contexts.**.domain.**", "contexts.a.domain.entities") is True
|
|
38
|
+
)
|
|
39
|
+
assert (
|
|
40
|
+
matches_pattern("contexts.**.domain.**", "contexts.a.domain.entities.metric")
|
|
41
|
+
is True
|
|
42
|
+
)
|
|
43
|
+
assert matches_pattern("contexts.**.domain.**", "contexts.a.domain") is False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_matches_pattern_double_star_alone():
|
|
47
|
+
# ** alone matches any module with one or more parts
|
|
48
|
+
assert matches_pattern("**", "anything") is True
|
|
49
|
+
assert matches_pattern("**", "a.b.c") is True
|
|
50
|
+
|
|
51
|
+
|
|
20
52
|
def test_matches_pattern_wildcard_in_allow():
|
|
21
53
|
assert matches_pattern("contexts.*.domain", "contexts.boards.domain") is True
|
|
22
54
|
|