fix: append and extend without touching attributes
This commit is contained in:
parent
10c3c9ea9b
commit
893a87df86
2 changed files with 96 additions and 72 deletions
|
|
@ -21,7 +21,7 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr
|
|||
from frappe.desk.form.document_follow import follow_document
|
||||
from frappe.integrations.doctype.webhook import run_webhooks
|
||||
from frappe.model import optional_fields, table_fields
|
||||
from frappe.model.base_document import BaseDocument, get_controller
|
||||
from frappe.model.base_document import BaseDocument, D, get_controller
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.naming import set_new_name, validate_name
|
||||
from frappe.model.utils import is_virtual_doctype, simple_singledispatch
|
||||
|
|
@ -1983,11 +1983,24 @@ class LazyDocument:
|
|||
|
||||
@override
|
||||
def get(self: Document, key, filters=None, limit=None, default=None):
|
||||
# Ensure that table descriptor is triggered at least once
|
||||
if isinstance(key, str) and key in self._table_fieldnames:
|
||||
# Trigger populating of __dict__
|
||||
getattr(self, key, None)
|
||||
return super().get(key, filters, limit, default)
|
||||
|
||||
@override
|
||||
def extend(self: Document, key, value):
|
||||
# Ensure that table descriptor is triggered at least once
|
||||
if isinstance(key, str) and key in self._table_fieldnames:
|
||||
getattr(self, key, None)
|
||||
return super().extend(key, value)
|
||||
|
||||
@override
|
||||
def append(self, key: str, value: D | dict | None = None, position: int = -1) -> D:
|
||||
if isinstance(key, str) and key in self._table_fieldnames:
|
||||
getattr(self, key, None)
|
||||
return super().append(key, value, position)
|
||||
|
||||
@override
|
||||
def db_update_all(self):
|
||||
self.db_update()
|
||||
|
|
|
|||
|
|
@ -527,76 +527,6 @@ class TestDocument(IntegrationTestCase):
|
|||
changed_val = frappe.db.get_single_value(c.doctype, key)
|
||||
self.assertEqual(val, changed_val)
|
||||
|
||||
def test_lazy_documents(self):
|
||||
# Warmup meta etc
|
||||
_ = frappe.get_lazy_doc("User", "Guest")
|
||||
eager_guest: User = frappe.get_doc("User", "Guest")
|
||||
|
||||
# Only one query for parent document
|
||||
with self.assertQueryCount(1):
|
||||
guest: User = frappe.get_lazy_doc("User", "Guest")
|
||||
self.assertEqual(guest.user_type, "Website User")
|
||||
|
||||
# Only one query for one table access
|
||||
with self.assertQueryCount(1):
|
||||
guest_role = guest.roles[0]
|
||||
self.assertEqual(guest_role.role, "Guest")
|
||||
self.assertIsInstance(guest_role, type(eager_guest.roles[0]))
|
||||
|
||||
# Only one query for one table access
|
||||
with self.assertQueryCount(1):
|
||||
_ = guest.role_profiles
|
||||
|
||||
# No queries for repeat access, same object
|
||||
with self.assertQueryCount(0):
|
||||
guest_role_repeat_access = guest.roles[0]
|
||||
self.assertIs(guest_role, guest_role_repeat_access)
|
||||
|
||||
# Same object after first access
|
||||
with self.assertQueryCount(0):
|
||||
self.assertIs(guest.roles, guest.get("roles"))
|
||||
|
||||
# things accessing __dict__ by default should be updated too
|
||||
self.assertTrue(frappe.get_lazy_doc("User", "Guest").get("roles"))
|
||||
|
||||
def test_lazy_doc_efficient_saves(self):
|
||||
# Only touched tables and self should be updated
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
with self.assertQueryCount(1):
|
||||
guest.db_update_all()
|
||||
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
_ = guest.roles
|
||||
with self.assertQueryCount(1 + len(guest.roles)):
|
||||
guest.db_update_all()
|
||||
|
||||
# Save should works, it won't be efficient because internal code will just trigger fetching
|
||||
# of child tables to resave them.
|
||||
guest.save()
|
||||
|
||||
def test_lazy_magic(self):
|
||||
self.assertIsNone(getattr(LazyChildTable, "__set__", None))
|
||||
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
# table fields will be populated on first access
|
||||
self.assertIsNone(guest.__dict__.get("roles"))
|
||||
roles = guest.roles
|
||||
self.assertIs(guest.__dict__.get("roles"), roles)
|
||||
|
||||
# Allow overriding from user code
|
||||
roles_copy = deepcopy(roles)
|
||||
guest.roles = roles_copy
|
||||
self.assertIs(guest.__dict__.get("roles"), roles_copy)
|
||||
|
||||
with patch(f"{LazyChildTable.__module__}.{LazyChildTable.__name__}.__get__") as getter:
|
||||
_ = guest.roles
|
||||
self.assertFalse(getter.called)
|
||||
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
with patch(f"{LazyChildTable.__module__}.{LazyChildTable.__name__}.__get__") as getter:
|
||||
_ = guest.roles
|
||||
self.assertTrue(getter.called)
|
||||
|
||||
|
||||
class TestDocumentWebView(IntegrationTestCase):
|
||||
def get(self, path, user="Guest"):
|
||||
|
|
@ -734,3 +664,84 @@ class TestDocumentWebView(IntegrationTestCase):
|
|||
)
|
||||
self.assertEqual(sent_docs - all_docs, set(), "All docs should be inserted")
|
||||
self.assertEqual(sent_child_docs - all_child_docs, set(), "All child docs should be inserted")
|
||||
|
||||
|
||||
class TestLazyDocument(IntegrationTestCase):
|
||||
def test_lazy_documents(self):
|
||||
# Warmup meta etc
|
||||
_ = frappe.get_lazy_doc("User", "Guest")
|
||||
eager_guest: User = frappe.get_doc("User", "Guest")
|
||||
|
||||
# Only one query for parent document
|
||||
with self.assertQueryCount(1):
|
||||
guest: User = frappe.get_lazy_doc("User", "Guest")
|
||||
self.assertEqual(guest.user_type, "Website User")
|
||||
|
||||
# Only one query for one table access
|
||||
with self.assertQueryCount(1):
|
||||
guest_role = guest.roles[0]
|
||||
self.assertEqual(guest_role.role, "Guest")
|
||||
self.assertIsInstance(guest_role, type(eager_guest.roles[0]))
|
||||
|
||||
# Only one query for one table access
|
||||
with self.assertQueryCount(1):
|
||||
_ = guest.role_profiles
|
||||
|
||||
# No queries for repeat access, same object
|
||||
with self.assertQueryCount(0):
|
||||
guest_role_repeat_access = guest.roles[0]
|
||||
self.assertIs(guest_role, guest_role_repeat_access)
|
||||
|
||||
# Same object after first access
|
||||
with self.assertQueryCount(0):
|
||||
self.assertIs(guest.roles, guest.get("roles"))
|
||||
|
||||
# things accessing __dict__ by default should be updated too
|
||||
self.assertTrue(frappe.get_lazy_doc("User", "Guest").get("roles"))
|
||||
|
||||
def test_lazy_doc_efficient_saves(self):
|
||||
# Only touched tables and self should be updated
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
with self.assertQueryCount(1):
|
||||
guest.db_update_all()
|
||||
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
_ = guest.roles
|
||||
with self.assertQueryCount(1 + len(guest.roles)):
|
||||
guest.db_update_all()
|
||||
|
||||
# Save should works, it won't be efficient because internal code will just trigger fetching
|
||||
# of child tables to resave them.
|
||||
guest.save()
|
||||
|
||||
def test_lazy_magic(self):
|
||||
self.assertIsNone(getattr(LazyChildTable, "__set__", None))
|
||||
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
# table fields will be populated on first access
|
||||
self.assertIsNone(guest.__dict__.get("roles"))
|
||||
roles = guest.roles
|
||||
self.assertIs(guest.__dict__.get("roles"), roles)
|
||||
|
||||
# Allow overriding from user code
|
||||
roles_copy = deepcopy(roles)
|
||||
guest.roles = roles_copy
|
||||
self.assertIs(guest.__dict__.get("roles"), roles_copy)
|
||||
|
||||
with patch(f"{LazyChildTable.__module__}.{LazyChildTable.__name__}.__get__") as getter:
|
||||
_ = guest.roles
|
||||
self.assertFalse(getter.called)
|
||||
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
with patch(f"{LazyChildTable.__module__}.{LazyChildTable.__name__}.__get__") as getter:
|
||||
_ = guest.roles
|
||||
self.assertTrue(getter.called)
|
||||
|
||||
def test_append_extend(self):
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
_ = guest.append("roles")
|
||||
self.assertEqual(len(guest.roles), 2)
|
||||
|
||||
guest = frappe.get_lazy_doc("User", "Guest")
|
||||
_ = guest.extend("roles", [{}])
|
||||
self.assertEqual(len(guest.roles), 2)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue