Opened 4 months ago

Last modified 2 months ago

#36415 assigned New feature

Add a public API to unregister model field lookups

Reported by: Tim Graham Owned by: Tim Graham
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Motivated by the ongoing work to build a MongoDB backend, I'm interested in writing code like this:
EmbeddedModelField._unregister_lookup(GreaterThanOrEqual)
(a lot of the built-in lookups aren't applicable to EmbeddedModelField... I think we only want exact.)

As far as I can tell, Field._unregister_lookup() isn't a public API ​to concerns about thread-safety.

Simon Charette provided this guidance:

As far as I know we don't run tests in a multi-threaded environment (it's multi-process based) so I'm not sure what this comment is referring to.

If you implement this logic I suspect you'll need to use a notion of sentinel / tombstone to denote unregistration to work like you intend as GreaterThanOrEqual is registered for Field as well.

The following code might explain why the current situation is broken:

from django.db.models import Field
from django.db.models.lookups import GreaterThanOrEqual

class EmbeddedModelField(Field):
   ...

>>> EmbeddedModelField._unregister_lookup(GreaterThanOrEqual)
>>> assert "gte" not in EmbeddedModelField.get_lookups()
>>> assert "gte" in Field.get_lookups()

I think that's what the "thread-safe" mention was referring to, it was more along the lines that _unregister_lookup() is broken if it doesn't have a corresponding register_lookup() call to restore its state because the unregistration logic doesn't ensure to create a per-class override prior to deletion.

A potential solution to this problem would be to avoid any form of deletion of registered lookups and use a UnregisteredLookup sentinel (or simply None) instead:

  • django/db/models/query_utils.py

    diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py
    index f0ae810f47..4df1324825 100644
    a b def register_instance_lookup(self, lookup, lookup_name=None):  
    337337    register_class_lookup = classmethod(register_class_lookup)
    338338
    339339    def _unregister_class_lookup(cls, lookup, lookup_name=None):
    340         """
    341         Remove given lookup from cls lookups. For use in tests only as it's
    342         not thread-safe.
    343         """
    344340        if lookup_name is None:
    345341            lookup_name = lookup.lookup_name
    346         del cls.class_lookups[lookup_name]
     342        if "class_lookups" not in cls.__dict__:
     343            cls.class_lookups = {}
     344        cls.class_lookups[lookup_name] = None
    347345        cls._clear_cached_class_lookups()
    348346
    349347    def _unregister_instance_lookup(self, lookup, lookup_name=None):
    â€Ļ â€Ļ def _unregister_instance_lookup(self, lookup, lookup_name=None):  
    353351        """
    354352        if lookup_name is None:
    355353            lookup_name = lookup.lookup_name
    356         del self.instance_lookups[lookup_name]
     354        if "instance_lookups" not in self.__dict__:
     355            self.instance_lookups = {}
     356        self.instance_lookups[lookup_name] = None
    357357
    358358    _unregister_lookup = class_or_instance_method(
    359359        _unregister_class_lookup, _unregister_instance_lookup

None is a suitable sentinel as both get_lookup and get_transform treat it as a missing entry.

Change History (3)

comment:1 by Simon Charette, 4 months ago

Triage Stage: Unreviewed → Accepted

Given there are few cases where ​we could use this internally and a registration interface already exists I think it's work adding as it's quite hard to get unregistration right now that we have class and instance level lookups support.

comment:2 by Ahmed Ibrahim, 2 months ago

Is this still going on, or should I take over?

comment:3 by Tim Graham, 2 months ago

I'll deassign the ticket if I'm not going to return to it. It's not an easy task anyway.

Note: See TracTickets for help on using tickets.
Back to Top