python-dependency-linter 0.3.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.
Files changed (50) hide show
  1. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/CHANGELOG.md +21 -0
  2. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/PKG-INFO +7 -10
  3. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/README.md +6 -9
  4. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/python_dependency_linter/cli.py +24 -13
  5. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/python_dependency_linter/config.py +31 -2
  6. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/test_cli.py +92 -24
  7. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/test_config.py +44 -1
  8. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.claude/skills/commit/SKILL.md +0 -0
  9. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.claude/skills/release/SKILL.md +0 -0
  10. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  11. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  12. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  13. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.github/dependabot.yml +0 -0
  14. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.github/pull_request_template.md +0 -0
  15. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.github/workflows/ci.yaml +0 -0
  16. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.github/workflows/publish.yaml +0 -0
  17. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.gitignore +0 -0
  18. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.pre-commit-config.yaml +0 -0
  19. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/.pre-commit-hooks.yaml +0 -0
  20. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/CLAUDE.md +0 -0
  21. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/CONTRIBUTING.md +0 -0
  22. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/LICENSE +0 -0
  23. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/pyproject.toml +0 -0
  24. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/python_dependency_linter/__init__.py +0 -0
  25. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/python_dependency_linter/checker.py +0 -0
  26. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/python_dependency_linter/matcher.py +0 -0
  27. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/python_dependency_linter/parser.py +0 -0
  28. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/python_dependency_linter/reporter.py +0 -0
  29. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/python_dependency_linter/resolver.py +0 -0
  30. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_config.yaml +0 -0
  31. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/__init__.py +0 -0
  32. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/__init__.py +0 -0
  33. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/application/__init__.py +0 -0
  34. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/application/service.py +0 -0
  35. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/domain/__init__.py +0 -0
  36. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/auth/domain/models.py +0 -0
  37. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/__init__.py +0 -0
  38. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/adapters/__init__.py +0 -0
  39. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/adapters/repository.py +0 -0
  40. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/application/__init__.py +0 -0
  41. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/application/service.py +0 -0
  42. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/domain/__init__.py +0 -0
  43. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_project/contexts/boards/domain/models.py +0 -0
  44. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/fixtures/sample_pyproject.toml +0 -0
  45. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/test_checker.py +0 -0
  46. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/test_matcher.py +0 -0
  47. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/test_parser.py +0 -0
  48. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/test_reporter.py +0 -0
  49. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/tests/test_resolver.py +0 -0
  50. {python_dependency_linter-0.3.0 → python_dependency_linter-0.4.0}/uv.lock +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
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
5
26
  ## [0.2.0] - 2026-03-30
6
27
 
7
28
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-dependency-linter
3
- Version: 0.3.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
@@ -309,14 +309,11 @@ third_party = ["boto3"]
309
309
  ## CLI
310
310
 
311
311
  ```bash
312
- # Check with default config (.python-dependency-linter.yaml)
312
+ # Check with auto-discovered config (searches upward from cwd)
313
313
  pdl check
314
314
 
315
- # Specify config file
315
+ # Specify config file (project root = config file's parent directory)
316
316
  pdl check --config path/to/config.yaml
317
-
318
- # Specify project root
319
- pdl check --project-root path/to/project
320
317
  ```
321
318
 
322
319
  Exit codes:
@@ -325,10 +322,10 @@ Exit codes:
325
322
  - `1` — Violations found
326
323
  - `2` — Config file not found
327
324
 
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`:
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`:
329
326
 
330
327
  ```
331
- Error: Config file not found: .python-dependency-linter.yaml
328
+ Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml.
332
329
  ```
333
330
 
334
331
  ## Pre-commit
@@ -342,14 +339,14 @@ Add to `.pre-commit-config.yaml`:
342
339
  - id: python-dependency-linter
343
340
  ```
344
341
 
345
- To pass custom options (e.g., a different config file or project root):
342
+ To pass custom options (e.g., a different config file):
346
343
 
347
344
  ```yaml
