diff --git a/frappe/__init__.py b/frappe/__init__.py index edc63431b8..d679a470de 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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: diff --git a/frappe/deprecation_dumpster.py b/frappe/deprecation_dumpster.py index 27af062aa1..686fbc57c1 100644 --- a/frappe/deprecation_dumpster.py +++ b/frappe/deprecation_dumpster.py @@ -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( diff --git a/frappe/tests/test_background_jobs.py b/frappe/tests/test_background_jobs.py index 487e6904d9..803f4ca50c 100644 --- a/frappe/tests/test_background_jobs.py +++ b/frappe/tests/test_background_jobs.py @@ -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 diff --git a/frappe/utils/local.py b/frappe/utils/local.py new file mode 100644 index 0000000000..f9724bddc0 --- /dev/null +++ b/frappe/utils/local.py @@ -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)