Opened 14 months ago
Last modified 7 days ago
#36415 assigned New feature
Add a public API to unregister model field lookups
| Reported by: | Tim Graham | Owned by: | Sina Chaichi |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Accepted | |
| Has patch: | yes | 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): 337 337 register_class_lookup = classmethod(register_class_lookup) 338 338 339 339 def _unregister_class_lookup(cls, lookup, lookup_name=None): 340 """341 Remove given lookup from cls lookups. For use in tests only as it's342 not thread-safe.343 """344 340 if lookup_name is None: 345 341 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 347 345 cls._clear_cached_class_lookups() 348 346 349 347 def _unregister_instance_lookup(self, lookup, lookup_name=None): … … def _unregister_instance_lookup(self, lookup, lookup_name=None): 353 351 """ 354 352 if lookup_name is None: 355 353 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 357 357 358 358 _unregister_lookup = class_or_instance_method( 359 359 _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 (7)
comment:1 by , 14 months ago
| Triage Stage: | Unreviewed → Accepted |
|---|
comment:3 by , 12 months ago
I'll deassign the ticket if I'm not going to return to it. It's not an easy task anyway.
comment:4 by , 2 weeks ago
| Owner: | removed |
|---|---|
| Status: | assigned → new |
comment:5 by , 7 days ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:6 by , 7 days ago
| Has patch: | set |
|---|
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.