#Is there an easy way to have Django let me know that I've set an "unknown" attribute on a model?

40 messages ยท Page 1 of 1 (latest)

thorn thicket
#

Here's an example: ```py
class Thing(models.Model):
x = models.CharField()
...
t = Thing(x="hi there")
t.y = "ho there" # accidentally set 'y' when I meant to set 'x'
t.save()


I can't think of *how*, but it'd be nice if I got some sort of warning, perhaps at save time.
cloud rune
#

Whether it is safe or efficient, or if/how it would work with a Django model, no idea... but off the top of my head:

>>> class Unsettable:
...     x = None
...     y = None
...     def __setattr__(self, name, value):
...         if not hasattr(self, name): raise KeyError
...         super().__setattr__(name, value)
... 
>>> u = Unsettable()
>>> u.x = 10
>>> u.z = 100
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __setattr__
KeyError

That with the _meta API might get you started on the right path...
https://docs.djangoproject.com/en/5.0/ref/models/meta/#django.db.models.options.Options.get_field
If t.y = "ho there" does call __setattr__ (and not some kind of Django magic), something like

def __setattr__(self, name, value):
    try:
        self._meta.get_field(name)
        super().__setattr__(name, value)
    except FieldDoesNotExist:
        print("learn to type")

maybe ๐Ÿค”

#

Cooking now, but if you implement it, let me know ๐Ÿ˜„

thorn thicket
#

ha, thanks. Might try this out later. (FWIW I would not push this to prod; it's just a development aid.)

mystic light
#

Does django-stubs and static type analysis help with this?

cloud rune
#

I'd have to check that when I go back to writing Django, but it certainly wouldn't help in the REPL, where I presume this would be most useful (at least for me).

atomic geyser
#

I do this:

def set_fields(obj: Model, **kwargs):
    update_fields = []
    for k, v in kwargs.items():
        if getattr(obj, k, sentinel) != v:
            setattr(obj, k, v)
            update_fields.append(k)
    return update_fields


def write_fields(obj: Model, **kwargs):
    update_fields = set_fields(obj, **kwargs)
    if update_fields:
        try:
            obj.save(update_fields=kwargs.keys())
        except DatabaseError as e:
            if str(e) != 'Save with update_fields did not affect any rows.':
                raise
        return True
    return False
#

So instead of

x = Foo.objects.get(...)
x.bar = 1
x.save()

I do:

x = Foo.objects.get(...)
write_fields(x, bar=1)
#

This has stopped a lot of bugs very early

mystic light
#

How does that stop you setting attributes on the instance that don't exist?

atomic geyser
mystic light
#

No? offby1's question was "I want a warning when I set an attribute on a model instance that doesn't exist/isn't one of the fields"

atomic geyser
atomic geyser
#

Which made me very confused :P

mystic light
mystic light
atomic geyser
#

Seriousy though, I pass the fields in the save call

#

obj.save(update_fields=kwargs.keys())

mystic light
#

Right, so it's on the model save rather than the setting of the attribute. I didn't realise that throws an exception when passed fields that don't exist, rather than ignoring them ๐Ÿ‘

atomic geyser
#

Yea exactly.

mystic light
#

This is horrible DX though: ```python
x = Foo.objects.get(...)
write_fields(x, bar=1)

atomic geyser
mystic light
#

Sure, but less isn't always better?

atomic geyser
#

Different doesn't mean "horrible DX". You'd have to be more specific why you don't like it imo.

#

I agree it's very different.

mystic light
#

It's ugly (yes I realise that's subjective), it goes against standard Django conventions of "set attribute, call save", it goes against standard Python conventions of "how you set attributes on objects", etc

atomic geyser
#

Yea, sure. But those conventions are the problem in the first place. ๐Ÿคทโ€โ™‚๏ธ

#

The DX of silently ignoring errors.. now that is "horrible"

#

But yea, you could go the __setattr__ path, but I would say you'd need to whitelist setting stuff starting with _, and my thing above solves more problems than the one asked. It also avoid overwriting fields touched by other updates as it doesn't overwrite everything.

#

It also avoids the save altogether if no changes were made.

cloud rune
#

It might take a while to get used to, like most changes, but at a first glance it looks like a nice solution.

atomic geyser
#

Even for me who wrote it, it took some getting used to for sure. 10+ years of Django habits was fighting me :)

cloud rune
#

One downside of mine is that it requires an abstract base model or something, which makes it more difficult to use in dev only... a separate function does help there.

atomic geyser
#

I think the idea of running checks in dev only is a bad idea. It's in prod you really don't want silent errors that you never learn about.

#

The base class has the problem that you can't easily add it to third party library models.

thorn thicket
#

well anyway I have plenty of tests, so even if I do nothing, at worst I'll waste some time wondering why a test is failing (before eventually figuring out "oh I should have set obj.x and not obj.y"

atomic geyser