django-orm-lens 1.2.4__tar.gz → 1.2.6__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 (41) hide show
  1. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/PKG-INFO +1 -1
  2. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/parser.py +15 -2
  3. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens.egg-info/PKG-INFO +1 -1
  4. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens.egg-info/SOURCES.txt +3 -1
  5. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/pyproject.toml +1 -1
  6. django_orm_lens-1.2.6/tests/test_aliased_module_and_third_party_fields.py +94 -0
  7. django_orm_lens-1.2.6/tests/test_tab_indented_models.py +54 -0
  8. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/README.md +0 -0
  9. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/__init__.py +0 -0
  10. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/cli.py +0 -0
  11. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/diff.py +0 -0
  12. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/formatters.py +0 -0
  13. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/mcp_server.py +0 -0
  14. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/migrations_parser.py +0 -0
  15. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/models.py +0 -0
  16. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/py.typed +0 -0
  17. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/query_analyzer.py +0 -0
  18. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens/signals_parser.py +0 -0
  19. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens.egg-info/dependency_links.txt +0 -0
  20. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens.egg-info/entry_points.txt +0 -0
  21. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens.egg-info/requires.txt +0 -0
  22. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/django_orm_lens.egg-info/top_level.txt +0 -0
  23. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/setup.cfg +0 -0
  24. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_abstract_and_auth_user.py +0 -0
  25. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_cli_hint.py +0 -0
  26. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_cli_subcommands_smoke.py +0 -0
  27. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_cli_verbose.py +0 -0
  28. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_cross_module_bases.py +0 -0
  29. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_diff_command.py +0 -0
  30. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_golden_fixtures.py +0 -0
  31. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_kwarg_order_and_auth_user.py +0 -0
  32. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_list_command.py +0 -0
  33. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_migration_risk.py +0 -0
  34. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_migrations_parser.py +0 -0
  35. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_nplusone_detector.py +0 -0
  36. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_parity.py +0 -0
  37. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_pep526_type_hints.py +0 -0
  38. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_pep695_generic_classes.py +0 -0
  39. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_query_analyzer.py +0 -0
  40. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/tests/test_scan_root.py +0 -0
  41. {django_orm_lens-1.2.4 → django_orm_lens-1.2.6}/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.4
3
+ Version: 1.2.6
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
@@ -135,12 +135,20 @@ def _build_body_regexes(indent: int):
135
135
  # ``[^=]+`` is safe because ``=`` never appears inside a type expression
136
136
  # (subscripts, unions, dotted refs, and generics all use other punctuation).
137
137
  ann = r"(?:\s*:[^=]+)?"
