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.
Files changed (49) hide show
  1. python_dependency_linter-0.2.0/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  2. python_dependency_linter-0.2.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  3. python_dependency_linter-0.2.0/.github/ISSUE_TEMPLATE/feature_request.yml +37 -0
  4. python_dependency_linter-0.2.0/.github/pull_request_template.md +13 -0
  5. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.github/workflows/ci.yaml +8 -4
  6. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.github/workflows/publish.yaml +4 -4
  7. python_dependency_linter-0.2.0/CHANGELOG.md +5 -0
  8. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/PKG-INFO +121 -2
  9. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/README.md +120 -1
  10. python_dependency_linter-0.2.0/pyproject.toml +95 -0
  11. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/matcher.py +19 -7
  12. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_matcher.py +32 -0
  13. python_dependency_linter-0.1.0/cliff.toml +0 -38
  14. python_dependency_linter-0.1.0/docs/superpowers/plans/2026-03-30-python-dependency-linter.md +0 -1343
  15. python_dependency_linter-0.1.0/docs/superpowers/specs/2026-03-30-python-dependency-linter-design.md +0 -235
  16. python_dependency_linter-0.1.0/pyproject.toml +0 -49
  17. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.github/dependabot.yml +0 -0
  18. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.gitignore +0 -0
  19. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.pre-commit-config.yaml +0 -0
  20. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/.pre-commit-hooks.yaml +0 -0
  21. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/LICENSE +0 -0
  22. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/__init__.py +0 -0
  23. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/checker.py +0 -0
  24. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/cli.py +0 -0
  25. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/config.py +0 -0
  26. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/parser.py +0 -0
  27. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/reporter.py +0 -0
  28. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/python_dependency_linter/resolver.py +0 -0
  29. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_config.yaml +0 -0
  30. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/__init__.py +0 -0
  31. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/__init__.py +0 -0
  32. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/application/__init__.py +0 -0
  33. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/application/service.py +0 -0
  34. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/domain/__init__.py +0 -0
  35. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/auth/domain/models.py +0 -0
  36. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/__init__.py +0 -0
  37. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/adapters/__init__.py +0 -0
  38. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/adapters/repository.py +0 -0
  39. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/application/__init__.py +0 -0
  40. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/application/service.py +0 -0
  41. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/domain/__init__.py +0 -0
  42. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_project/contexts/boards/domain/models.py +0 -0
  43. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/fixtures/sample_pyproject.toml +0 -0
  44. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_checker.py +0 -0
  45. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_cli.py +0 -0
  46. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_config.py +0 -0
  47. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_parser.py +0 -0
  48. {python_dependency_linter-0.1.0 → python_dependency_linter-0.2.0}/tests/test_reporter.py +0 -0
  49. {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,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Question / Discussion
4
+ url: https://github.com/heumsi/python-layer-dependency-linter/discussions
5
+ about: Ask questions or start a discussion here.
@@ -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@v4
15
+ - uses: actions/checkout@v6
12
16
  - uses: astral-sh/setup-uv@v4
13
- - uses: actions/setup-python@v5
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@v4
30
+ - uses: actions/checkout@v6
27
31
  - uses: astral-sh/setup-uv@v4
28
- - uses: actions/setup-python@v5
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]"
@@ -11,13 +11,13 @@ jobs:
11
11
  permissions:
12
12
  contents: write
13
13
  steps:
14
- - uses: actions/checkout@v4
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: cliff.toml
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: cliff.toml
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@v4
53
+ - uses: actions/checkout@v6
54
54
  - uses: actions/setup-python@v5
55
55
  with:
56
56
  python-version: "3.13"
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.0] - 2026-03-30
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-dependency-linter
3
- Version: 0.1.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
- if len(pattern_parts) != len(module_parts):
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
- for p, m in zip(pattern_parts, module_parts):
14
- if p == "*":
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 True
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