django-orm-lens 1.2.0__tar.gz → 1.2.1__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 (37) hide show
  1. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/PKG-INFO +1 -1
  2. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/cli.py +7 -3
  3. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens.egg-info/PKG-INFO +1 -1
  4. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens.egg-info/SOURCES.txt +1 -0
  5. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/pyproject.toml +1 -1
  6. django_orm_lens-1.2.1/tests/test_cli_subcommands_smoke.py +118 -0
  7. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/README.md +0 -0
  8. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/__init__.py +0 -0
  9. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/diff.py +0 -0
  10. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/formatters.py +0 -0
  11. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/mcp_server.py +0 -0
  12. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/migrations_parser.py +0 -0
  13. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/models.py +0 -0
  14. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/parser.py +0 -0
  15. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/py.typed +0 -0
  16. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/query_analyzer.py +0 -0
  17. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens/signals_parser.py +0 -0
  18. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens.egg-info/dependency_links.txt +0 -0
  19. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens.egg-info/entry_points.txt +0 -0
  20. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens.egg-info/requires.txt +0 -0
  21. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/django_orm_lens.egg-info/top_level.txt +0 -0
  22. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/setup.cfg +0 -0
  23. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_abstract_and_auth_user.py +0 -0
  24. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_cli_hint.py +0 -0
  25. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_cli_verbose.py +0 -0
  26. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_cross_module_bases.py +0 -0
  27. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_diff_command.py +0 -0
  28. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_golden_fixtures.py +0 -0
  29. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_kwarg_order_and_auth_user.py +0 -0
  30. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_list_command.py +0 -0
  31. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_migration_risk.py +0 -0
  32. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_migrations_parser.py +0 -0
  33. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_nplusone_detector.py +0 -0
  34. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_parity.py +0 -0
  35. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_query_analyzer.py +0 -0
  36. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_scan_root.py +0 -0
  37. {django_orm_lens-1.2.0 → django_orm_lens-1.2.1}/tests/test_signals_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-orm-lens
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Static analysis + MCP server for Django models. Sidebar tree, ER diagrams, and JSON output for terminals and AI coding agents.
5
5
  Author: frowningdev
6
6
  License: MIT
@@ -23,6 +23,8 @@ from typing import List, Optional, Sequence
23
23
  from . import __version__
24
24
  from .diff import diff_schemas, format_diff
25
25
  from .formatters import format_hover, format_index, format_model
