python-dependency-linter 0.2.0__tar.gz → 0.4.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.4.0/.claude/skills/commit/SKILL.md +24 -0
- python_dependency_linter-0.4.0/.claude/skills/release/SKILL.md +14 -0
- python_dependency_linter-0.4.0/.github/pull_request_template.md +27 -0
- python_dependency_linter-0.4.0/CHANGELOG.md +94 -0
- python_dependency_linter-0.4.0/CLAUDE.md +9 -0
- python_dependency_linter-0.4.0/CONTRIBUTING.md +84 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/PKG-INFO +43 -8
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/README.md +42 -7
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/cli.py +62 -16
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/config.py +43 -4
- python_dependency_linter-0.4.0/python_dependency_linter/parser.py +60 -0
- python_dependency_linter-0.4.0/tests/fixtures/sample_project/contexts/boards/__init__.py +1 -0
- python_dependency_linter-0.4.0/tests/fixtures/sample_project/contexts/boards/adapters/repository.py +7 -0
- python_dependency_linter-0.4.0/tests/test_cli.py +239 -0
- python_dependency_linter-0.4.0/tests/test_config.py +126 -0
- python_dependency_linter-0.4.0/tests/test_parser.py +74 -0
- python_dependency_linter-0.4.0/uv.lock +368 -0
- python_dependency_linter-0.2.0/.github/pull_request_template.md +0 -13
- python_dependency_linter-0.2.0/CHANGELOG.md +0 -5
- python_dependency_linter-0.2.0/python_dependency_linter/parser.py +0 -27
- python_dependency_linter-0.2.0/tests/fixtures/sample_project/contexts/boards/adapters/repository.py +0 -3
- python_dependency_linter-0.2.0/tests/fixtures/sample_project/contexts/boards/domain/__init__.py +0 -0
- python_dependency_linter-0.2.0/tests/test_cli.py +0 -69
- python_dependency_linter-0.2.0/tests/test_config.py +0 -43
- python_dependency_linter-0.2.0/tests/test_parser.py +0 -30
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.github/dependabot.yml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.github/workflows/ci.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.github/workflows/publish.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.gitignore +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.pre-commit-config.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/.pre-commit-hooks.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/LICENSE +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/pyproject.toml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/checker.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/matcher.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/reporter.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/resolver.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_config.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/application/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/application/service.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/domain/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/domain/models.py +0 -0
- {python_dependency_linter-0.2.0/tests/fixtures/sample_project/contexts/boards → python_dependency_linter-0.4.0/tests/fixtures/sample_project/contexts/boards/adapters}/__init__.py +0 -0
- {python_dependency_linter-0.2.0/tests/fixtures/sample_project/contexts/boards/adapters → python_dependency_linter-0.4.0/tests/fixtures/sample_project/contexts/boards/application}/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/application/service.py +0 -0
- {python_dependency_linter-0.2.0/tests/fixtures/sample_project/contexts/boards/application → python_dependency_linter-0.4.0/tests/fixtures/sample_project/contexts/boards/domain}/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/domain/models.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_pyproject.toml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/test_checker.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/test_matcher.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/test_reporter.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/tests/test_resolver.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commit
|
|
3
|
+
description: Create a git commit following the project's commit convention
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Create a git commit following the project's commit convention defined in [CONTRIBUTING.md](../../../CONTRIBUTING.md).
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
|
|
10
|
+
1. Read `CONTRIBUTING.md` to check the commit convention.
|
|
11
|
+
2. Run `git status` and `git diff` to review all changes.
|
|
12
|
+
3. Draft a commit message following the convention.
|
|
13
|
+
4. Stage only relevant files (do NOT use `git add -A`). Exclude secrets and unrelated files.
|
|
14
|
+
5. Commit using a HEREDOC for proper formatting:
|
|
15
|
+
```bash
|
|
16
|
+
git commit -m "$(cat <<'EOF'
|
|
17
|
+
<commit message here>
|
|
18
|
+
|
|
19
|
+
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
20
|
+
EOF
|
|
21
|
+
)"
|
|
22
|
+
```
|
|
23
|
+
6. Run `git status` to verify success.
|
|
24
|
+
7. If pre-commit hook fails, fix the issue and create a **new** commit (never amend).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release
|
|
3
|
+
description: Create a new release by calculating the next version from conventional commits and pushing a git tag
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Create a new release following the project's release process defined in [CONTRIBUTING.md](../../../CONTRIBUTING.md).
|
|
7
|
+
|
|
8
|
+
The release workflow is defined in [.github/workflows/publish.yaml](../../../.github/workflows/publish.yaml).
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. Read `CONTRIBUTING.md` to check the release process.
|
|
13
|
+
2. Follow the steps described in the Release section.
|
|
14
|
+
3. Ask the user to confirm the version before creating and pushing the tag.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
⚠️ PR Title Convention
|
|
3
|
+
PRs are squash merged, so the PR title becomes the final commit message.
|
|
4
|
+
Please use the following format:
|
|
5
|
+
|
|
6
|
+
<gitmoji> <type>: <Description>
|
|
7
|
+
|
|
8
|
+
Examples:
|
|
9
|
+
✨ feat: Add support for relative imports
|
|
10
|
+
🐛 fix: Use exit code 2 for config file not found
|
|
11
|
+
|
|
12
|
+
See CONTRIBUTING.md for the full list of types.
|
|
13
|
+
-->
|
|
14
|
+
|
|
15
|
+
## What does this PR do?
|
|
16
|
+
|
|
17
|
+
<!-- A brief description of the change. -->
|
|
18
|
+
|
|
19
|
+
## Related issue
|
|
20
|
+
|
|
21
|
+
<!-- Link to the issue this PR addresses, e.g., Closes #123 -->
|
|
22
|
+
|
|
23
|
+
## Checklist
|
|
24
|
+
|
|
25
|
+
- [ ] Tests added / updated
|
|
26
|
+
- [ ] `pdl check` runs without errors on the example config
|
|
27
|
+
- [ ] Documentation updated (if applicable)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.3.0] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Use exit code 2 for config file not found (#11)
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
- Add CONTRIBUTING.md and CLAUDE.md
|
|
14
|
+
- Add PR title convention to template and CONTRIBUTING.md
|
|
15
|
+
- Add release process to CONTRIBUTING.md and /release skill
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- Resolve relative imports to absolute module names (#10)
|
|
20
|
+
- Add include/exclude file filtering options (#12)
|
|
21
|
+
|
|
22
|
+
### Miscellaneous
|
|
23
|
+
|
|
24
|
+
- Add /commit skill for Claude Code
|
|
25
|
+
- Add uv.lock for reproducible builds
|
|
26
|
+
## [0.2.0] - 2026-03-30
|
|
27
|
+
|
|
28
|
+
### Documentation
|
|
29
|
+
|
|
30
|
+
- Remove design spec and implementation plan docs
|
|
31
|
+
- Add GitHub issue and PR templates
|
|
32
|
+
- Add example for omitted category behavior in allow rules
|
|
33
|
+
- Add architecture examples to README
|
|
34
|
+
- Add pre-commit hook custom options example
|
|
35
|
+
- Expand pyproject.toml examples with multiple rules and deny
|
|
36
|
+
- Add "What It Does" section to README
|
|
37
|
+
|
|
38
|
+
### Features
|
|
39
|
+
|
|
40
|
+
- Support `**` glob pattern for matching nested submodules (#6)
|
|
41
|
+
- Add undocumented features to README
|
|
42
|
+
|
|
43
|
+
### Miscellaneous
|
|
44
|
+
|
|
45
|
+
- Use hatch-vcs for dynamic versioning from git tags
|
|
46
|
+
- Add gitmoji preprocessor to git-cliff config
|
|
47
|
+
- Move git-cliff config from cliff.toml to pyproject.toml
|
|
48
|
+
- Skip CHANGELOG update commits in git-cliff output
|
|
49
|
+
|
|
50
|
+
### Testing
|
|
51
|
+
|
|
52
|
+
- Limit CI to source and test file changes
|
|
53
|
+
|
|
54
|
+
### Build
|
|
55
|
+
|
|
56
|
+
- Bump actions/checkout from 4 to 6 (#3)
|
|
57
|
+
- Bump actions/setup-python from 5 to 6 (#2)
|
|
58
|
+
## [0.1.0] - 2026-03-30
|
|
59
|
+
|
|
60
|
+
### Bug Fixes
|
|
61
|
+
|
|
62
|
+
- Add isort config and fix plan typo
|
|
63
|
+
- Add tomli fallback for Python 3.10 compatibility
|
|
64
|
+
- Add import content to fixture files
|
|
65
|
+
|
|
66
|
+
### CI/CD
|
|
67
|
+
|
|
68
|
+
- Add GitHub Actions for CI, PyPI publish, and Dependabot
|
|
69
|
+
- Add git-cliff changelog generation on release
|
|
70
|
+
- Add GitHub Release creation on tag
|
|
71
|
+
|
|
72
|
+
### Documentation
|
|
73
|
+
|
|
74
|
+
- Add design spec for python-dependency-linter
|
|
75
|
+
- Add implementation plan
|
|
76
|
+
- Add README
|
|
77
|
+
- Add MIT LICENSE
|
|
78
|
+
|
|
79
|
+
### Features
|
|
80
|
+
|
|
81
|
+
- Add config loading for YAML and pyproject.toml
|
|
82
|
+
- Add AST-based import parser
|
|
83
|
+
- Add import resolver for classification
|
|
84
|
+
- Add wildcard matcher and rule merging
|
|
85
|
+
- Add dependency checker with allow/deny logic
|
|
86
|
+
- Add violation reporter
|
|
87
|
+
- Add CLI with check command
|
|
88
|
+
- Add pre-commit hook definition
|
|
89
|
+
|
|
90
|
+
### Miscellaneous
|
|
91
|
+
|
|
92
|
+
- Initialize project scaffolding
|
|
93
|
+
- Add license, readme, and classifiers to pyproject.toml
|
|
94
|
+
- Add .gitignore
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Commit Convention
|
|
4
|
+
|
|
5
|
+
Commit messages must follow [Conventional Commits](https://www.conventionalcommits.org/) with [gitmoji](https://gitmoji.dev/) prefix.
|
|
6
|
+
|
|
7
|
+
### Format
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
<gitmoji> <type>: <description>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- The first letter after the colon must be **capitalized**.
|
|
14
|
+
- The description must be in **English**.
|
|
15
|
+
|
|
16
|
+
### Types
|
|
17
|
+
|
|
18
|
+
| Gitmoji | Type | Description |
|
|
19
|
+
|---------|------------|--------------------------|
|
|
20
|
+
| ✨ | `feat` | New feature |
|
|
21
|
+
| 🐛 | `fix` | Bug fix |
|
|
22
|
+
| ♻️ | `refactor` | Code refactoring |
|
|
23
|
+
| 📝 | `docs` | Documentation |
|
|
24
|
+
| ✅ | `test` | Adding or updating tests |
|
|
25
|
+
| 🔧 | `chore` | Maintenance tasks |
|
|
26
|
+
| 👷 | `ci` | CI/CD changes |
|
|
27
|
+
| ⚡ | `perf` | Performance improvement |
|
|
28
|
+
|
|
29
|
+
### Examples
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
✨ feat: Add support for relative imports
|
|
33
|
+
🐛 fix: Use exit code 2 for config file not found
|
|
34
|
+
♻️ refactor: Simplify module resolver logic
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Pull Request Convention
|
|
38
|
+
|
|
39
|
+
- PRs are always **squash merged**, so the PR title becomes the final commit message.
|
|
40
|
+
- PR titles must follow the same format as commit messages (`<gitmoji> <type>: <description>`).
|
|
41
|
+
- PR descriptions must be written in **English**.
|
|
42
|
+
|
|
43
|
+
## Pre-commit Hooks
|
|
44
|
+
|
|
45
|
+
This project uses [pre-commit](https://pre-commit.com/) with [ruff](https://docs.astral.sh/ruff/) for linting and formatting.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Install pre-commit hooks
|
|
49
|
+
pre-commit install
|
|
50
|
+
|
|
51
|
+
# Run manually
|
|
52
|
+
pre-commit run --all-files
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
All commits must pass the pre-commit hooks before being accepted.
|
|
56
|
+
|
|
57
|
+
## Release
|
|
58
|
+
|
|
59
|
+
Releases are automated via GitHub Actions. You only need to create and push a version tag.
|
|
60
|
+
|
|
61
|
+
### Steps
|
|
62
|
+
|
|
63
|
+
1. Calculate the next version based on conventional commits:
|
|
64
|
+
```bash
|
|
65
|
+
uvx git-cliff --bumped-version
|
|
66
|
+
```
|
|
67
|
+
2. Review the commits since the last tag:
|
|
68
|
+
```bash
|
|
69
|
+
git log $(git describe --tags --abbrev=0)..HEAD --oneline
|
|
70
|
+
```
|
|
71
|
+
3. Push the latest commits to `main`:
|
|
72
|
+
```bash
|
|
73
|
+
git push origin main
|
|
74
|
+
```
|
|
75
|
+
4. Create and push the tag:
|
|
76
|
+
```bash
|
|
77
|
+
git tag <version>
|
|
78
|
+
git push origin <version>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The GitHub Actions workflow will then automatically:
|
|
82
|
+
- Generate `CHANGELOG.md` and commit it to `main`
|
|
83
|
+
- Create a GitHub Release with release notes
|
|
84
|
+
- Publish the package to PyPI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-dependency-linter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A dependency linter for Python projects
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -148,6 +148,35 @@ rules:
|
|
|
148
148
|
|
|
149
149
|
## Configuration
|
|
150
150
|
|
|
151
|
+
### Include / Exclude
|
|
152
|
+
|
|
153
|
+
Control which files are scanned using `include` and `exclude`:
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
include:
|
|
157
|
+
- src
|
|
158
|
+
exclude:
|
|
159
|
+
- src/generated/**
|
|
160
|
+
|
|
161
|
+
rules:
|
|
162
|
+
- name: ...
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- **No `include` or `exclude`** — All `.py` files under the project root are scanned
|
|
166
|
+
- **`include` only** — Only files matching the given paths are scanned
|
|
167
|
+
- **`exclude` only** — All files except those matching the given paths are scanned
|
|
168
|
+
- **Both** — `include` is applied first, then `exclude` filters within that result
|
|
169
|
+
|
|
170
|
+
Bare directory names (e.g., `src`) and trailing-slash forms (e.g., `src/`) are treated the same as `src/**`.
|
|
171
|
+
|
|
172
|
+
In `pyproject.toml`:
|
|
173
|
+
|
|
174
|
+
```toml
|
|
175
|
+
[tool.python-dependency-linter]
|
|
176
|
+
include = ["src"]
|
|
177
|
+
exclude = ["src/generated/**"]
|
|
178
|
+
```
|
|
179
|
+
|
|
151
180
|
### Rule Structure
|
|
152
181
|
|
|
153
182
|
Each rule has:
|
|
@@ -177,6 +206,8 @@ Dependencies are classified into three categories (per PEP 8):
|
|
|
177
206
|
- `third_party` — Installed packages (`pydantic`, `sqlalchemy`, ...)
|
|
178
207
|
- `local` — Modules in your project
|
|
179
208
|
|
|
209
|
+
Both absolute imports (`from contexts.boards.domain import models`) and relative imports (`from ..domain import models`) are analyzed. Relative imports are resolved to absolute module names based on the file's location.
|
|
210
|
+
|
|
180
211
|
### Behavior
|
|
181
212
|
|
|
182
213
|
- **No rule** — Everything is allowed
|
|
@@ -278,20 +309,24 @@ third_party = ["boto3"]
|
|
|
278
309
|
## CLI
|
|
279
310
|
|
|
280
311
|
```bash
|
|
281
|
-
# Check with
|
|
312
|
+
# Check with auto-discovered config (searches upward from cwd)
|
|
282
313
|
pdl check
|
|
283
314
|
|
|
284
|
-
# Specify config file
|
|
315
|
+
# Specify config file (project root = config file's parent directory)
|
|
285
316
|
pdl check --config path/to/config.yaml
|
|
286
|
-
|
|
287
|
-
# Specify project root
|
|
288
|
-
pdl check --project-root path/to/project
|
|
289
317
|
```
|
|
290
318
|
|
|
291
319
|
Exit codes:
|
|
292
320
|
|
|
293
321
|
- `0` — No violations
|
|
294
322
|
- `1` — Violations found
|
|
323
|
+
- `2` — Config file not found
|
|
324
|
+
|
|
325
|
+
If no `--config` is given, the tool searches upward from the current directory for `.python-dependency-linter.yaml` or `pyproject.toml` (with `[tool.python-dependency-linter]`). The config file's parent directory is used as the project root. If no config file is found, the tool prints an error and exits with code `2`:
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml.
|
|
329
|
+
```
|
|
295
330
|
|
|
296
331
|
## Pre-commit
|
|
297
332
|
|
|
@@ -304,14 +339,14 @@ Add to `.pre-commit-config.yaml`:
|
|
|
304
339
|
- id: python-dependency-linter
|
|
305
340
|
```
|
|
306
341
|
|
|
307
|
-
To pass custom options (e.g., a different config file
|
|
342
|
+
To pass custom options (e.g., a different config file):
|
|
308
343
|
|
|
309
344
|
```yaml
|
|
310
345
|
- repo: https://github.com/heumsi/python-dependency-linter
|
|
311
346
|
rev: v0.1.0
|
|
312
347
|
hooks:
|
|
313
348
|
- id: python-dependency-linter
|
|
314
|
-
args: [--config, custom-config.yaml
|
|
349
|
+
args: [--config, custom-config.yaml]
|
|
315
350
|
```
|
|
316
351
|
|
|
317
352
|
## License
|
|
@@ -123,6 +123,35 @@ rules:
|
|
|
123
123
|
|
|
124
124
|
## Configuration
|
|
125
125
|
|
|
126
|
+
### Include / Exclude
|
|
127
|
+
|
|
128
|
+
Control which files are scanned using `include` and `exclude`:
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
include:
|
|
132
|
+
- src
|
|
133
|
+
exclude:
|
|
134
|
+
- src/generated/**
|
|
135
|
+
|
|
136
|
+
rules:
|
|
137
|
+
- name: ...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- **No `include` or `exclude`** — All `.py` files under the project root are scanned
|
|
141
|
+
- **`include` only** — Only files matching the given paths are scanned
|
|
142
|
+
- **`exclude` only** — All files except those matching the given paths are scanned
|
|
143
|
+
- **Both** — `include` is applied first, then `exclude` filters within that result
|
|
144
|
+
|
|
145
|
+
Bare directory names (e.g., `src`) and trailing-slash forms (e.g., `src/`) are treated the same as `src/**`.
|
|
146
|
+
|
|
147
|
+
In `pyproject.toml`:
|
|
148
|
+
|
|
149
|
+
```toml
|
|
150
|
+
[tool.python-dependency-linter]
|
|
151
|
+
include = ["src"]
|
|
152
|
+
exclude = ["src/generated/**"]
|
|
153
|
+
```
|
|
154
|
+
|
|
126
155
|
### Rule Structure
|
|
127
156
|
|
|
128
157
|
Each rule has:
|
|
@@ -152,6 +181,8 @@ Dependencies are classified into three categories (per PEP 8):
|
|
|
152
181
|
- `third_party` — Installed packages (`pydantic`, `sqlalchemy`, ...)
|
|
153
182
|
- `local` — Modules in your project
|
|
154
183
|
|
|
184
|
+
Both absolute imports (`from contexts.boards.domain import models`) and relative imports (`from ..domain import models`) are analyzed. Relative imports are resolved to absolute module names based on the file's location.
|
|
185
|
+
|
|
155
186
|
### Behavior
|
|
156
187
|
|
|
157
188
|
- **No rule** — Everything is allowed
|
|
@@ -253,20 +284,24 @@ third_party = ["boto3"]
|
|
|
253
284
|
## CLI
|
|
254
285
|
|
|
255
286
|
```bash
|
|
256
|
-
# Check with
|
|
287
|
+
# Check with auto-discovered config (searches upward from cwd)
|
|
257
288
|
pdl check
|
|
258
289
|
|
|
259
|
-
# Specify config file
|
|
290
|
+
# Specify config file (project root = config file's parent directory)
|
|
260
291
|
pdl check --config path/to/config.yaml
|
|
261
|
-
|
|
262
|
-
# Specify project root
|
|
263
|
-
pdl check --project-root path/to/project
|
|
264
292
|
```
|
|
265
293
|
|
|
266
294
|
Exit codes:
|
|
267
295
|
|
|
268
296
|
- `0` — No violations
|
|
269
297
|
- `1` — Violations found
|
|
298
|
+
- `2` — Config file not found
|
|
299
|
+
|
|
300
|
+
If no `--config` is given, the tool searches upward from the current directory for `.python-dependency-linter.yaml` or `pyproject.toml` (with `[tool.python-dependency-linter]`). The config file's parent directory is used as the project root. If no config file is found, the tool prints an error and exits with code `2`:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml.
|
|
304
|
+
```
|
|
270
305
|
|
|
271
306
|
## Pre-commit
|
|
272
307
|
|
|
@@ -279,14 +314,14 @@ Add to `.pre-commit-config.yaml`:
|
|
|
279
314
|
- id: python-dependency-linter
|
|
280
315
|
```
|
|
281
316
|
|
|
282
|
-
To pass custom options (e.g., a different config file
|
|
317
|
+
To pass custom options (e.g., a different config file):
|
|
283
318
|
|
|
284
319
|
```yaml
|
|
285
320
|
- repo: https://github.com/heumsi/python-dependency-linter
|
|
286
321
|
rev: v0.1.0
|
|
287
322
|
hooks:
|
|
288
323
|
- id: python-dependency-linter
|
|
289
|
-
args: [--config, custom-config.yaml
|
|
324
|
+
args: [--config, custom-config.yaml]
|
|
290
325
|
```
|
|
291
326
|
|
|
292
327
|
## License
|
{python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/cli.py
RENAMED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import fnmatch
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
|
|
7
8
|
from python_dependency_linter.checker import check_import
|
|
8
|
-
from python_dependency_linter.config import load_config
|
|
9
|
+
from python_dependency_linter.config import find_config, load_config
|
|
9
10
|
from python_dependency_linter.matcher import find_matching_rules, merge_rules
|
|
10
11
|
from python_dependency_linter.parser import parse_imports
|
|
11
12
|
from python_dependency_linter.reporter import format_violations
|
|
@@ -36,8 +37,43 @@ def _package_module(file_path: Path, project_root: Path) -> str:
|
|
|
36
37
|
return ".".join(parts)
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def
|
|
40
|
-
|
|
40
|
+
def _normalize_pattern(pattern: str, project_root: Path) -> str:
|
|
41
|
+
"""Normalize a pattern so that bare directory names match all files within."""
|
|
42
|
+
clean = pattern.rstrip("/")
|
|
43
|
+
candidate = project_root / clean
|
|
44
|
+
if candidate.is_dir() or not any(c in clean for c in ("*", "?")):
|
|
45
|
+
clean = f"{clean}/**"
|
|
46
|
+
return clean
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _matches_any(path: Path, patterns: list[str]) -> bool:
|
|
50
|
+
return any(fnmatch.fnmatch(str(path), p) for p in patterns)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _find_python_files(
|
|
54
|
+
project_root: Path,
|
|
55
|
+
include: list[str] | None = None,
|
|
56
|
+
exclude: list[str] | None = None,
|
|
57
|
+
) -> list[Path]:
|
|
58
|
+
all_files = sorted(project_root.rglob("*.py"))
|
|
59
|
+
|
|
60
|
+
if include is not None:
|
|
61
|
+
normalized = [_normalize_pattern(p, project_root) for p in include]
|
|
62
|
+
all_files = [
|
|
63
|
+
f
|
|
64
|
+
for f in all_files
|
|
65
|
+
if _matches_any(f.relative_to(project_root), normalized)
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
if exclude is not None:
|
|
69
|
+
normalized = [_normalize_pattern(p, project_root) for p in exclude]
|
|
70
|
+
all_files = [
|
|
71
|
+
f
|
|
72
|
+
for f in all_files
|
|
73
|
+
if not _matches_any(f.relative_to(project_root), normalized)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
return all_files
|
|
41
77
|
|
|
42
78
|
|
|
43
79
|
@click.group()
|
|
@@ -49,22 +85,32 @@ def main():
|
|
|
49
85
|
@click.option(
|
|
50
86
|
"--config",
|
|
51
87
|
"config_path",
|
|
52
|
-
default=
|
|
88
|
+
default=None,
|
|
53
89
|
help="Path to config file.",
|
|
54
90
|
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
def check(config_path: str | None):
|
|
92
|
+
if config_path is not None:
|
|
93
|
+
config_file = Path(config_path)
|
|
94
|
+
if not config_file.exists():
|
|
95
|
+
click.echo(f"Error: Config file not found: {config_file}", err=True)
|
|
96
|
+
raise SystemExit(2)
|
|
97
|
+
root = config_file.resolve().parent
|
|
98
|
+
else:
|
|
99
|
+
config_file = find_config()
|
|
100
|
+
if config_file is None:
|
|
101
|
+
click.echo(
|
|
102
|
+
"Error: Config file not found. "
|
|
103
|
+
"Create .python-dependency-linter.yaml or configure "
|
|
104
|
+
"[tool.python-dependency-linter] in pyproject.toml.",
|
|
105
|
+
err=True,
|
|
106
|
+
)
|
|
107
|
+
raise SystemExit(2)
|
|
108
|
+
root = config_file.resolve().parent
|
|
109
|
+
|
|
110
|
+
config = load_config(config_file)
|
|
65
111
|
|
|
66
112
|
all_violations = []
|
|
67
|
-
python_files = _find_python_files(root)
|
|
113
|
+
python_files = _find_python_files(root, config.include, config.exclude)
|
|
68
114
|
|
|
69
115
|
for file_path in python_files:
|
|
70
116
|
module = _file_to_module(file_path, root)
|
|
@@ -74,7 +120,7 @@ def check(config_path: str, project_root: str):
|
|
|
74
120
|
continue
|
|
75
121
|
|
|
76
122
|
merged_rule = merge_rules(matching_rules)
|
|
77
|
-
imports = parse_imports(file_path)
|
|
123
|
+
imports = parse_imports(file_path, root)
|
|
78
124
|
|
|
79
125
|
file_violations = []
|
|
80
126
|
for imp in imports:
|
{python_dependency_linter-0.2.0 → python_dependency_linter-0.4.0}/python_dependency_linter/config.py
RENAMED
|
@@ -24,6 +24,8 @@ class Rule:
|
|
|
24
24
|
@dataclass
|
|
25
25
|
class Config:
|
|
26
26
|
rules: list[Rule]
|
|
27
|
+
include: list[str] | None = None
|
|
28
|
+
exclude: list[str] | None = None
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def _parse_allow_deny(data: dict | None) -> AllowDeny | None:
|
|
@@ -53,19 +55,56 @@ def _parse_rules(rules_data: list[dict]) -> list[Rule]:
|
|
|
53
55
|
def _load_yaml(path: Path) -> Config:
|
|
54
56
|
with open(path) as f:
|
|
55
57
|
data = yaml.safe_load(f)
|
|
56
|
-
return Config(
|
|
58
|
+
return Config(
|
|
59
|
+
rules=_parse_rules(data["rules"]),
|
|
60
|
+
include=data.get("include"),
|
|
61
|
+
exclude=data.get("exclude"),
|
|
62
|
+
)
|
|
57
63
|
|
|
58
64
|
|
|
59
|
-
def
|
|
65
|
+
def _load_toml(path: Path) -> dict:
|
|
60
66
|
try:
|
|
61
67
|
import tomllib
|
|
62
68
|
except ImportError:
|
|
63
69
|
import tomli as tomllib # type: ignore[no-redef]
|
|
64
70
|
|
|
65
71
|
with open(path, "rb") as f:
|
|
66
|
-
|
|
72
|
+
return tomllib.load(f)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_pyproject_toml(path: Path) -> Config:
|
|
76
|
+
data = _load_toml(path)
|
|
67
77
|
tool_config = data["tool"]["python-dependency-linter"]
|
|
68
|
-
return Config(
|
|
78
|
+
return Config(
|
|
79
|
+
rules=_parse_rules(tool_config["rules"]),
|
|
80
|
+
include=tool_config.get("include"),
|
|
81
|
+
exclude=tool_config.get("exclude"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _has_pdl_section(path: Path) -> bool:
|
|
86
|
+
"""Check if a pyproject.toml contains [tool.python-dependency-linter]."""
|
|
87
|
+
data = _load_toml(path)
|
|
88
|
+
return "python-dependency-linter" in data.get("tool", {})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_CONFIG_FILENAMES = [".python-dependency-linter.yaml", "pyproject.toml"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def find_config() -> Path | None:
|
|
95
|
+
"""Search upward from cwd for a config file. Returns None if not found."""
|
|
96
|
+
current = Path.cwd().resolve()
|
|
97
|
+
while True:
|
|
98
|
+
for name in _CONFIG_FILENAMES:
|
|
99
|
+
candidate = current / name
|
|
100
|
+
if candidate.is_file():
|
|
101
|
+
if name == "pyproject.toml" and not _has_pdl_section(candidate):
|
|
102
|
+
continue
|
|
103
|
+
return candidate
|
|
104
|
+
parent = current.parent
|
|
105
|
+
if parent == current:
|
|
106
|
+
return None
|
|
107
|
+
current = parent
|
|
69
108
|
|
|
70
109
|
|
|
71
110
|
def load_config(path: Path) -> Config:
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ImportInfo:
|
|
10
|
+
module: str
|
|
11
|
+
lineno: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _resolve_relative_import(
|
|
15
|
+
file_path: Path,
|
|
16
|
+
project_root: Path,
|
|
17
|
+
level: int,
|
|
18
|
+
module: str | None,
|
|
19
|
+
) -> str | None:
|
|
20
|
+
"""Resolve a relative import to an absolute module name.
|
|
21
|
+
|
|
22
|
+
Returns ``None`` when *level* exceeds the package depth (i.e. the
|
|
23
|
+
import would escape *project_root*).
|
|
24
|
+
"""
|
|
25
|
+
relative = file_path.relative_to(project_root)
|
|
26
|
+
parts = list(relative.with_suffix("").parts)
|
|
27
|
+
parts = parts[:-1]
|
|
28
|
+
|
|
29
|
+
# level=1 means current package, level=2 means parent package, etc.
|
|
30
|
+
go_up = level - 1
|
|
31
|
+
if go_up >= len(parts):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
base_parts = parts[: len(parts) - go_up]
|
|
35
|
+
resolved = ".".join(base_parts)
|
|
36
|
+
if module:
|
|
37
|
+
resolved = f"{resolved}.{module}" if resolved else module
|
|
38
|
+
return resolved or None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_imports(file_path: Path, project_root: Path) -> list[ImportInfo]:
|
|
42
|
+
source = file_path.read_text()
|
|
43
|
+
tree = ast.parse(source, filename=str(file_path))
|
|
44
|
+
|
|
45
|
+
imports = []
|
|
46
|
+
for node in ast.walk(tree):
|
|
47
|
+
if isinstance(node, ast.Import):
|
|
48
|
+
for alias in node.names:
|
|
49
|
+
imports.append(ImportInfo(module=alias.name, lineno=node.lineno))
|
|
50
|
+
elif isinstance(node, ast.ImportFrom):
|
|
51
|
+
if node.level and node.level > 0:
|
|
52
|
+
resolved = _resolve_relative_import(
|
|
53
|
+
file_path, project_root, node.level, node.module
|
|
54
|
+
)
|
|
55
|
+
if resolved is not None:
|
|
56
|
+
imports.append(ImportInfo(module=resolved, lineno=node.lineno))
|
|
57
|
+
elif node.module is not None:
|
|
58
|
+
imports.append(ImportInfo(module=node.module, lineno=node.lineno))
|
|
59
|
+
|
|
60
|
+
return imports
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .domain import models # noqa: F401
|