Repository: pallets/werkzeug

Issue:
doctest causes a working out of context RuntimeError with request
Migrating from Flask 2.1 to 2.2 failed a few unit tests of mine. It looks like we can no longer import `flask.request` top level in a test file (reproduced across Python 3.7, 3.8, 3.9, 3.10):

```python
from flask import Flask
from flask import request

def test_something():
    assert True
```

This gives a `RuntimeError`:

```
E   RuntimeError: Working outside of request context.
E
E   This typically means that you attempted to use functionality that needed
E   an active HTTP request. Consult the documentation on testing for
E   information about how to avoid this problem.
```

On my side the fix was simple: importing `flask.request` outside of the global scope, just when I actually need it (my actual tests create small Flask apps to test a client).

Edit: forgot to mention that this only occurs when pytest is run with `--doctest-modules`


Discussion:
This used to happen in pallets/flask#1680, but was fixed in Werkzeug #924 a long time ago. Seems it has started happening again.
Running `python -m doctest example.py`, with just `from flask import request` in the file, causes the error.

What's happening is that Werkzeug only sets `__wrapped__` if the `Local` wraps a function, but does not set the attribute otherwise. Flask 2.2 started wrapping `ContextVar` directly instead of a function. Doctest uses `inspect.unwrap` to access that attribute, which fails with a proxy error instead of an attribute error. This bug was always present with doctest and previous versions, but it was hidden because of the way Flask previously used `Local`.

Unfortunately, just the presence of `__wrapped__` indicates to `inspect.unwrap` that something is a wrapper. So we can't set it to `None`, as that would make it present but incorrect.

I really think this is a bug in Python's `inspect`, it should ignore errors when accessing the attribute, but it's unlikely to get fixed there. The only think we can really do in Werkzeug is to either set the attribute to something incorrect, or see if there's a way to simulate an `AttributeError` on access.
This was reported in doctest years ago: https://github.com/python/cpython/issues/70186. I think this is an issue with doctest, Sphinx uses `inspect.unwrap` as well but catches exceptions so that it doesn't fail.

For now, I'll set `__wrapped__` to whatever `LocalProxy` is wrapping, not only if it's wrapping a function.

Resolving patch:
diff --git a/src/werkzeug/local.py b/src/werkzeug/local.py
--- a/src/werkzeug/local.py
+++ b/src/werkzeug/local.py
@@ -435,6 +435,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.
 
@@ -453,7 +457,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
@@ -515,16 +519,18 @@ 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(
+        fallback=lambda self: self._LocalProxy__wrapped, is_attr=True
+    )
     # __del__ should only delete the proxy
     __repr__ = _ProxyLookup(  # type: ignore
         repr, fallback=lambda self: f"<{type(self).__name__} unbound>"


Test patch:
diff --git a/tests/test_local.py b/tests/test_local.py
--- a/tests/test_local.py
+++ b/tests/test_local.py
@@ -4,7 +4,6 @@
 import operator
 import time
 from contextvars import ContextVar
-from functools import partial
 from threading import Thread
 
 import pytest
@@ -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)
@@ -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():