26
+ from .migrations_parser import analyze_migration_risks
27
+ from .query_analyzer import scan_for_nplusone
26
28
  from .models import (
27
29
  ParsedModel,
28
30
  WorkspaceIndex,
@@ -205,8 +207,10 @@ def _cmd_diff(args: argparse.Namespace) -> int:
205
207
  def _cmd_nplusone(args: argparse.Namespace) -> int:
206
208
  """Static N+1 detector — flag FK/M2M access inside loops without select/prefetch."""
207
209
  idx = _load_index(args)
208
- schema = _build_schema_from_index(idx)
209
- findings = scan_for_nplusone(args.path, schema)
210
+ # scan_for_nplusone accepts a WorkspaceIndex directly (see
211
+ # query_analyzer._normalise_schema) — no need to pre-flatten. Passing idx
212
+ # avoids importing the private _build_schema_from_index helper here.
213
+ findings = scan_for_nplusone(args.path, idx)
210
214
  if args.confidence == "high":
211
215
  findings = [f for f in findings if f.confidence == "high"]
212
216
  elif args.confidence == "medium":
@@ -248,7 +252,7 @@ def _cmd_migration_risk(args: argparse.Namespace) -> int:
248
252
  print("No risky migration operations found.")
249
253
  for f in findings:
250
254
  print(
251
- f"{f.filePath}:{f.lineNumber} [{f.severity}/{f.confidence}] "
255
+ f"{f.file_path}:{f.line_number} [{f.severity}/{f.confidence}] "
252
256
  f"{f.operation} {f.app}.{f.model}"
253
257
  + (f".{f.field}" if f.field else "")
254
258
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-orm-lens
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Static analysis + MCP server for Django models. Sidebar tree, ER diagrams, and JSON output for terminals and AI coding agents.
5
5
  Author: frowningdev
6
6
  License: MIT
@@ -19,6 +19,7 @@ django_orm_lens.egg-info/requires.txt
19
19
  django_orm_lens.egg-info/top_level.txt
20
20
  tests/test_abstract_and_auth_user.py
21
21
  tests/test_cli_hint.py
22
+ tests/test_cli_subcommands_smoke.py
22
23
  tests/test_cli_verbose.py
23
24
  tests/test_cross_module_bases.py
24
25
  tests/test_diff_command.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "django-orm-lens"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Static analysis + MCP server for Django models. Sidebar tree, ER diagrams, and JSON output for terminals and AI coding agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,118 @@
1
+ """Regression: every CLI subcommand must import and execute cleanly.
2
+
3
+ The v0.6.0 → v0.7.0 audit surfaced two bugs where a subcommand's body
4
+ referenced module-level names that were never imported (or that used the
5
+ wrong attribute case) — the unit tests still passed because they exercised
6
+ the helper functions directly, never the argparse dispatch path.
7
+
8
+ This suite runs each subcommand end-to-end via ``main([...])`` against the
9
+ miniapp fixture and the migration_risks fixture. Any `NameError`,
10
+ `ImportError`, or `AttributeError` in a subcommand body surfaces as a
11
+ test failure instead of a crash the next user sees.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import contextlib
17
+ import io
18
+ import json as _json
19
+ import unittest
20
+ from pathlib import Path
21
+
22
+ from django_orm_lens.cli import main
23
+
24
+ FIXTURE_ROOT = Path(__file__).parent / "fixtures"
25
+ MINIAPP = FIXTURE_ROOT / "miniapp"
26
+ MIGRATION_RISKS = FIXTURE_ROOT / "migration_risks"
27
+
28
+
29
+ def _run(argv):
30
+ out, err = io.StringIO(), io.StringIO()
31
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
32
+ exit_code = main(argv)
33
+ return exit_code, out.getvalue(), err.getvalue()
34
+
35
+
36
+ class SubcommandsSmokeTest(unittest.TestCase):
37
+ """Every subcommand runs without a NameError / ImportError / AttributeError."""
38
+
39
+ def test_scan_runs(self) -> None:
40
+ code, out, _err = _run(
41
+ ["scan", "--path", str(MINIAPP), "--format", "table"]
42
+ )
43
+ self.assertEqual(code, 0)
44
+ self.assertIn("Order", out)
45
+
46
+ def test_list_runs(self) -> None:
47
+ code, out, _err = _run(["list", "--path", str(MINIAPP)])
48
+ self.assertEqual(code, 0)
49
+ self.assertIn("orders.Order", out)
50
+
51
+ def test_er_runs(self) -> None:
52
+ code, out, _err = _run(["er", "--path", str(MINIAPP)])
53
+ self.assertEqual(code, 0)
54
+ self.assertIn("erDiagram", out)
55
+
56
+ def test_describe_runs(self) -> None:
57
+ code, out, _err = _run(
58
+ ["describe", "orders.Order", "--path", str(MINIAPP)]
59
+ )
60
+ self.assertEqual(code, 0)
61
+ self.assertIn("Order", out)
62
+
63
+ def test_hover_runs(self) -> None:
64
+ code, out, _err = _run(
65
+ ["hover", "orders.Order", "--path", str(MINIAPP)]
66
+ )
67
+ self.assertEqual(code, 0)
68
+ self.assertIn("Order", out)
69
+
70
+ def test_nplusone_runs(self) -> None:
71
+ # Miniapp has no n+1 patterns — but the subcommand must dispatch
72
+ # without a NameError (the pre-v0.7 bug was scan_for_nplusone +
73
+ # _build_schema_from_index not being imported at module level).
74
+ code, out, _err = _run(
75
+ ["nplusone", "--path", str(MINIAPP), "--exit-zero"]
76
+ )
77
+ self.assertEqual(code, 0)
78
+ self.assertIn("N+1", out)
79
+
80
+ def test_migration_risk_text_format(self) -> None:
81
+ # Text format previously crashed with AttributeError because the
82
+ # print used camelCase attrs (filePath / lineNumber) instead of the
83
+ # snake_case dataclass fields (file_path / line_number).
84
+ code, out, _err = _run(
85
+ [
86
+ "migration-risk",
87
+ "--path",
88
+ str(MIGRATION_RISKS / "add_field_notnull"),
89
+ "--exit-zero",
90
+ ]
91
+ )
92
+ self.assertEqual(code, 0)
93
+ # A finding should be printed with "file:line [severity/confidence]".
94
+ self.assertTrue(
95
+ any(":" in line and "[" in line for line in out.splitlines()),
96
+ f"expected 'file:line [severity/confidence]' line in output, got: {out!r}",
97
+ )
98
+
99
+ def test_migration_risk_json_format(self) -> None:
100
+ # JSON path uses to_dict() — separate from the text path bug,
101
+ # but worth pinning too.
102
+ code, out, _err = _run(
103
+ [
104
+ "migration-risk",
105
+ "--path",
106
+ str(MIGRATION_RISKS / "add_field_notnull"),
107
+ "--format",
108
+ "json",
109
+ "--exit-zero",
110
+ ]
111
+ )
112
+ self.assertEqual(code, 0)
113
+ payload = _json.loads(out)
114
+ self.assertIn("findings", payload)
115
+
116
+
117
+ if __name__ == "__main__":
118
+ unittest.main()