django-orm-lens 1.2.0__tar.gz → 1.2.2__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.
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/PKG-INFO +1 -1
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/cli.py +11 -3
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens.egg-info/PKG-INFO +1 -1
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens.egg-info/SOURCES.txt +1 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/pyproject.toml +1 -1
- django_orm_lens-1.2.2/tests/test_cli_subcommands_smoke.py +118 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/README.md +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/__init__.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/diff.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/formatters.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/mcp_server.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/migrations_parser.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/models.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/parser.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/py.typed +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/query_analyzer.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens/signals_parser.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens.egg-info/dependency_links.txt +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens.egg-info/entry_points.txt +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens.egg-info/requires.txt +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens.egg-info/top_level.txt +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/setup.cfg +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_abstract_and_auth_user.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_cli_hint.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_cli_verbose.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_cross_module_bases.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_diff_command.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_golden_fixtures.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_kwarg_order_and_auth_user.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_list_command.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_migration_risk.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_migrations_parser.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_nplusone_detector.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_parity.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_query_analyzer.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_scan_root.py +0 -0
- {django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/tests/test_signals_parser.py +0 -0
|
@@ -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
|
-
|
|
209
|
-
|
|
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.
|
|
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
|
)
|
|
@@ -453,6 +457,10 @@ def _cmd_hello(_args: argparse.Namespace) -> int:
|
|
|
453
457
|
print("run `django-orm-lens <command> --help` for options.")
|
|
454
458
|
print("docs: https://github.com/FROWNINGdev/django-orm-lens")
|
|
455
459
|
print()
|
|
460
|
+
print("prefer a visual sidebar + ER diagram in your editor?")
|
|
461
|
+
print(" VS Code / Cursor / Windsurf: code --install-extension frowningdev.django-orm-lens")
|
|
462
|
+
print(" VSCodium / code-server: codium --install-extension frowningdev.django-orm-lens")
|
|
463
|
+
print()
|
|
456
464
|
print("if this saves you a search, star helps others find it:")
|
|
457
465
|
print(" https://github.com/FROWNINGdev/django-orm-lens")
|
|
458
466
|
return 0
|
|
@@ -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.
|
|
7
|
+
version = "1.2.2"
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_orm_lens-1.2.0 → django_orm_lens-1.2.2}/django_orm_lens.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|