348
345
  - repo: https://github.com/heumsi/python-dependency-linter
349
346
  rev: v0.1.0
350
347
  hooks:
351
348
  - id: python-dependency-linter
352
- args: [--config, custom-config.yaml, --project-root, src]
349
+ args: [--config, custom-config.yaml]
353
350
  ```
354
351
 
355
352
  ## License
@@ -284,14 +284,11 @@ third_party = ["boto3"]
284
284
  ## CLI
285
285
 
286
286
  ```bash
287
- # Check with default config (.python-dependency-linter.yaml)
287
+ # Check with auto-discovered config (searches upward from cwd)
288
288
  pdl check
289
289
 
290
- # Specify config file
290
+ # Specify config file (project root = config file's parent directory)
291
291
  pdl check --config path/to/config.yaml
292
-
293
- # Specify project root
294
- pdl check --project-root path/to/project
295
292
  ```
296
293
 
297
294
  Exit codes:
@@ -300,10 +297,10 @@ Exit codes:
300
297
  - `1` — Violations found
301
298
  - `2` — Config file not found
302
299
 
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`:
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`:
304
301
 
305
302
  ```
306
- Error: Config file not found: .python-dependency-linter.yaml
303
+ Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml.
307
304
  ```
308
305
 
309
306
  ## Pre-commit
@@ -317,14 +314,14 @@ Add to `.pre-commit-config.yaml`:
317
314
  - id: python-dependency-linter
318
315
  ```
319
316
 
320
- To pass custom options (e.g., a different config file or project root):
317
+ To pass custom options (e.g., a different config file):
321
318
 
322
319
  ```yaml
323
320
  - repo: https://github.com/heumsi/python-dependency-linter
324
321
  rev: v0.1.0
325
322
  hooks:
326
323
  - id: python-dependency-linter
327
- args: [--config, custom-config.yaml, --project-root, src]
324
+ args: [--config, custom-config.yaml]
328
325
  ```
329
326
 
330
327
  ## License
@@ -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
@@ -46,7 +47,7 @@ def _normalize_pattern(pattern: str, project_root: Path) -> str:
46
47
 
47
48
 
48
49
  def _matches_any(path: Path, patterns: list[str]) -> bool:
49
- return any(path.match(p) for p in patterns)
50
+ return any(fnmatch.fnmatch(str(path), p) for p in patterns)
50
51
 
51
52
 
52
53
  def _find_python_files(
@@ -84,19 +85,29 @@ def main():
84
85
  @click.option(
85
86
  "--config",
86
87
  "config_path",
87
- default=".python-dependency-linter.yaml",
88
+ default=None,
88
89
  help="Path to config file.",
89
90
  )
90
- @click.option("--project-root", default=".", help="Project root directory.")
91
- def check(config_path: str, project_root: str):
92
- root = Path(project_root).resolve()
93
- config_file = Path(config_path)
94
-
95
- try:
96
- config = load_config(config_file)
97
- except FileNotFoundError as e:
98
- click.echo(f"Error: {e}", err=True)
99
- raise SystemExit(2)
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)
100
111
 
101
112
  all_violations = []
102
113
  python_files = _find_python_files(root, config.include, config.exclude)
@@ -62,14 +62,18 @@ def _load_yaml(path: Path) -> Config:
62
62
  )
63
63
 
64
64
 
65
- def _load_pyproject_toml(path: Path) -> Config:
65
+ def _load_toml(path: Path) -> dict:
66
66
  try:
67
67
  import tomllib
68
68
  except ImportError:
69
69
  import tomli as tomllib # type: ignore[no-redef]
70
70
 
71
71
  with open(path, "rb") as f:
72
- data = tomllib.load(f)
72
+ return tomllib.load(f)
73
+
74
+
75
+ def _load_pyproject_toml(path: Path) -> Config:
76
+ data = _load_toml(path)
73
77
  tool_config = data["tool"]["python-dependency-linter"]
