Coverage for shellcraft / mixins.py: 93%
63 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-12 09:09 +0530
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-12 09:09 +0530
1import warnings
3_INJECTED = False
6def inject():
7 """Inject Rails-style class/instance methods onto Django's base Model.
9 Safe to call multiple times — idempotent after first call.
10 Only call this from the shellcraft management command.
12 Raises RuntimeError when DEBUG is False so that an accidental import of
13 this module in a production Celery worker or WSGI request cannot
14 activate the shell shortcuts on every model in the process.
15 """
16 global _INJECTED
17 if _INJECTED:
18 return
20 # Belt-and-suspenders production guard. The management command also
21 # checks DEBUG before calling inject(), but inject() is a public function
22 # and could be called from user code (e.g. a custom management command
23 # that wraps shellcraft). Checking here ensures the guard can never be
24 # bypassed simply by calling inject() directly.
25 from django.conf import settings
26 if not settings.DEBUG:
27 raise RuntimeError(
28 "shellcraft.inject() must not be called when DEBUG=False. "
29 "The shellcraft shell is a development-only tool."
30 )
32 _INJECTED = True
34 from django.db import models
36 from shellcraft.printer import ap, format_fields
38 def _find(cls, pk):
39 return cls.objects.get(pk=pk)
41 def _all(cls):
42 # Return the queryset so callers can assign: `users = User.all()`.
43 # Printing and returning are both useful; returning None was a silent
44 # footgun for anyone who stored the result.
45 qs = cls.objects.all()
46 ap(qs)
47 return qs
49 def _where(cls, **kwargs):
50 qs = cls.objects.filter(**kwargs)
51 ap(qs)
52 return qs
54 def _first(cls):
55 obj = cls.objects.first()
56 if obj is not None: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 ap(obj)
58 return obj
60 def _last(cls):
61 obj = cls.objects.last()
62 if obj is not None: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 ap(obj)
64 return obj
66 def _count(cls):
67 return cls.objects.count()
69 def _fields(cls):
70 print(format_fields(cls))
72 def _update(self, **kwargs):
73 # Block dunder and private-attribute keys outright. Allowing
74 # user.update(__class__=X) or user.update(__dict__={...}) would let
75 # shell users overwrite Python internals, bypassing Django's field
76 # validation entirely and corrupting the object in memory.
77 for key in kwargs:
78 if key.startswith("_"):
79 raise ValueError(
80 f"shellcraft: cannot update private or dunder attribute '{key}'. "
81 "Pass only model field names."
82 )
84 # Warn (don't raise) for keys that don't correspond to any known field
85 # on this model. Django's save() will silently ignore them, which is
86 # confusing during interactive development.
87 valid_fields = {f.attname for f in self.__class__._meta.concrete_fields}
88 valid_fields.update(f.name for f in self.__class__._meta.concrete_fields)
89 valid_fields.update(f.name for f in self.__class__._meta.many_to_many)
91 for key in kwargs:
92 if key not in valid_fields:
93 warnings.warn(
94 f"shellcraft: '{key}' is not a recognised field on "
95 f"{self.__class__.__name__} — the attribute will be set "
96 "but will not be persisted by Django's save().",
97 stacklevel=2,
98 )
100 for key, value in kwargs.items():
101 setattr(self, key, value)
102 self.save()
103 return self
105 def _destroy(self):
106 return self.delete()
108 class_methods = {
109 'find': _find,
110 'all': _all,
111 'where': _where,
112 'first': _first,
113 'last': _last,
114 'count': _count,
115 'fields': _fields,
116 }
117 instance_methods = {
118 'update': _update,
119 'destroy': _destroy,
120 }
122 for name, fn in class_methods.items():
123 _safe_inject(models.Model, name, classmethod(fn))
125 for name, fn in instance_methods.items():
126 _safe_inject(models.Model, name, fn)
129def _safe_inject(cls, name, method):
130 if hasattr(cls, name):
131 existing = getattr(cls, name)
132 warnings.warn(
133 f"shellcraft: '{name}' already exists on {cls.__name__} "
134 f"(defined in {getattr(existing, '__module__', '?')}) — skipping injection",
135 stacklevel=3,
136 )
137 return
138 setattr(cls, name, method)