python-dependency-linter 0.2.0__tar.gz → 0.3.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.3.0/.claude/skills/commit/SKILL.md +24 -0
- python_dependency_linter-0.3.0/.claude/skills/release/SKILL.md +14 -0
- python_dependency_linter-0.3.0/.github/pull_request_template.md +27 -0
- python_dependency_linter-0.3.0/CHANGELOG.md +73 -0
- python_dependency_linter-0.3.0/CLAUDE.md +9 -0
- python_dependency_linter-0.3.0/CONTRIBUTING.md +84 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/PKG-INFO +39 -1
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/README.md +38 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/cli.py +40 -5
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/config.py +12 -2
- python_dependency_linter-0.3.0/python_dependency_linter/parser.py +60 -0
- python_dependency_linter-0.3.0/tests/fixtures/sample_project/contexts/boards/__init__.py +1 -0
- python_dependency_linter-0.3.0/tests/fixtures/sample_project/contexts/boards/adapters/repository.py +7 -0
- python_dependency_linter-0.3.0/tests/test_cli.py +171 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/test_config.py +40 -0
- python_dependency_linter-0.3.0/tests/test_parser.py +74 -0
- python_dependency_linter-0.3.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_parser.py +0 -30
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/dependabot.yml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/workflows/ci.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/workflows/publish.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.gitignore +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.pre-commit-config.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.pre-commit-hooks.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/LICENSE +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/pyproject.toml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/checker.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/matcher.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/reporter.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/resolver.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_config.yaml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/application/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/application/service.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/domain/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.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.3.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.3.0/tests/fixtures/sample_project/contexts/boards/application}/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.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.3.0/tests/fixtures/sample_project/contexts/boards/domain}/__init__.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/boards/domain/models.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_pyproject.toml +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/test_checker.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/test_matcher.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/test_reporter.py +0 -0
- {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.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,73 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
### Documentation
|
|
8
|
+
|
|
9
|
+
- Remove design spec and implementation plan docs
|
|
10
|
+
- Add GitHub issue and PR templates
|
|
11
|
+
- Add example for omitted category behavior in allow rules
|
|
12
|
+
- Add architecture examples to README
|
|
13
|
+
- Add pre-commit hook custom options example
|
|
14
|
+
- Expand pyproject.toml examples with multiple rules and deny
|
|
15
|
+
- Add "What It Does" section to README
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- Support `**` glob pattern for matching nested submodules (#6)
|
|
20
|
+
- Add undocumented features to README
|
|
21
|
+
|
|
22
|
+
### Miscellaneous
|
|
23
|
+
|
|
24
|
+
- Use hatch-vcs for dynamic versioning from git tags
|
|
25
|
+
- Add gitmoji preprocessor to git-cliff config
|
|
26
|
+
- Move git-cliff config from cliff.toml to pyproject.toml
|
|
27
|
+
- Skip CHANGELOG update commits in git-cliff output
|
|
28
|
+
|
|
29
|
+
### Testing
|
|
30
|
+
|
|
31
|
+
- Limit CI to source and test file changes
|
|
32
|
+
|
|
33
|
+
### Build
|
|
34
|
+
|
|
35
|
+
- Bump actions/checkout from 4 to 6 (#3)
|
|
36
|
+
- Bump actions/setup-python from 5 to 6 (#2)
|
|
37
|
+
## [0.1.0] - 2026-03-30
|
|
38
|
+
|
|
39
|
+
### Bug Fixes
|
|
40
|
+
|
|
41
|
+
- Add isort config and fix plan typo
|
|
42
|
+
- Add tomli fallback for Python 3.10 compatibility
|
|
43
|
+
- Add import content to fixture files
|
|
44
|
+
|
|
45
|
+
### CI/CD
|
|
46
|
+
|
|
47
|
+
- Add GitHub Actions for CI, PyPI publish, and Dependabot
|
|
48
|
+
- Add git-cliff changelog generation on release
|
|
49
|
+
- Add GitHub Release creation on tag
|
|
50
|
+
|
|
51
|
+
### Documentation
|
|
52
|
+
|
|
53
|
+
- Add design spec for python-dependency-linter
|
|
54
|
+
- Add implementation plan
|
|
55
|
+
- Add README
|
|
56
|
+
- Add MIT LICENSE
|
|
57
|
+
|
|
58
|
+
### Features
|
|
59
|
+
|
|
60
|
+
- Add config loading for YAML and pyproject.toml
|
|
61
|
+
- Add AST-based import parser
|
|
62
|
+
- Add import resolver for classification
|
|
63
|
+
- Add wildcard matcher and rule merging
|
|
64
|
+
- Add dependency checker with allow/deny logic
|
|
65
|
+
- Add violation reporter
|
|
66
|
+
- Add CLI with check command
|
|
67
|
+
- Add pre-commit hook definition
|
|
68
|
+
|
|
69
|
+
### Miscellaneous
|
|
70
|
+
|
|
71
|
+
- Initialize project scaffolding
|
|
72
|
+
- Add license, readme, and classifiers to pyproject.toml
|
|
73
|
+
- 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.3.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
|
|
@@ -292,6 +323,13 @@ Exit codes:
|
|
|
292
323
|
|
|
293
324
|
- `0` — No violations
|
|
294
325
|
- `1` — Violations found
|
|
326
|
+
- `2` — Config file not found
|
|
327
|
+
|
|
328
|
+
If no `--config` is given, the tool looks for `.python-dependency-linter.yaml` in the current directory. If the config file does not exist, the tool prints an error and exits with code `2`:
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
Error: Config file not found: .python-dependency-linter.yaml
|
|
332
|
+
```
|
|
295
333
|
|
|
296
334
|
## Pre-commit
|
|
297
335
|
|
|
@@ -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
|
|
@@ -267,6 +298,13 @@ Exit codes:
|
|
|
267
298
|
|
|
268
299
|
- `0` — No violations
|
|
269
300
|
- `1` — Violations found
|
|
301
|
+
- `2` — Config file not found
|
|
302
|
+
|
|
303
|
+
If no `--config` is given, the tool looks for `.python-dependency-linter.yaml` in the current directory. If the config file does not exist, the tool prints an error and exits with code `2`:
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
Error: Config file not found: .python-dependency-linter.yaml
|
|
307
|
+
```
|
|
270
308
|
|
|
271
309
|
## Pre-commit
|
|
272
310
|
|
{python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/cli.py
RENAMED
|
@@ -36,8 +36,43 @@ def _package_module(file_path: Path, project_root: Path) -> str:
|
|
|
36
36
|
return ".".join(parts)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def
|
|
40
|
-
|
|
39
|
+
def _normalize_pattern(pattern: str, project_root: Path) -> str:
|
|
40
|
+
"""Normalize a pattern so that bare directory names match all files within."""
|
|
41
|
+
clean = pattern.rstrip("/")
|
|
42
|
+
candidate = project_root / clean
|
|
43
|
+
if candidate.is_dir() or not any(c in clean for c in ("*", "?")):
|
|
44
|
+
clean = f"{clean}/**"
|
|
45
|
+
return clean
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _matches_any(path: Path, patterns: list[str]) -> bool:
|
|
49
|
+
return any(path.match(p) for p in patterns)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _find_python_files(
|
|
53
|
+
project_root: Path,
|
|
54
|
+
include: list[str] | None = None,
|
|
55
|
+
exclude: list[str] | None = None,
|
|
56
|
+
) -> list[Path]:
|
|
57
|
+
all_files = sorted(project_root.rglob("*.py"))
|
|
58
|
+
|
|
59
|
+
if include is not None:
|
|
60
|
+
normalized = [_normalize_pattern(p, project_root) for p in include]
|
|
61
|
+
all_files = [
|
|
62
|
+
f
|
|
63
|
+
for f in all_files
|
|
64
|
+
if _matches_any(f.relative_to(project_root), normalized)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
if exclude is not None:
|
|
68
|
+
normalized = [_normalize_pattern(p, project_root) for p in exclude]
|
|
69
|
+
all_files = [
|
|
70
|
+
f
|
|
71
|
+
for f in all_files
|
|
72
|
+
if not _matches_any(f.relative_to(project_root), normalized)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
return all_files
|
|
41
76
|
|
|
42
77
|
|
|
43
78
|
@click.group()
|
|
@@ -61,10 +96,10 @@ def check(config_path: str, project_root: str):
|
|
|
61
96
|
config = load_config(config_file)
|
|
62
97
|
except FileNotFoundError as e:
|
|
63
98
|
click.echo(f"Error: {e}", err=True)
|
|
64
|
-
raise SystemExit(
|
|
99
|
+
raise SystemExit(2)
|
|
65
100
|
|
|
66
101
|
all_violations = []
|
|
67
|
-
python_files = _find_python_files(root)
|
|
102
|
+
python_files = _find_python_files(root, config.include, config.exclude)
|
|
68
103
|
|
|
69
104
|
for file_path in python_files:
|
|
70
105
|
module = _file_to_module(file_path, root)
|
|
@@ -74,7 +109,7 @@ def check(config_path: str, project_root: str):
|
|
|
74
109
|
continue
|
|
75
110
|
|
|
76
111
|
merged_rule = merge_rules(matching_rules)
|
|
77
|
-
imports = parse_imports(file_path)
|
|
112
|
+
imports = parse_imports(file_path, root)
|
|
78
113
|
|
|
79
114
|
file_violations = []
|
|
80
115
|
for imp in imports:
|
{python_dependency_linter-0.2.0 → python_dependency_linter-0.3.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,7 +55,11 @@ 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
65
|
def _load_pyproject_toml(path: Path) -> Config:
|
|
@@ -65,7 +71,11 @@ def _load_pyproject_toml(path: Path) -> Config:
|
|
|
65
71
|
with open(path, "rb") as f:
|
|
66
72
|
data = tomllib.load(f)
|
|
67
73
|
tool_config = data["tool"]["python-dependency-linter"]
|
|
68
|
-
return Config(
|
|
74
|
+
return Config(
|
|
75
|
+
rules=_parse_rules(tool_config["rules"]),
|
|
76
|
+
include=tool_config.get("include"),
|
|
77
|
+
exclude=tool_config.get("exclude"),
|
|
78
|
+
)
|
|
69
79
|
|
|
70
80
|
|
|
71
81
|
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
|
python_dependency_linter-0.3.0/tests/fixtures/sample_project/contexts/boards/adapters/repository.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from sqlalchemy import Column # noqa
|
|
2
|
+
from contexts.boards.domain.models import Board # noqa
|
|
3
|
+
from contexts.boards.application.service import BoardService # noqa
|
|
4
|
+
from .repository_utils import helper # noqa: F401
|
|
5
|
+
from . import __init__ # noqa: F401
|
|
6
|
+
from ..domain import models # noqa: F401
|
|
7
|
+
from ....outside import something # noqa: F401
|