74
78
  return Config(
75
79
  rules=_parse_rules(tool_config["rules"]),
@@ -78,6 +82,31 @@ def _load_pyproject_toml(path: Path) -> Config:
78
82
  )
79
83
 
80
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
108
+
109
+
81
110
  def load_config(path: Path) -> Config:
82
111
  if not path.exists():
83
112
  raise FileNotFoundError(f"Config file not found: {path}")
@@ -7,7 +7,7 @@ from python_dependency_linter.cli import main
7
7
  FIXTURES = Path(__file__).parent / "fixtures"
8
8
 
9
9
 
10
- def test_cli_check_with_violations(tmp_path):
10
+ def test_cli_check_with_violations(tmp_path, monkeypatch):
11
11
  config_content = """\
12
12
  rules:
13
13
  - name: domain-isolation
@@ -27,16 +27,15 @@ rules:
27
27
  dst = tmp_path / "contexts"
28
28
  shutil.copytree(src, dst)
29
29
 
30
+ monkeypatch.chdir(tmp_path)
30
31
  runner = CliRunner()
31
- result = runner.invoke(
32
- main, ["check", "--config", str(config_file), "--project-root", str(tmp_path)]
33
- )
32
+ result = runner.invoke(main, ["check"])
34
33
  assert result.exit_code == 1
35
34
  assert "[domain-isolation]" in result.output
36
35
  assert "Found" in result.output
37
36
 
38
37
 
39
- def test_cli_check_no_violations(tmp_path):
38
+ def test_cli_check_no_violations(tmp_path, monkeypatch):
40
39
  config_content = """\
41
40
  rules:
42
41
  - name: allow-all
@@ -55,15 +54,14 @@ rules:
55
54
  dst = tmp_path / "contexts"
56
55
  shutil.copytree(src, dst)
57
56
 
57
+ monkeypatch.chdir(tmp_path)
58
58
  runner = CliRunner()
59
- result = runner.invoke(
60
- main, ["check", "--config", str(config_file), "--project-root", str(tmp_path)]
61
- )
59
+ result = runner.invoke(main, ["check"])
62
60
  assert result.exit_code == 0
63
61
  assert "No violations found." in result.output
64
62
 
65
63
 
66
- def test_cli_check_with_include(tmp_path):
64
+ def test_cli_check_with_include(tmp_path, monkeypatch):
67
65
  """Files outside include paths should be skipped."""
68
66
  config_content = """\
69
67
  include:
@@ -74,7 +72,7 @@ rules:
74
72
  deny:
75
73
  third_party: [pydantic]
76
74
  """
77
- config_file = tmp_path / "config.yaml"
75
+ config_file = tmp_path / ".python-dependency-linter.yaml"
78
76
  config_file.write_text(config_content)
79
77
 
80
78
  # Create files inside and outside include path
@@ -88,16 +86,52 @@ rules:
88
86
  (other / "__init__.py").write_text("")
89
87
  (other / "app.py").write_text("import pydantic\n")
90
88
 
89
+ monkeypatch.chdir(tmp_path)
91
90
  runner = CliRunner()
92
- result = runner.invoke(
93
- main, ["check", "--config", str(config_file), "--project-root", str(tmp_path)]
94
- )
91
+ result = runner.invoke(main, ["check"])
95
92
  assert result.exit_code == 1
96
93
  assert "src/app.py" in result.output
97
94
  assert "other/app.py" not in result.output
98
95
 
99
96
 
