django-orm-lens 1.2.3__tar.gz → 1.2.5__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.3 → django_orm_lens-1.2.5}/PKG-INFO +1 -1
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/parser.py +20 -3
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens.egg-info/PKG-INFO +1 -1
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens.egg-info/SOURCES.txt +2 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/pyproject.toml +1 -1
- django_orm_lens-1.2.5/tests/test_aliased_module_and_third_party_fields.py +94 -0
- django_orm_lens-1.2.5/tests/test_pep695_generic_classes.py +71 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/README.md +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/__init__.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/cli.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/diff.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/formatters.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/mcp_server.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/migrations_parser.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/models.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/py.typed +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/query_analyzer.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens/signals_parser.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens.egg-info/dependency_links.txt +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens.egg-info/entry_points.txt +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens.egg-info/requires.txt +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/django_orm_lens.egg-info/top_level.txt +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/setup.cfg +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_abstract_and_auth_user.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_cli_hint.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_cli_subcommands_smoke.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_cli_verbose.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_cross_module_bases.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_diff_command.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_golden_fixtures.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_kwarg_order_and_auth_user.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_list_command.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_migration_risk.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_migrations_parser.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_nplusone_detector.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_parity.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_pep526_type_hints.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_query_analyzer.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_scan_root.py +0 -0
- {django_orm_lens-1.2.3 → django_orm_lens-1.2.5}/tests/test_signals_parser.py +0 -0
|
@@ -37,8 +37,17 @@ from .models import (
|
|
|
37
37
|
|
|
38
38
|
RELATION_TYPES: Tuple[str, ...] = ("ForeignKey", "ManyToManyField", "OneToOneField")
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
# ``(?:\s*\[[^\]]*\])?`` — optional PEP-695 generic type parameter list
|
|
41
|
+
# introduced in Python 3.12, e.g. ``class Container[T](models.Model):``.
|
|
42
|
+
# Only handles a single-line, non-nested bracket group — nested brackets in
|
|
43
|
+
# class-header generics are vanishingly rare and don't warrant a full
|
|
44
|
+
# bracket matcher.
|
|
45
|
+
CLASS_RE = re.compile(
|
|
46
|
+
r"^class\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*\[[^\]]*\])?\s*\(([^)]*)\)\s*:"
|
|
47
|
+
)
|
|
48
|
+
CLASS_START_RE = re.compile(
|
|
49
|
+
r"^class\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*\[[^\]]*\])?\s*\("
|
|
50
|
+
)
|
|
42
51
|
|
|
43
52
|
NON_MODEL_TAIL = re.compile(
|
|
44
53
|
r"^(ModelAdmin|ModelForm|ModelSerializer|ModelChoiceField|"
|
|
@@ -126,12 +135,20 @@ def _build_body_regexes(indent: int):
|
|
|
126
135
|
# ``[^=]+`` is safe because ``=`` never appears inside a type expression
|
|
127
136
|
# (subscripts, unions, dotted refs, and generics all use other punctuation).
|
|
128
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_]*\.)?"
|
|
129
146
|
result = {
|
|
130
147
|
"FIELD_RE": re.compile(
|
|
131
148
|
rf"^{w}([a-zA-Z_][a-zA-Z0-9_]*){ann}\s*=\s*models\.([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
132
149
|
),
|
|
133
150
|
"BARE_FIELD_RE": re.compile(
|
|
134
|
-
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*\("
|
|
135
152
|
),
|
|
136
153
|
"META_START_RE": re.compile(rf"^{w}class\s+Meta\s*(?:\([^)]*\))?\s*:"),
|
|
137
154
|
"META_ITEM_RE": re.compile(
|
|
@@ -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
|
|
@@ -31,6 +32,7 @@ tests/test_migrations_parser.py
|
|
|
31
32
|
tests/test_nplusone_detector.py
|
|
32
33
|
tests/test_parity.py
|
|
33
34
|
tests/test_pep526_type_hints.py
|
|
35
|
+
tests/test_pep695_generic_classes.py
|
|
34
36
|
tests/test_query_analyzer.py
|
|
35
37
|
tests/test_scan_root.py
|
|
36
38
|
tests/test_signals_parser.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.5"
|
|
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,71 @@
|
|
|
1
|
+
"""Regression: PEP-695 generic classes (Python 3.12+) must parse.
|
|
2
|
+
|
|
3
|
+
Before this fix, ``class Container[T](models.Model):`` failed the
|
|
4
|
+
``CLASS_RE`` pattern entirely — the parser walked past it and reported
|
|
5
|
+
zero models for the whole file. Users on Python 3.12+ writing modern
|
|
6
|
+
typed Django models saw an empty sidebar/ER diagram with no error.
|
|
7
|
+
|
|
8
|
+
The fix adds an optional ``(?:\\s*\\[[^\\]]*\\])?`` group between the
|
|
9
|
+
class name and the opening ``(`` in both ``CLASS_RE`` and
|
|
10
|
+
``CLASS_START_RE``. Covers single/multi-param, bounded, variadic, and
|
|
11
|
+
paramspec generics — the four common PEP-695 shapes.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import unittest
|
|
17
|
+
|
|
18
|
+
from django_orm_lens.parser import parse_models_file
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Pep695GenericClassTest(unittest.TestCase):
|
|
22
|
+
def test_single_param_generic(self) -> None:
|
|
23
|
+
src = (
|
|
24
|
+
"class Container[T](models.Model):\n"
|
|
25
|
+
" label = models.CharField(max_length=50)\n"
|
|
26
|
+
)
|
|
27
|
+
models = parse_models_file("t.py", src)
|
|
28
|
+
self.assertEqual(len(models), 1)
|
|
29
|
+
self.assertEqual(models[0].name, "Container")
|
|
30
|
+
self.assertEqual([f.name for f in models[0].fields], ["label"])
|
|
31
|
+
|
|
32
|
+
def test_multi_param_generic(self) -> None:
|
|
33
|
+
src = (
|
|
34
|
+
"class Pair[K, V](models.Model):\n"
|
|
35
|
+
" key = models.CharField(max_length=10)\n"
|
|
36
|
+
" val = models.CharField(max_length=10)\n"
|
|
37
|
+
)
|
|
38
|
+
models = parse_models_file("t.py", src)
|
|
39
|
+
self.assertEqual(len(models), 1)
|
|
40
|
+
self.assertEqual([f.name for f in models[0].fields], ["key", "val"])
|
|
41
|
+
|
|
42
|
+
def test_bounded_generic(self) -> None:
|
|
43
|
+
src = (
|
|
44
|
+
"class Bounded[T: str](models.Model):\n"
|
|
45
|
+
" x = models.CharField(max_length=5)\n"
|
|
46
|
+
)
|
|
47
|
+
models = parse_models_file("t.py", src)
|
|
48
|
+
self.assertEqual(len(models), 1)
|
|
49
|
+
|
|
50
|
+
def test_variadic_and_paramspec(self) -> None:
|
|
51
|
+
src = (
|
|
52
|
+
"class Variadic[*Ts, **P](models.Model):\n"
|
|
53
|
+
" x = models.CharField(max_length=5)\n"
|
|
54
|
+
)
|
|
55
|
+
models = parse_models_file("t.py", src)
|
|
56
|
+
self.assertEqual(len(models), 1)
|
|
57
|
+
|
|
58
|
+
def test_non_generic_still_works(self) -> None:
|
|
59
|
+
# Backward-compat guard — the ``[T]`` group is optional, plain
|
|
60
|
+
# class headers must still parse identically.
|
|
61
|
+
src = (
|
|
62
|
+
"class Plain(models.Model):\n"
|
|
63
|
+
" x = models.CharField(max_length=5)\n"
|
|
64
|
+
)
|
|
65
|
+
models = parse_models_file("t.py", src)
|
|
66
|
+
self.assertEqual(len(models), 1)
|
|
67
|
+
self.assertEqual(models[0].name, "Plain")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
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.3 → django_orm_lens-1.2.5}/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
|
|
File without changes
|
|
File without changes
|