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.
Files changed (57) hide show
  1. python_dependency_linter-0.3.0/.claude/skills/commit/SKILL.md +24 -0
  2. python_dependency_linter-0.3.0/.claude/skills/release/SKILL.md +14 -0
  3. python_dependency_linter-0.3.0/.github/pull_request_template.md +27 -0
  4. python_dependency_linter-0.3.0/CHANGELOG.md +73 -0
  5. python_dependency_linter-0.3.0/CLAUDE.md +9 -0
  6. python_dependency_linter-0.3.0/CONTRIBUTING.md +84 -0
  7. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/PKG-INFO +39 -1
  8. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/README.md +38 -0
  9. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/cli.py +40 -5
  10. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/config.py +12 -2
  11. python_dependency_linter-0.3.0/python_dependency_linter/parser.py +60 -0
  12. python_dependency_linter-0.3.0/tests/fixtures/sample_project/contexts/boards/__init__.py +1 -0
  13. python_dependency_linter-0.3.0/tests/fixtures/sample_project/contexts/boards/adapters/repository.py +7 -0
  14. python_dependency_linter-0.3.0/tests/test_cli.py +171 -0
  15. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/test_config.py +40 -0
  16. python_dependency_linter-0.3.0/tests/test_parser.py +74 -0
  17. python_dependency_linter-0.3.0/uv.lock +368 -0
  18. python_dependency_linter-0.2.0/.github/pull_request_template.md +0 -13
  19. python_dependency_linter-0.2.0/CHANGELOG.md +0 -5
  20. python_dependency_linter-0.2.0/python_dependency_linter/parser.py +0 -27
  21. python_dependency_linter-0.2.0/tests/fixtures/sample_project/contexts/boards/adapters/repository.py +0 -3
  22. python_dependency_linter-0.2.0/tests/fixtures/sample_project/contexts/boards/domain/__init__.py +0 -0
  23. python_dependency_linter-0.2.0/tests/test_cli.py +0 -69
  24. python_dependency_linter-0.2.0/tests/test_parser.py +0 -30
  25. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  26. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  27. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  28. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/dependabot.yml +0 -0
  29. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/workflows/ci.yaml +0 -0
  30. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.github/workflows/publish.yaml +0 -0
  31. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.gitignore +0 -0
  32. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.pre-commit-config.yaml +0 -0
  33. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/.pre-commit-hooks.yaml +0 -0
  34. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/LICENSE +0 -0
  35. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/pyproject.toml +0 -0
  36. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/__init__.py +0 -0
  37. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/checker.py +0 -0
  38. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/matcher.py +0 -0
  39. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/reporter.py +0 -0
  40. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/python_dependency_linter/resolver.py +0 -0
  41. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_config.yaml +0 -0
  42. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/__init__.py +0 -0
  43. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/__init__.py +0 -0
  44. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/application/__init__.py +0 -0
  45. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/application/service.py +0 -0
  46. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/domain/__init__.py +0 -0
  47. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/auth/domain/models.py +0 -0
  48. {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
  49. {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
  50. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/boards/application/service.py +0 -0
  51. {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
  52. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_project/contexts/boards/domain/models.py +0 -0
  53. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/fixtures/sample_pyproject.toml +0 -0
  54. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/test_checker.py +0 -0
  55. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/test_matcher.py +0 -0
  56. {python_dependency_linter-0.2.0 → python_dependency_linter-0.3.0}/tests/test_reporter.py +0 -0
  57. {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,9 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ Python dependency linter (`pdl`) - A CLI tool that lints layer dependency rules in Python projects.
6
+
7
+ ## Contributing
8
+
9
+ Follow the conventions in [CONTRIBUTING.md](./CONTRIBUTING.md).
@@ -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.2.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
 
@@ -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 _find_python_files(project_root: Path) -> list[Path]:
40
- return sorted(project_root.rglob("*.py"))
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(1)
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:
@@ -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(rules=_parse_rules(data["rules"]))
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(rules=_parse_rules(tool_config["rules"]))
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
@@ -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