Repository: pallets/werkzeug

Issue:
LocalProxy breaking isinstance() for abc.ABC
When tested with isinstance() against an ABC, LocalProxy instances raise an exception rather than returning False.

```
import abc
from werkzeug import local

ls=local.LocalStack()
x=ls()

class C(abc.ABC):
	pass

isinstance(x, C)

```
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.9/abc.py", line 119, in __instancecheck__
    return _abc_instancecheck(cls, instance)
  File "/usr/lib/python3.9/abc.py", line 123, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
TypeError: issubclass() arg 1 must be a class
```

The call to isinstance() should have returned False rather than an exception.

It seems to be related to the fact that `x.__class__` is not a class as expected, but an unbound method. Calling it with `x.__class__()` returns the expected `werkzeug.local.LocalProxy` class. While it's not documented in Python except for builtin types, most of the ecosystem will assume `type(x) is x.__class__`, including it appears the standard library.

Environment:

- Python version: Python 3.9.6
- Werkzeug version: 2.0.1



Discussion:
It looks like the C implementation of `abc` doesn't trigger the descriptor protocol when inspecting the `__class__` attribute. This seems like it should be reported to CPython.
If something is not triggered, it's not only in `abc` though:
> It seems to be related to the fact that `x.__class__` is not a class as expected, but an unbound method

This can be observed with:
```
import abc
from werkzeug import local

ls=local.LocalStack()
x=ls()

print(x.__class__)
assert isinstance(x.__class__, type)
```
Which prints:
```
<bound method LocalProxy.<lambda> of <LocalProxy unbound>>
Traceback (most recent call last):
  File "testwzeug.py", line 8, in <module>
    assert isinstance(x.__class__, type)
AssertionError
```
It would be expected to print:
```
<class 'werkzeug.local.LocalProxy'>
```
which can be obtained with `x.__class__()`. 

The descriptor protocol seems to be followed by Python:
```
(Pdb) type(x).__dict__['__class__'].__get__(x, type(x))
<bound method LocalProxy.<lambda> of <LocalProxy unbound>>
```

In local.py:
```
class _ProxyLookup:
    ....
    def __get__(self, instance: "LocalProxy", owner: t.Optional[type] = None) -> t.Any:
        ....
            return self.fallback.__get__(instance, owner)  # type: ignore
```

Replacing with `return self.fallback.__get__(instance, owner)()` fixes the problem by actually calling the bound method, rather than just returning it


Resolving patch:
diff --git a/src/werkzeug/local.py b/src/werkzeug/local.py
--- a/src/werkzeug/local.py
+++ b/src/werkzeug/local.py
@@ -381,19 +381,23 @@ 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 fallback: Call this method if the proxy is unbound instead of
-        raising a :exc:`RuntimeError`.
-    :param class_value: Value to return when accessed from the class.
-        Used for ``__doc__`` so building docs still works.
+    :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.
+        Call the fallback immediately to get the value.
+    :param class_value: Value to return when accessed from the
+        ``LocalProxy`` class directly. Used for ``__doc__`` so building
+        docs still works.
     """
 
-    __slots__ = ("bind_f", "fallback", "class_value", "name")
+    __slots__ = ("bind_f", "fallback", "is_attr", "class_value", "name")
 
     def __init__(
         self,
         f: t.Optional[t.Callable] = None,
         fallback: t.Optional[t.Callable] = None,
         class_value: t.Optional[t.Any] = None,
+        is_attr: bool = False,
     ) -> None:
         bind_f: t.Optional[t.Callable[["LocalProxy", t.Any], t.Callable]]
 
@@ -416,6 +420,7 @@ def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable:
         self.bind_f = bind_f
         self.fallback = fallback
         self.class_value = class_value
+        self.is_attr = is_attr
 
     def __set_name__(self, owner: "LocalProxy", name: str) -> None:
         self.name = name
@@ -433,7 +438,14 @@ def __get__(self, instance: "LocalProxy", owner: t.Optional[type] = None) -> t.A
             if self.fallback is None:
                 raise
 
-            return self.fallback.__get__(instance, owner)  # type: ignore
+            fallback = self.fallback.__get__(instance, owner)  # type: ignore
+
+            if self.is_attr:
+                # __class__ and __doc__ are attributes, not methods.
+                # Call the fallback to get the value.
+                return fallback()
+
+            return fallback
 
         if self.bind_f is not None:
             return self.bind_f(instance, obj)
@@ -559,7 +571,7 @@ def _get_current_object(self) -> t.Any:
             raise RuntimeError(f"no object bound to {name}") from None
 
     __doc__ = _ProxyLookup(  # type: ignore
-        class_value=__doc__, fallback=lambda self: type(self).__doc__
+        class_value=__doc__, fallback=lambda self: type(self).__doc__, is_attr=True
     )
     # __del__ should only delete the proxy
     __repr__ = _ProxyLookup(  # type: ignore
@@ -591,7 +603,9 @@ def _get_current_object(self) -> t.Any:
     # __weakref__ (__getattr__)
     # __init_subclass__ (proxying metaclass not supported)
     # __prepare__ (metaclass)
-    __class__ = _ProxyLookup(fallback=lambda self: type(self))  # type: ignore
+    __class__ = _ProxyLookup(
+        fallback=lambda self: type(self), is_attr=True
+    )  # type: ignore
     __instancecheck__ = _ProxyLookup(lambda self, other: isinstance(other, self))
     __subclasscheck__ = _ProxyLookup(lambda self, other: issubclass(other, self))
     # __class_getitem__ triggered through __getitem__


Test patch:
diff --git a/tests/test_local.py b/tests/test_local.py
--- a/tests/test_local.py
+++ b/tests/test_local.py
@@ -225,13 +225,20 @@ def example():
 
 
 def test_proxy_fallback():
-    def _raises():
-        raise RuntimeError()
+    local_stack = local.LocalStack()
+    local_proxy = local_stack()
 
-    local_proxy = local.LocalProxy(_raises)
     assert repr(local_proxy) == "<LocalProxy unbound>"
     assert isinstance(local_proxy, local.LocalProxy)
-    assert not isinstance(local_proxy, Thread)
+    assert local_proxy.__class__ is local.LocalProxy
+    assert "LocalProxy" in local_proxy.__doc__
+
+    local_stack.push(42)
+
+    assert repr(local_proxy) == "42"
+    assert isinstance(local_proxy, int)
+    assert local_proxy.__class__ is int
+    assert "int(" in local_proxy.__doc__
 
 
 def test_proxy_unbound():