Skip to content

Commit

Permalink
LocalProxy.__wrapped__ is always set when unbound
Browse files Browse the repository at this point in the history
  • Loading branch information
davidism committed Aug 8, 2022
1 parent fe8a56e commit 128d3d3
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 15 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Unreleased
- Parsing of some invalid header characters is more robust. :pr:`2494`
- When starting the development server, a warning not to use it in a
production deployment is always shown. :issue:`2480`
- ``LocalProxy.__wrapped__`` is always set to the wrapped object when
the proxy is unbound, fixing an issue in doctest that would cause it
to fail. :issue:`2485`


Version 2.2.1
Expand Down
26 changes: 21 additions & 5 deletions src/werkzeug/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ class _ProxyLookup:
:param f: The built-in function this attribute is accessed through.
Instead of looking up the special method, the function call
is redone on the object.
:param slot: Return this attribute on the proxy itself if the proxy
is unbound. Used for ``__wrapped__``.
:param fallback: Return this function if the proxy is unbound
instead of raising a :exc:`RuntimeError`.
:param is_attr: This proxied name is an attribute, not a function.
Expand All @@ -270,11 +272,12 @@ class _ProxyLookup:
docs still works.
"""

__slots__ = ("bind_f", "fallback", "is_attr", "class_value", "name")
__slots__ = ("bind_f", "slot", "fallback", "is_attr", "class_value", "name")

def __init__(
self,
f: t.Optional[t.Callable] = None,
slot: t.Union[str, None] = None,
fallback: t.Optional[t.Callable] = None,
class_value: t.Optional[t.Any] = None,
is_attr: bool = False,
Expand All @@ -298,6 +301,7 @@ def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable:
bind_f = None

self.bind_f = bind_f
self.slot = slot
self.fallback = fallback
self.class_value = class_value
self.is_attr = is_attr
Expand All @@ -315,6 +319,9 @@ def __get__(self, instance: "LocalProxy", owner: t.Optional[type] = None) -> t.A
try:
obj = instance._get_current_object() # type: ignore[misc]
except RuntimeError:
if self.slot is not None:
return getattr(instance, self.slot)

if self.fallback is None:
raise

Expand All @@ -328,7 +335,12 @@ def __get__(self, instance: "LocalProxy", owner: t.Optional[type] = None) -> t.A
return fallback

if self.bind_f is not None:
return self.bind_f(instance, obj)
f = self.bind_f(instance, obj)

if self.is_attr:
return f()

return f

return getattr(obj, self.name)

Expand Down Expand Up @@ -435,6 +447,10 @@ class LocalProxy(t.Generic[T]):
isinstance(user, User) # True
issubclass(type(user), LocalProxy) # True
.. versionchanged:: 2.2.2
``__wrapped__`` is set when wrapping an object, not only when
wrapping a function, to prevent doctest from failing.
.. versionchanged:: 2.2
Can proxy a ``ContextVar`` or ``LocalStack`` directly.
Expand All @@ -453,7 +469,7 @@ class LocalProxy(t.Generic[T]):
The class can be instantiated with a callable.
"""

__slots__ = ("__wrapped__", "_get_current_object")
__slots__ = ("__wrapped", "_get_current_object")

_get_current_object: t.Callable[[], T]
"""Return the current object this proxy is bound to. If the proxy is
Expand Down Expand Up @@ -515,16 +531,16 @@ def _get_current_object() -> T:
def _get_current_object() -> T:
return get_name(local()) # type: ignore

object.__setattr__(self, "__wrapped__", local)

else:
raise TypeError(f"Don't know how to proxy '{type(local)}'.")

object.__setattr__(self, "_LocalProxy__wrapped", local)
object.__setattr__(self, "_get_current_object", _get_current_object)

__doc__ = _ProxyLookup( # type: ignore
class_value=__doc__, fallback=lambda self: type(self).__doc__, is_attr=True
)
__wrapped__ = _ProxyLookup(slot="_LocalProxy__wrapped")
# __del__ should only delete the proxy
__repr__ = _ProxyLookup( # type: ignore
repr, fallback=lambda self: f"<{type(self).__name__} unbound>"
Expand Down
19 changes: 9 additions & 10 deletions tests/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import operator
import time
from contextvars import ContextVar
from functools import partial
from threading import Thread

import pytest
Expand All @@ -15,6 +14,7 @@
# to avoid accumulating anonymous context vars that can't be collected.
_cv_ns = ContextVar("werkzeug.tests.ns")
_cv_stack = ContextVar("werkzeug.tests.stack")
_cv_val = ContextVar("werkzeug.tests.val")


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -165,22 +165,21 @@ def test_proxy_wrapped():
class SomeClassWithWrapped:
__wrapped__ = "wrapped"

def lookup_func():
return 42
proxy = local.LocalProxy(_cv_val)
assert proxy.__wrapped__ is _cv_val
_cv_val.set(42)

proxy = local.LocalProxy(lookup_func)
assert proxy.__wrapped__ is lookup_func

partial_lookup_func = partial(lookup_func)
partial_proxy = local.LocalProxy(partial_lookup_func)
assert partial_proxy.__wrapped__ == partial_lookup_func
with pytest.raises(AttributeError):
proxy.__wrapped__

ns = local.Local(_cv_ns)
ns.foo = SomeClassWithWrapped()
ns.bar = 42

assert ns("foo").__wrapped__ == "wrapped"
pytest.raises(AttributeError, lambda: ns("bar").__wrapped__)

with pytest.raises(AttributeError):
ns("bar").__wrapped__


def test_proxy_doc():
Expand Down

0 comments on commit 128d3d3

Please sign in to comment.