perf: faster local attr lookup

This commit is contained in:
Sagar Vora 2025-03-12 14:03:23 +05:30
parent 20220ef894
commit 3c8912f6e7
4 changed files with 71 additions and 59 deletions

View file

@ -45,6 +45,7 @@ from frappe.query_builder.utils import (
from frappe.utils.caching import deprecated_local_cache as local_cache
from frappe.utils.caching import request_cache
from frappe.utils.data import as_unicode, bold, cint, cstr, safe_decode, safe_encode, sbool
from frappe.utils.local import FrappeLocal
# Local application imports
from .exceptions import *
@ -75,7 +76,7 @@ if TYPE_CHECKING: # pragma: no cover
from frappe.utils.redis_wrapper import ClientCache, RedisWrapper
controllers: dict[str, "Document"] = {}
local = Local()
local = FrappeLocal()
cache: Optional["RedisWrapper"] = None
client_cache: Optional["ClientCache"] = None
STANDARD_USERS = ("Guest", "Administrator")
@ -87,27 +88,6 @@ if _dev_server:
warnings.simplefilter("always", PendingDeprecationWarning)
def _get_local_proxy(self: Local, name: str) -> LocalProxy:
"""Get local proxy object by name."""
_local_contextvar = self._Local__storage
def _get_current_object() -> Any:
obj = _local_contextvar.get(None)
if obj is not None and name in obj:
return obj[name]
raise RuntimeError("object is not bound") from None
lp = LocalProxy(_get_current_object)
object.__setattr__(lp, "_get_current_object", _get_current_object)
return lp
Local.__call__ = _get_local_proxy
def _(msg: str, lang: str | None = None, context: str | None = None) -> str:
"""Return translated string in current lang, if exists.
Usage:

View file

@ -202,42 +202,6 @@ def deprecation_warning(marked: str, graduation: str, msg: str):
### Party starts here
if typing.TYPE_CHECKING:
from werkzeug.local import Local
def get_local_with_deprecations() -> "Local":
from werkzeug.local import Local
class DeprecatedLocalAttribute:
def __init__(self, name, warning):
self.name = name
self.warning = warning
def __get__(self, obj, type=None):
self.warning()
return obj.__getattr__(self.name)
def __set__(self, obj, value):
return obj.__setattr__(self.name, value)
def __delete__(self, obj):
return obj.__delattr__(self.name)
class LocalWithDeprecations(Local):
"""Can deprecate local attributes."""
# sites_path = DeprecatedLocalAttribute(
# "sites_path",
# lambda: deprecation_warning(
# "2024-12-06",
# "v17",
# "'local.sites_path' will be deprecated: use 'frappe.bench.sites.path instead'",
# ),
# )
return LocalWithDeprecations()
def _old_deprecated(func):
return deprecated(

View file

@ -3,6 +3,7 @@ from contextlib import contextmanager
from unittest.mock import patch
from rq import Queue
from werkzeug.local import Local
import frappe
from frappe.core.doctype.rq_job.rq_job import remove_failed_jobs
@ -93,7 +94,7 @@ def after_job(*args, **kwargs):
@contextmanager
def freeze_local():
locals = frappe.local
frappe.local = frappe.Local()
frappe.local = Local()
yield locals
frappe.local = locals

67
frappe/utils/local.py Normal file
View file

@ -0,0 +1,67 @@
from contextvars import ContextVar
from typing import Any
from werkzeug.local import Local, LocalProxy
_contextvar = ContextVar("frappe_local")
_local_attributes = frozenset(dir(Local))
_local_proxy_attributes = frozenset(dir(LocalProxy))
class FrappeLocal(Local):
"""
For internal use only. Do not use this class directly.
"""
__slots__ = ()
def __init__(self):
super().__init__(_contextvar)
def __getattribute__(self, name: str) -> Any:
if name in _local_attributes:
return object.__getattribute__(self, name)
obj = _contextvar.get(None)
if obj is not None and name in obj:
return obj[name]
return object.__getattribute__(self, name)
def __setattr__(self, name: str, value: Any) -> None:
obj = _contextvar.get(None)
if obj is None:
obj = {}
_contextvar.set(obj)
obj[name] = value
def __delattr__(self, name: str) -> None:
obj = _contextvar.get(None)
if obj is not None and name in obj:
del obj[name]
return
raise AttributeError(name)
def __call__(self, name: str) -> LocalProxy:
def _get_current_object() -> Any:
obj = _contextvar.get(None)
if obj is not None and name in obj:
return obj[name]
raise RuntimeError("object is not bound") from None
lp = FrappeLocalProxy(_get_current_object)
object.__setattr__(lp, "_get_current_object", _get_current_object)
return lp
class FrappeLocalProxy(LocalProxy):
__slots__ = ()
def __getattribute__(self, name: str) -> Any:
if name in _local_proxy_attributes:
return object.__getattribute__(self, name)
return getattr(object.__getattribute__(self, "_get_current_object")(), name)