138
+ # ``(?:[A-Za-z_][A-Za-z0-9_]*\.)?`` — optional single-identifier prefix on
139
+ # the RHS. Covers:
140
+ # * aliased models module — ``from django.db import models as m; x = m.CharField(...)``
141
+ # * third-party field packages — ``x = jsonfield.JSONField(default=dict)``
142
+ # * standard bare imports — ``from django.db.models import CharField; x = CharField(...)``
143
+ # Safe against false positives because the type name is still restricted
144
+ # to Django's known field whitelist (BARE_FIELD_TYPES).
145
+ prefix_opt = r"(?:[A-Za-z_][A-Za-z0-9_]*\.)?"
138
146
  result = {
139
147
  "FIELD_RE": re.compile(
140
148
  rf"^{w}([a-zA-Z_][a-zA-Z0-9_]*){ann}\s*=\s*models\.([A-Za-z_][A-Za-z0-9_]*)\s*\("
141
149
  ),
142
150
  "BARE_FIELD_RE": re.compile(
143
- rf"^{w}([a-zA-Z_][a-zA-Z0-9_]*){ann}\s*=\s*({BARE_FIELD_TYPES})\s*\("
151
+ rf"^{w}([a-zA-Z_][a-zA-Z0-9_]*){ann}\s*=\s*{prefix_opt}({BARE_FIELD_TYPES})\s*\("
144
152
  ),
145
153
  "META_START_RE": re.compile(rf"^{w}class\s+Meta\s*(?:\([^)]*\))?\s*:"),
146
154
  "META_ITEM_RE": re.compile(
@@ -390,7 +398,12 @@ def _collect_defs(file_path: str, content: str) -> List[ParsedModel]:
390
398
  result with definitions from other files before calling
391
399
  ``_resolve_and_filter``.
392
400
  """
393
- lines = content.splitlines()
401
+ # Expand tab characters to 4 spaces so the ``\s{indent}`` prefix used in
402
+ # FIELD_RE / BARE_FIELD_RE / META_ITEM_RE matches tab-indented code too.
403
+ # A single ``\t`` character otherwise contributes 1 to ``\s`` even though
404
+ # it's visually 4 columns — tab-indented models used to have zero fields
405
+ # detected. Line numbers are preserved (per-line expansion only).
406
+ lines = [line.expandtabs(4) for line in content.splitlines()]
394
407
  all_defs: List[ParsedModel] = []
395
408
  parent = os.path.basename(os.path.dirname(file_path))
396
409
  if parent == "models":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-orm-lens
3
- Version: 1.2.4
3
+ Version: 1.2.6
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
@@ -18,6 +18,7 @@ django_orm_lens.egg-info/entry_points.txt
18
18
  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
+ tests/test_aliased_module_and_third_party_fields.py
21
22
  tests/test_cli_hint.py
22
23
  tests/test_cli_subcommands_smoke.py
23
24
  tests/test_cli_verbose.py
@@ -34,4 +35,5 @@ tests/test_pep526_type_hints.py
34
35
  tests/test_pep695_generic_classes.py
35
36
  tests/test_query_analyzer.py
36
37
  tests/test_scan_root.py
37
- tests/test_signals_parser.py
38
+ tests/test_signals_parser.py
39
+ tests/test_tab_indented_models.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.4"
7
+ version = "1.2.6"
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,94 @@
1
+ """Regression tests for two field-detection gaps surfaced by targeted fuzz:
2
+
3
+ 1. **Aliased models module.** ``from django.db import models as m`` followed
4
+ by ``class X(m.Model): x = m.CharField(...)`` used to lose every field
5
+ because ``FIELD_RE`` hardcoded the ``models\\.`` prefix and
6
+ ``BARE_FIELD_RE`` had no prefix allowance at all.
7
+
8
+ 2. **Third-party field packages.** Fields declared via a namespaced
9
+ third-party import - ``x = jsonfield.JSONField(default=dict)``,
10
+ ``x = timezonefield.TimeZoneField()`` - also fell through the same
11
+ two-way gap.
12
+
13
+ Fixed by adding an optional single-identifier prefix on the RHS in
14
+ ``BARE_FIELD_RE``. Safe against false positives because the type name
15
+ is still restricted to Django's known field whitelist
16
+ (``BARE_FIELD_TYPES``) - random ``foo.CharField(...)`` calls in
17
+ non-Django code won't leak in unless the trailing name is a real
18
+ Django field type.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import unittest
24
+
25
+ from django_orm_lens.parser import parse_models_file
26
+
27
+
28
+ class AliasedModelsModuleTest(unittest.TestCase):
29
+ def test_all_fields_captured_via_alias(self) -> None:
30
+ src = (
31
+ "from django.db import models as m\n\n"
32
+ "class Order(m.Model):\n"
33
+ " status = m.CharField(max_length=20)\n"
34
+ " total = m.DecimalField(max_digits=10, decimal_places=2)\n"
35
+ " quantity = m.IntegerField(default=1)\n"
36
+ )
37
+ models = parse_models_file("shop/models.py", src)
38
+ self.assertEqual(len(models), 1)
39
+ self.assertEqual(models[0].name, "Order")
40
+ self.assertEqual(
41
+ [f.name for f in models[0].fields],
42
+ ["status", "total", "quantity"],
43
+ )
44
+ self.assertEqual(models[0].fields[0].type, "CharField")
45
+
46
+ def test_mixed_alias_and_plain_and_bare(self) -> None:
47
+ # One model, three import styles interleaved - every field must
48
+ # be captured regardless of prefix.
49
+ src = (
50
+ "from django.db import models as m\n"
51
+ "from django.db.models import IntegerField\n\n"
52
+ "class Row(m.Model):\n"
53
+ " aliased = m.CharField(max_length=10)\n"
54
+ " plain = models.TextField()\n"
55
+ " bare = IntegerField(default=0)\n"
56
+ )
57
+ models = parse_models_file("t/models.py", src)
58
+ self.assertEqual(
59
+ [f.name for f in models[0].fields],
60
+ ["aliased", "plain", "bare"],
61
+ )
62
+
63
+
64
+ class ThirdPartyFieldPackageTest(unittest.TestCase):
65
+ def test_jsonfield_style_package(self) -> None:
66
+ # `django-jsonfield`-style: `x = jsonfield.JSONField(default=dict)`.
67
+ # JSONField is in the standard whitelist, so this must match even
68
+ # under a non-`models.` prefix.
69
+ src = (
70
+ "import jsonfield\n\n"
71
+ "class Config(models.Model):\n"
72
+ " data = jsonfield.JSONField(default=dict)\n"
73
+ )
74
+ models = parse_models_file("t/models.py", src)
75
+ self.assertEqual([f.name for f in models[0].fields], ["data"])
76
+ self.assertEqual(models[0].fields[0].type, "JSONField")
77
+
78
+ def test_plain_bare_import_still_works(self) -> None:
79
+ # Backward-compat: the pre-fix bare form (no prefix at all) must
80
+ # still match - the prefix is truly optional, not required.
81
+ src = (
82
+ "from django.db.models import CharField, IntegerField\n\n"
83
+ "class R(models.Model):\n"
84
+ " name = CharField(max_length=50)\n"
85
+ " count = IntegerField(default=0)\n"
86
+ )
87
+ models = parse_models_file("t/models.py", src)
88
+ self.assertEqual(
89
+ [f.name for f in models[0].fields], ["name", "count"]
90
+ )
91
+
92
+
93
+ if __name__ == "__main__":
94
+ unittest.main()
@@ -0,0 +1,54 @@
1
+ """Regression: tab-indented model bodies must parse.
2
+
3
+ Editors that default to tabs (or projects that use PEP-8 exceptions with
4
+ tabs) produced empty models - every field silently dropped. Root cause:
5
+ ``FIELD_RE`` uses ``\\s{indent}`` as its column prefix and
6
+ ``_detect_class_indent`` expands tabs to width 4, but a single ``\\t``
7
+ character is only one match for ``\\s``, not four. Fixed by pre-expanding
8
+ tabs to spaces in the line buffer before regex matching.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import unittest
14
+
15
+ from django_orm_lens.parser import parse_models_file
16
+
17
+
18
+ class TabIndentationTest(unittest.TestCase):
19
+ def test_pure_tab_indented_body(self) -> None:
20
+ src = (
21
+ "class Post(models.Model):\n"
22
+ "\ttitle = models.CharField(max_length=100)\n"
23
+ "\tauthor = models.CharField(max_length=100)\n"
24
+ "\tbody = models.TextField()\n"
25
+ )
26
+ models = parse_models_file("blog/models.py", src)
27
+ self.assertEqual(len(models), 1)
28
+ self.assertEqual(
29
+ [f.name for f in models[0].fields],
30
+ ["title", "author", "body"],
31
+ )
32
+
33
+ def test_mixed_tab_then_space(self) -> None:
34
+ # Editors that mid-migrate to tabs sometimes end up with a tab
35
+ # followed by a space; that used to fail equally silently.
36
+ src = (
37
+ "class M(models.Model):\n"
38
+ "\t x = models.CharField(max_length=5)\n"
39
+ )
40
+ models = parse_models_file("t/models.py", src)
41
+ self.assertEqual([f.name for f in models[0].fields], ["x"])
42
+
43
+ def test_space_indented_still_works(self) -> None:
44
+ # Backward-compat guard.
45
+ src = (
46
+ "class M(models.Model):\n"
47
+ " x = models.CharField(max_length=5)\n"
48
+ )
49
+ models = parse_models_file("t/models.py", src)
50
+ self.assertEqual([f.name for f in models[0].fields], ["x"])
51
+
52
+
53
+ if __name__ == "__main__":
54
+ unittest.main()