100
- def test_cli_check_with_exclude(tmp_path):
97
+ def test_cli_check_with_include_nested(tmp_path, monkeypatch):
98
+ """Include should match files in deeply nested subdirectories."""
99
+ config_content = """\
100
+ include:
101
+ - src
102
+ rules:
103
+ - name: domain-isolation
104
+ modules: "**"
105
+ deny:
106
+ third_party: [pydantic]
107
+ """
108
+ config_file = tmp_path / ".python-dependency-linter.yaml"
109
+ config_file.write_text(config_content)
110
+
111
+ # Create deeply nested files inside include path
112
+ nested = tmp_path / "src" / "contexts" / "analytics" / "domain"
113
+ nested.mkdir(parents=True)
114
+ (tmp_path / "src" / "__init__.py").write_text("")
115
+ (tmp_path / "src" / "contexts" / "__init__.py").write_text("")
116
+ (tmp_path / "src" / "contexts" / "analytics" / "__init__.py").write_text("")
117
+ (nested / "__init__.py").write_text("")
118
+ (nested / "models.py").write_text("import pydantic\n")
119
+
120
+ # Create files outside include path
121
+ other = tmp_path / "other"
122
+ other.mkdir()
123
+ (other / "__init__.py").write_text("")
124
+ (other / "app.py").write_text("import pydantic\n")
125
+
126
+ monkeypatch.chdir(tmp_path)
127
+ runner = CliRunner()
128
+ result = runner.invoke(main, ["check"])
129
+ assert result.exit_code == 1
130
+ assert "src/contexts/analytics/domain/models.py" in result.output
131
+ assert "other/app.py" not in result.output
132
+
133
+
134
+ def test_cli_check_with_exclude(tmp_path, monkeypatch):
101
135
  """Files matching exclude patterns should be skipped."""
102
136
  config_content = """\
103
137
  exclude:
@@ -108,7 +142,7 @@ rules:
108
142
  deny:
109
143
  third_party: [pydantic]
110
144
  """
111
- config_file = tmp_path / "config.yaml"
145
+ config_file = tmp_path / ".python-dependency-linter.yaml"
112
146
  config_file.write_text(config_content)
113
147
 
114
148
  src = tmp_path / "src"
@@ -121,16 +155,15 @@ rules:
121
155
  (generated / "__init__.py").write_text("")
122
156
  (generated / "models.py").write_text("import pydantic\n")
123
157
 
158
+ monkeypatch.chdir(tmp_path)
124
159
  runner = CliRunner()
125
- result = runner.invoke(
126
- main, ["check", "--config", str(config_file), "--project-root", str(tmp_path)]
127
- )
160
+ result = runner.invoke(main, ["check"])
128
161
  assert result.exit_code == 1
129
162
  assert "src/app.py" in result.output
130
163
  assert "generated/" not in result.output
131
164
 
132
165
 
133
- def test_cli_check_with_include_and_exclude(tmp_path):
166
+ def test_cli_check_with_include_and_exclude(tmp_path, monkeypatch):
134
167
  """Exclude should filter within included paths."""
135
168
  config_content = """\
136
169
  include:
@@ -143,7 +176,7 @@ rules:
143
176
  deny:
144
177
  third_party: [pydantic]
145
178
  """
146
- config_file = tmp_path / "config.yaml"
179
+ config_file = tmp_path / ".python-dependency-linter.yaml"
147
180
  config_file.write_text(config_content)
148
181
 
149
182
  app = tmp_path / "src"
@@ -156,16 +189,51 @@ rules:
156
189
  (generated / "__init__.py").write_text("")
157
190
  (generated / "models.py").write_text("import pydantic\n")
158
191
 
192
+ monkeypatch.chdir(tmp_path)
159
193
  runner = CliRunner()
160
- result = runner.invoke(
161
- main, ["check", "--config", str(config_file), "--project-root", str(tmp_path)]
162
- )
194
+ result = runner.invoke(main, ["check"])
163
195
  assert result.exit_code == 1
164
196
  assert "src/app.py" in result.output
165
197
  assert "generated/" not in result.output
166
198
 
167
199
 
168
- def test_cli_check_config_not_found():
200
+ def test_cli_check_config_not_found(tmp_path, monkeypatch):
201
+ monkeypatch.chdir(tmp_path)
202
+ runner = CliRunner()
203
+ result = runner.invoke(main, ["check"])
204
+ assert result.exit_code == 2
205
+
206
+
207
+ def test_cli_check_explicit_config_not_found():
169
208
  runner = CliRunner()
170
209
  result = runner.invoke(main, ["check", "--config", "nonexistent.yaml"])
171
210
  assert result.exit_code == 2
211
+ assert "not found" in result.output.lower()
212
+
213
+
214
+ def test_cli_check_with_explicit_config(tmp_path, monkeypatch):
215
+ """--config should use the config file's parent as project root."""
216
+ project_dir = tmp_path / "project"
217
+ project_dir.mkdir()
218
+
219
+ config_content = """\
220
+ rules:
221
+ - name: domain-isolation
222
+ modules: "**"
223
+ deny:
224
+ third_party: [pydantic]
225
+ """
226
+ config_file = project_dir / "custom-config.yaml"
227
+ config_file.write_text(config_content)
228
+
229
+ src = project_dir / "src"
230
+ src.mkdir()
231
+ (src / "__init__.py").write_text("")
232
+ (src / "app.py").write_text("import pydantic\n")
233
+
234
+ # Run from a different directory, but point --config to project_dir
235
+ monkeypatch.chdir(tmp_path)
236
+ runner = CliRunner()
237
+ result = runner.invoke(main, ["check", "--config", str(config_file)])
238
+ assert result.exit_code == 1
239
+ assert "src/app.py" in result.output
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from python_dependency_linter.config import load_config
3
+ from python_dependency_linter.config import find_config, load_config
4
4
 
5
5
  FIXTURES = Path(__file__).parent / "fixtures"
6
6
 
@@ -81,3 +81,46 @@ def test_load_config_file_not_found():
81
81
 
82
82
  with pytest.raises(FileNotFoundError):
83
83
  load_config(Path("nonexistent.yaml"))
84
+
85
+
86
+ def test_find_config_yaml_in_cwd(tmp_path, monkeypatch):
87
+ (tmp_path / ".python-dependency-linter.yaml").write_text("rules: []\n")
88
+ monkeypatch.chdir(tmp_path)
89
+ assert find_config() == tmp_path / ".python-dependency-linter.yaml"
90
+
91
+
92
+ def test_find_config_yaml_in_parent(tmp_path, monkeypatch):
93
+ (tmp_path / ".python-dependency-linter.yaml").write_text("rules: []\n")
94
+ child = tmp_path / "sub"
95
+ child.mkdir()
96
+ monkeypatch.chdir(child)
97
+ assert find_config() == tmp_path / ".python-dependency-linter.yaml"
98
+
99
+
100
+ def test_find_config_pyproject_toml(tmp_path, monkeypatch):
101
+ toml_content = "[tool.python-dependency-linter]\nrules = []\n"
102
+ (tmp_path / "pyproject.toml").write_text(toml_content)
103
+ monkeypatch.chdir(tmp_path)
104
+ assert find_config() == tmp_path / "pyproject.toml"
105
+
106
+
107
+ def test_find_config_yaml_preferred_over_toml(tmp_path, monkeypatch):
108
+ """When both exist in the same directory, YAML wins."""
109
+ (tmp_path / ".python-dependency-linter.yaml").write_text("rules: []\n")
110
+ toml_content = "[tool.python-dependency-linter]\nrules = []\n"
111
+ (tmp_path / "pyproject.toml").write_text(toml_content)
112
+ monkeypatch.chdir(tmp_path)
113
+ assert find_config() == tmp_path / ".python-dependency-linter.yaml"
114
+
115
+
116
+ def test_find_config_not_found(tmp_path, monkeypatch):
117
+ monkeypatch.chdir(tmp_path)
118
+ result = find_config()
119
+ assert result is None
120
+
121
+
122
+ def test_find_config_skips_pyproject_without_section(tmp_path, monkeypatch):
123
+ """pyproject.toml without [tool.python-dependency-linter] should be skipped."""
124
+ (tmp_path / "pyproject.toml").write_text("[tool.other]\nfoo = 1\n")
125
+ monkeypatch.chdir(tmp_path)
126
+ assert find_config() is None