Repository: pallets/werkzeug

Issue:
Update LocalProxy special methods
Some old dunder methods from Python 2 are still present, and some new ones are missing. Go through and make sure it matches up with https://docs.python.org/3/reference/datamodel.html#special-method-names.

It might be good to sort the methods so they're in the same order as those docs.


Discussion:

Resolving patch:
diff --git a/src/werkzeug/local.py b/src/werkzeug/local.py
--- a/src/werkzeug/local.py
+++ b/src/werkzeug/local.py
@@ -1,5 +1,8 @@
 import copy
+import math
+import operator
 import typing as t
+from functools import partial
 from functools import update_wrapper
 
 from .wsgi import ClosingIterator
@@ -243,43 +246,153 @@ def __repr__(self) -> str:
         return f"<{type(self).__name__} storages: {len(self.locals)}>"
 
 
+class _ProxyLookup:
+    """Descriptor that handles proxied attribute lookup for
+    :class:`LocalProxy`.
+
+    :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.
+    """
+
+    __slots__ = ("bind_f", "fallback", "class_value", "name")
+
+    def __init__(self, f=None, fallback=None, class_value=None):
+        if hasattr(f, "__get__"):
+            # A Python function, can be turned into a bound method.
+
+            def bind_f(instance, obj):
+                return f.__get__(obj, type(obj))
+
+        elif f is not None:
+            # A C function, use partial to bind the first argument.
+
+            def bind_f(instance, obj):
+                return partial(f, obj)
+
+        else:
+            # Use getattr, which will produce a bound method.
+            bind_f = None
+
+        self.bind_f = bind_f
+        self.fallback = fallback
+        self.class_value = class_value
+
+    def __set_name__(self, owner, name):
+        self.name = name
+
+    def __get__(self, instance, owner=None):
+        if instance is None:
+            if self.class_value is not None:
+                return self.class_value
+
+            return self
+
+        try:
+            obj = instance._get_current_object()
+        except RuntimeError:
+            if self.fallback is None:
+                raise
+
+            return self.fallback.__get__(instance, owner)
+
+        if self.bind_f is not None:
+            return self.bind_f(instance, obj)
+
+        return getattr(obj, self.name)
+
+    def __repr__(self):
+        return f"proxy {self.name}"
+
+    def __call__(self, instance, *args, **kwargs):
+        """Support calling unbound methods from the class. For example,
+        this happens with ``copy.copy``, which does
+        ``type(x).__copy__(x)``. ``type(x)`` can't be proxied, so it
+        returns the proxy type and descriptor.
+        """
+        return self.__get__(instance, type(instance))(*args, **kwargs)
+
+
+class _ProxyIOp(_ProxyLookup):
+    """Look up an augmented assignment method on a proxied object. The
+    method is wrapped to return the proxy instead of the object.
+    """
+
+    __slots__ = ()
+
+    def __init__(self, f=None, fallback=None):
+        super().__init__(f, fallback)
+
+        def bind_f(instance, obj):
+            def i_op(self, other):
+                f(self, other)
+                return instance
+
+            return i_op.__get__(obj, type(obj))
+
+        self.bind_f = bind_f
+
+
+def _l_to_r_op(op):
+    """Swap the argument order to turn an l-op into an r-op."""
+
+    def r_op(obj, other):
+        return op(other, obj)
+
+    return r_op
+
+
 class LocalProxy:
-    """Acts as a proxy for a werkzeug local.  Forwards all operations to
-    a proxied object.  The only operations not supported for forwarding
-    are right handed operands and any kind of assignment.
+    """A proxy to the object bound to a :class:`Local`. All operations
+    on the proxy are forwarded to the bound object. If no object is
+    bound, a :exc:`RuntimeError` is raised.
 
-    Example usage::
+    .. code-block:: python
 
         from werkzeug.local import Local
         l = Local()
 
-        # these are proxies
-        request = l('request')
-        user = l('user')
-
+        # a proxy to whatever l.user is set to
+        user = l("user")
 
         from werkzeug.local import LocalStack
-        _response_local = LocalStack()
+        _request_stack = LocalStack()
 
-        # this is a proxy
-        response = _response_local()
+        # a proxy to _request_stack.top
+        request = _request_stack()
 
-    Whenever something is bound to l.user / l.request the proxy objects
-    will forward all operations.  If no object is bound a :exc:`RuntimeError`
-    will be raised.
+        # a proxy to the session attribute of the request proxy
+        session = LocalProxy(lambda: request.session)
 
-    To create proxies to :class:`Local` or :class:`LocalStack` objects,
-    call the object as shown above.  If you want to have a proxy to an
-    object looked up by a function, you can (as of Werkzeug 0.6.1) pass
-    a function to the :class:`LocalProxy` constructor::
+    ``__repr__`` and ``__class__`` are forwarded, so ``repr(x)`` and
+    ``isinstance(x, cls)`` will look like the proxied object. Use
+    ``issubclass(type(x), LocalProxy)`` to check if an object is a
+    proxy.
 
-        session = LocalProxy(lambda: get_current_request().session)
+    .. code-block:: python
+
+        repr(user)  # <User admin>
+        isinstance(user, User)  # True
+        issubclass(type(user), LocalProxy)  # True
+
+    :param local: The :class:`Local` or callable that provides the
+        proxied object.
+    :param name: The attribute name to look up on a :class:`Local`. Not
+        used if a callable is given.
+
+    .. versionchanged:: 2.0
+        Updated proxied attributes and methods to reflect the current
+        data model.
 
     .. versionchanged:: 0.6.1
-       The class can be instantiated with a callable as well now.
+        The class can be instantiated with a callable.
     """
 
-    __slots__ = ("__local", "__dict__", "__name__", "__wrapped__")
+    __slots__ = ("__local", "__name", "__wrapped__")
 
     def __init__(
         self,
@@ -287,7 +400,8 @@ def __init__(
         name: t.Optional[str] = None,
     ) -> None:
         object.__setattr__(self, "_LocalProxy__local", local)
-        object.__setattr__(self, "__name__", name)
+        object.__setattr__(self, "_LocalProxy__name", name)
+
         if callable(local) and not hasattr(local, "__release_local__"):
             # "local" is a callable that is not an instance of Local or
             # LocalManager: mark it as a wrapped function.
@@ -298,100 +412,125 @@ def _get_current_object(self) -> t.Any:
         object behind the proxy at a time for performance reasons or because
         you want to pass the object into a different context.
         """
-        if not hasattr(self.__local, "__release_local__"):
-            return self.__local()
-        try:
-            return getattr(self.__local, self.__name__)
-        except AttributeError:
-            raise RuntimeError(f"no object bound to {self.__name__}")
+        if not hasattr(self.__local, "__release_local__"):  # type: ignore
+            return self.__local()  # type: ignore
 
-    @property
-    def __dict__(self):
         try:
-            return self._get_current_object().__dict__
-        except RuntimeError:
-            raise AttributeError("__dict__")
-
-    def __repr__(self) -> str:
-        try:
-            obj = self._get_current_object()
-        except RuntimeError:
-            return f"<{type(self).__name__} unbound>"
-        return repr(obj)
-
-    def __bool__(self) -> bool:
-        try:
-            return bool(self._get_current_object())
-        except RuntimeError:
-            return False
-
-    def __dir__(self) -> t.List[str]:
-        try:
-            return dir(self._get_current_object())
-        except RuntimeError:
-            return []
-
-    def __getattr__(self, name: str) -> t.Any:
-        if name == "__members__":
-            return dir(self._get_current_object())
-        return getattr(self._get_current_object(), name)
-
-    def __setitem__(self, key: str, value: t.Any) -> None:
-        self._get_current_object()[key] = value
-
-    def __delitem__(self, key: str):
-        del self._get_current_object()[key]
-
-    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)  # type: ignore
-    __delattr__ = lambda x, n: delattr(x._get_current_object(), n)  # type: ignore
-    __str__ = lambda x: str(x._get_current_object())  # type: ignore
-    __lt__ = lambda x, o: x._get_current_object() < o
-    __le__ = lambda x, o: x._get_current_object() <= o
-    __eq__ = lambda x, o: x._get_current_object() == o  # type: ignore
-    __ne__ = lambda x, o: x._get_current_object() != o  # type: ignore
-    __gt__ = lambda x, o: x._get_current_object() > o
-    __ge__ = lambda x, o: x._get_current_object() >= o
-    __hash__ = lambda x: hash(x._get_current_object())  # type: ignore
-    __call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw)
-    __len__ = lambda x: len(x._get_current_object())
-    __getitem__ = lambda x, i: x._get_current_object()[i]
-    __iter__ = lambda x: iter(x._get_current_object())
-    __contains__ = lambda x, i: i in x._get_current_object()
-    __add__ = lambda x, o: x._get_current_object() + o
-    __sub__ = lambda x, o: x._get_current_object() - o
-    __mul__ = lambda x, o: x._get_current_object() * o
-    __floordiv__ = lambda x, o: x._get_current_object() // o
-    __mod__ = lambda x, o: x._get_current_object() % o
-    __divmod__ = lambda x, o: x._get_current_object().__divmod__(o)
-    __pow__ = lambda x, o: x._get_current_object() ** o
-    __lshift__ = lambda x, o: x._get_current_object() << o
-    __rshift__ = lambda x, o: x._get_current_object() >> o
-    __and__ = lambda x, o: x._get_current_object() & o
-    __xor__ = lambda x, o: x._get_current_object() ^ o
-    __or__ = lambda x, o: x._get_current_object() | o
-    __div__ = lambda x, o: x._get_current_object().__div__(o)
-    __truediv__ = lambda x, o: x._get_current_object().__truediv__(o)
-    __neg__ = lambda x: -(x._get_current_object())
-    __pos__ = lambda x: +(x._get_current_object())
-    __abs__ = lambda x: abs(x._get_current_object())
-    __invert__ = lambda x: ~(x._get_current_object())
-    __complex__ = lambda x: complex(x._get_current_object())
-    __int__ = lambda x: int(x._get_current_object())
-    __long__ = lambda x: long(x._get_current_object())  # type: ignore # noqa
-    __float__ = lambda x: float(x._get_current_object())
-    __oct__ = lambda x: oct(x._get_current_object())
-    __hex__ = lambda x: hex(x._get_current_object())
-    __index__ = lambda x: x._get_current_object().__index__()
-    __coerce__ = lambda x, o: x._get_current_object().__coerce__(x, o)
-    __enter__ = lambda x: x._get_current_object().__enter__()
-    __exit__ = lambda x, *a, **kw: x._get_current_object().__exit__(*a, **kw)
-    __radd__ = lambda x, o: o + x._get_current_object()
-    __rsub__ = lambda x, o: o - x._get_current_object()
-    __rmul__ = lambda x, o: o * x._get_current_object()
-    __rdiv__ = lambda x, o: o / x._get_current_object()
-    __rtruediv__ = __rdiv__
-    __rfloordiv__ = lambda x, o: o // x._get_current_object()
-    __rmod__ = lambda x, o: o % x._get_current_object()
-    __rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o)
-    __copy__ = lambda x: copy.copy(x._get_current_object())
-    __deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo)
+            return getattr(self.__local, self.__name)  # type: ignore
+        except AttributeError:
+            raise RuntimeError(f"no object bound to {self.__name}")  # type: ignore
+
+    __doc__ = _ProxyLookup(class_value=__doc__)  # type: ignore
+    # __del__ should only delete the proxy
+    __repr__ = _ProxyLookup(
+        repr, fallback=lambda self: f"<{type(self).__name__} unbound>"
+    )
+    __str__ = _ProxyLookup(str)
+    __bytes__ = _ProxyLookup(bytes)
+    __format__ = _ProxyLookup()  # type: ignore
+    __lt__ = _ProxyLookup(operator.lt)
+    __le__ = _ProxyLookup(operator.le)
+    __eq__ = _ProxyLookup(operator.eq)
+    __ne__ = _ProxyLookup(operator.ne)
+    __gt__ = _ProxyLookup(operator.gt)
+    __ge__ = _ProxyLookup(operator.ge)
+    __hash__ = _ProxyLookup(hash)  # type: ignore
+    __bool__ = _ProxyLookup(bool, fallback=lambda self: False)
+    __getattr__ = _ProxyLookup(getattr)
+    # __getattribute__ triggered through __getattr__
+    __setattr__ = _ProxyLookup(setattr)
+    __delattr__ = _ProxyLookup(delattr)
+    __dir__ = _ProxyLookup(dir, fallback=lambda self: [])  # type: ignore
+    # __get__ (proxying descriptor not supported)
+    # __set__ (descriptor)
+    # __delete__ (descriptor)
+    # __set_name__ (descriptor)
+    # __objclass__ (descriptor)
+    # __slots__ used by proxy itself
+    # __dict__ (__getattr__)
+    # __weakref__ (__getattr__)
+    # __init_subclass__ (proxying metaclass not supported)
+    # __prepare__ (metaclass)
+    __class__ = _ProxyLookup()  # type: ignore
+    __instancecheck__ = _ProxyLookup(lambda self, other: isinstance(other, self))
+    __subclasscheck__ = _ProxyLookup(lambda self, other: issubclass(other, self))
+    # __class_getitem__ triggered through __getitem__
+    __call__ = _ProxyLookup(lambda self, *args, **kwargs: self(*args, **kwargs))
+    __len__ = _ProxyLookup(len)
+    __length_hint__ = _ProxyLookup(operator.length_hint)
+    __getitem__ = _ProxyLookup(operator.getitem)
+    __setitem__ = _ProxyLookup(operator.setitem)
+    __delitem__ = _ProxyLookup(operator.delitem)
+    # __missing__ triggered through __getitem__
+    __iter__ = _ProxyLookup(iter)
+    __next__ = _ProxyLookup(next)
+    __reversed__ = _ProxyLookup(reversed)
+    __contains__ = _ProxyLookup(operator.contains)
+    __add__ = _ProxyLookup(operator.add)
+    __sub__ = _ProxyLookup(operator.sub)
+    __mul__ = _ProxyLookup(operator.mul)
+    __matmul__ = _ProxyLookup(operator.matmul)
+    __truediv__ = _ProxyLookup(operator.truediv)
+    __floordiv__ = _ProxyLookup(operator.floordiv)
+    __mod__ = _ProxyLookup(operator.mod)
+    __divmod__ = _ProxyLookup(divmod)
+    __pow__ = _ProxyLookup(pow)
+    __lshift__ = _ProxyLookup(operator.lshift)
+    __rshift__ = _ProxyLookup(operator.rshift)
+    __and__ = _ProxyLookup(operator.and_)
+    __xor__ = _ProxyLookup(operator.xor)
+    __or__ = _ProxyLookup(operator.or_)
+    __radd__ = _ProxyLookup(_l_to_r_op(operator.add))
+    __rsub__ = _ProxyLookup(_l_to_r_op(operator.sub))
+    __rmul__ = _ProxyLookup(_l_to_r_op(operator.mul))
+    __rmatmul__ = _ProxyLookup(_l_to_r_op(operator.matmul))
+    __rtruediv__ = _ProxyLookup(_l_to_r_op(operator.truediv))
+    __rfloordiv__ = _ProxyLookup(_l_to_r_op(operator.floordiv))
+    __rmod__ = _ProxyLookup(_l_to_r_op(operator.mod))
+    __rdivmod__ = _ProxyLookup(_l_to_r_op(divmod))
+    __rpow__ = _ProxyLookup(_l_to_r_op(pow))
+    __rlshift__ = _ProxyLookup(_l_to_r_op(operator.lshift))
+    __rrshift__ = _ProxyLookup(_l_to_r_op(operator.rshift))
+    __rand__ = _ProxyLookup(_l_to_r_op(operator.and_))
+    __rxor__ = _ProxyLookup(_l_to_r_op(operator.xor))
+    __ror__ = _ProxyLookup(_l_to_r_op(operator.or_))
+    __iadd__ = _ProxyIOp(operator.iadd)
+    __isub__ = _ProxyIOp(operator.isub)
+    __imul__ = _ProxyIOp(operator.imul)
+    __imatmul__ = _ProxyIOp(operator.imatmul)
+    __itruediv__ = _ProxyIOp(operator.itruediv)
+    __ifloordiv__ = _ProxyIOp(operator.ifloordiv)
+    __imod__ = _ProxyIOp(operator.imod)
+    __ipow__ = _ProxyIOp(operator.ipow)
+    __ilshift__ = _ProxyIOp(operator.ilshift)
+    __irshift__ = _ProxyIOp(operator.irshift)
+    __iand__ = _ProxyIOp(operator.iand)
+    __ixor__ = _ProxyIOp(operator.ixor)
+    __ior__ = _ProxyIOp(operator.ior)
+    __neg__ = _ProxyLookup(operator.neg)
+    __pos__ = _ProxyLookup(operator.pos)
+    __abs__ = _ProxyLookup(abs)
+    __invert__ = _ProxyLookup(operator.invert)
+    __complex__ = _ProxyLookup(complex)
+    __int__ = _ProxyLookup(int)
+    __float__ = _ProxyLookup(float)
+    __index__ = _ProxyLookup(operator.index)
+    __round__ = _ProxyLookup(round)
+    __trunc__ = _ProxyLookup(math.trunc)
+    __floor__ = _ProxyLookup(math.floor)
+    __ceil__ = _ProxyLookup(math.ceil)
+    __enter__ = _ProxyLookup()
+    __exit__ = _ProxyLookup()
+    __await__ = _ProxyLookup()
+    __aiter__ = _ProxyLookup()
+    __anext__ = _ProxyLookup()
+    __aenter__ = _ProxyLookup()
+    __aexit__ = _ProxyLookup()
+    __copy__ = _ProxyLookup(copy.copy)
+    __deepcopy__ = _ProxyLookup(copy.deepcopy)
+    # __getnewargs_ex__ (pickle through proxy not supported)
+    # __getnewargs__ (pickle)
+    # __getstate__ (pickle)
+    # __setstate__ (pickle)
+    # __reduce__ (pickle)
+    # __reduce_ex__ (pickle)


Test patch:
diff --git a/tests/test_local.py b/tests/test_local.py
--- a/tests/test_local.py
+++ b/tests/test_local.py
@@ -1,4 +1,7 @@
+import asyncio
 import copy
+import math
+import operator
 import time
 from functools import partial
 from threading import Thread
@@ -48,49 +51,6 @@ def test_local_release():
     assert ls.top is None
 
 
-def test_local_proxy():
-    foo = []
-    ls = local.LocalProxy(lambda: foo)
-    ls.append(42)
-    ls.append(23)
-    ls[1:] = [1, 2, 3]
-    assert foo == [42, 1, 2, 3]
-    assert repr(foo) == repr(ls)
-    assert foo[0] == 42
-    foo += [1]
-    assert list(foo) == [42, 1, 2, 3, 1]
-
-
-def test_local_proxy_operations_math():
-    foo = 2
-    ls = local.LocalProxy(lambda: foo)
-    assert ls == 2
-    assert ls != 3
-    assert ls + 1 == 3
-    assert 1 + ls == 3
-    assert ls - 1 == 1
-    assert 1 - ls == -1
-    assert ls * 1 == 2
-    assert 1 * ls == 2
-    assert ls / 1 == 2
-    assert 1.0 / ls == 0.5
-    assert ls // 1.0 == 2.0
-    assert 1.0 // ls == 0.0
-    assert ls % 2 == 0
-    assert 2 % ls == 0
-
-
-def test_local_proxy_operations_strings():
-    foo = "foo"
-    ls = local.LocalProxy(lambda: foo)
-    assert ls + "bar" == "foobar"
-    assert "bar" + ls == "barfoo"
-    assert ls * 2 == "foofoo"
-
-    foo = "foo %s"
-    assert ls % ("bar",) == "foo bar"
-
-
 def test_local_stack():
     ident = local.get_ident()
 
@@ -121,16 +81,6 @@ def test_local_stack():
     assert ident not in ls._local.__storage__
 
 
-def test_local_proxies_with_callables():
-    foo = 42
-    ls = local.LocalProxy(lambda: foo)
-    assert ls == 42
-    foo = [23]
-    ls.append(42)
-    assert ls == [23, 42]
-    assert foo == [23, 42]
-
-
 def test_custom_idents():
     ident = 0
     ns = local.Local()
@@ -154,43 +104,45 @@ def test_custom_idents():
     assert stack.top is None
 
 
-def test_deepcopy_on_proxy():
-    class Foo:
-        attr = 42
-
-        def __copy__(self):
-            return self
-
-        def __deepcopy__(self, memo):
-            return self
-
-    f = Foo()
-    p = local.LocalProxy(lambda: f)
-    assert p.attr == 42
-    assert copy.deepcopy(p) is f
-    assert copy.copy(p) is f
-
-    a = []
-    p2 = local.LocalProxy(lambda: [a])
-    assert copy.copy(p2) == [a]
-    assert copy.copy(p2)[0] is a
-
-    assert copy.deepcopy(p2) == [a]
-    assert copy.deepcopy(p2)[0] is not a
-
-
-def test_local_proxy_wrapped_attribute():
+def test_proxy_local():
+    ns = local.Local()
+    ns.foo = []
+    p = local.LocalProxy(ns, "foo")
+    p.append(42)
+    p.append(23)
+    p[1:] = [1, 2, 3]
+    assert p == [42, 1, 2, 3]
+    assert p == ns.foo
+    ns.foo += [1]
+    assert list(p) == [42, 1, 2, 3, 1]
+    p_from_local = ns("foo")
+    p_from_local.append(2)
+    assert p == p_from_local
+    assert p._get_current_object() is ns.foo
+
+
+def test_proxy_callable():
+    value = 42
+    p = local.LocalProxy(lambda: value)
+    assert p == 42
+    value = [23]
+    p.append(42)
+    assert p == [23, 42]
+    assert value == [23, 42]
+    assert p._get_current_object() is value
+
+
+def test_proxy_wrapped():
     class SomeClassWithWrapped:
         __wrapped__ = "wrapped"
 
     def lookup_func():
         return 42
 
-    partial_lookup_func = partial(lookup_func)
-
     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
 
@@ -200,3 +152,419 @@ def lookup_func():
 
     assert ns("foo").__wrapped__ == "wrapped"
     pytest.raises(AttributeError, lambda: ns("bar").__wrapped__)
+
+
+def test_proxy_doc():
+    def example():
+        """example doc"""
+
+    assert local.LocalProxy(lambda: example).__doc__ == "example doc"
+    # The __doc__ descriptor shouldn't block the LocalProxy's class doc.
+    assert local.LocalProxy.__doc__.startswith("A proxy")
+
+
+def test_proxy_unbound():
+    ns = local.Local()
+    p = ns("value")
+    assert repr(p) == "<LocalProxy unbound>"
+    assert not p
+    assert dir(p) == []
+
+
+def _make_proxy(value):
+    ns = local.Local()
+    ns.value = value
+    p = ns("value")
+    return ns, p
+
+
+def test_proxy_type():
+    _, p = _make_proxy([])
+    assert isinstance(p, list)
+    assert p.__class__ is list
+    assert issubclass(type(p), local.LocalProxy)
+    assert type(p) is local.LocalProxy
+
+
+def test_proxy_string_representations():
+    class Example:
+        def __repr__(self):
+            return "a"
+
+        def __bytes__(self):
+            return b"b"
+
+        def __index__(self):
+            return 23
+
+    _, p = _make_proxy(Example())
+    assert str(p) == "a"
+    assert repr(p) == "a"
+    assert bytes(p) == b"b"
+    # __index__
+    assert bin(p) == "0b10111"
+    assert oct(p) == "0o27"
+    assert hex(p) == "0x17"
+
+
+def test_proxy_hash():
+    ns, p = _make_proxy("abc")
+    assert hash(ns.value) == hash(p)
+
+
+@pytest.mark.parametrize(
+    "op",
+    [
+        operator.lt,
+        operator.le,
+        operator.eq,
+        operator.ne,
+        operator.gt,
+        operator.ge,
+        operator.add,
+        operator.sub,
+        operator.mul,
+        operator.truediv,
+        operator.floordiv,
+        operator.mod,
+        divmod,
+        pow,
+        operator.lshift,
+        operator.rshift,
+        operator.and_,
+        operator.or_,
+        operator.xor,
+    ],
+)
+def test_proxy_binop_int(op):
+    _, p = _make_proxy(2)
+    assert op(p, 3) == op(2, 3)
+    # r-op
+    assert op(3, p) == op(3, 2)
+
+
+@pytest.mark.parametrize("op", [operator.neg, operator.pos, abs, operator.invert])
+def test_proxy_uop_int(op):
+    _, p = _make_proxy(-2)
+    assert op(p) == op(-2)
+
+
+def test_proxy_numeric():
+    class Example:
+        def __complex__(self):
+            return 1 + 2j
+
+        def __int__(self):
+            return 1
+
+        def __float__(self):
+            return 2.1
+
+        def __round__(self, n=None):
+            if n is not None:
+                return 3.3
+
+            return 3
+
+        def __trunc__(self):
+            return 4
+
+        def __floor__(self):
+            return 5
+
+        def __ceil__(self):
+            return 6
+
+        def __index__(self):
+            return 2
+
+    _, p = _make_proxy(Example())
+    assert complex(p) == 1 + 2j
+    assert int(p) == 1
+    assert float(p) == 2.1
+    assert round(p) == 3
+    assert round(p, 2) == 3.3
+    assert math.trunc(p) == 4
+    assert math.floor(p) == 5
+    assert math.ceil(p) == 6
+    assert [1, 2, 3][p] == 3  # __index__
+
+
+@pytest.mark.parametrize(
+    "op",
+    [
+        operator.iadd,
+        operator.isub,
+        operator.imul,
+        operator.imatmul,
+        operator.itruediv,
+        operator.ifloordiv,
+        operator.imod,
+        operator.ipow,
+        operator.ilshift,
+        operator.irshift,
+        operator.iand,
+        operator.ior,
+        operator.ixor,
+    ],
+)
+def test_proxy_iop(op):
+    class Example:
+        value = 1
+
+        def fake_op(self, other):
+            self.value = other
+            return self
+
+        __iadd__ = fake_op
+        __isub__ = fake_op
+        __imul__ = fake_op
+        __imatmul__ = fake_op
+        __itruediv__ = fake_op
+        __ifloordiv__ = fake_op
+        __imod__ = fake_op
+        __ipow__ = fake_op
+        __ilshift__ = fake_op
+        __irshift__ = fake_op
+        __iand__ = fake_op
+        __ior__ = fake_op
+        __ixor__ = fake_op
+
+    ns, p = _make_proxy(Example())
+    p_out = op(p, 2)
+    assert type(p_out) is local.LocalProxy
+    assert p.value == 2
+    assert ns.value.value == 2
+
+
+def test_proxy_matmul():
+    class Example:
+        def __matmul__(self, other):
+            return 2 * other
+
+        def __rmatmul__(self, other):
+            return 2 * other
+
+    _, p = _make_proxy(Example())
+    assert p @ 3 == 6
+    assert 4 @ p == 8
+
+
+def test_proxy_str():
+    _, p = _make_proxy("{act} %s")
+    assert p + " world" == "{act} %s world"
+    assert "say " + p == "say {act} %s"
+    assert p * 2 == "{act} %s{act} %s"
+    assert 2 * p == p * 2
+    assert p % ("world",) == "{act} world"
+    assert p.format(act="test") == "test %s"
+
+
+def test_proxy_list():
+    _, p = _make_proxy([1, 2, 3])
+    assert len(p) == 3
+    assert p[0] == 1
+    assert 3 in p
+    assert 4 not in p
+    assert tuple(p) == (1, 2, 3)
+    assert list(reversed(p)) == [3, 2, 1]
+    p[0] = 4
+    assert p == [4, 2, 3]
+    del p[-1]
+    assert p == [4, 2]
+    p += [5]
+    assert p[-1] == 5
+    p *= 2
+    assert len(p) == 6
+    p[:] = []
+    assert not p
+    p.append(1)
+    assert p
+    assert p + [2] == [1, 2]
+    assert [2] + p == [2, 1]
+
+
+def test_proxy_copy():
+    class Foo:
+        def __copy__(self):
+            return self
+
+        def __deepcopy__(self, memo):
+            return self
+
+    ns, p = _make_proxy(Foo())
+    assert copy.copy(p) is ns.value
+    assert copy.deepcopy(p) is ns.value
+
+    a = []
+    _, p = _make_proxy([a])
+    assert copy.copy(p) == [a]
+    assert copy.copy(p)[0] is a
+    assert copy.deepcopy(p) == [a]
+    assert copy.deepcopy(p)[0] is not a
+
+
+def test_proxy_iterator():
+    a = [1, 2, 3]
+    _, p = _make_proxy(iter(a))
+    assert next(p) == 1
+
+
+def test_proxy_length_hint():
+    class Example:
+        def __length_hint__(self):
+            return 2
+
+    _, p = _make_proxy(Example())
+    assert operator.length_hint(p) == 2
+
+
+def test_proxy_context_manager():
+    class Example:
+        value = 2
+
+        def __enter__(self):
+            self.value += 1
+            return self
+
+        def __exit__(self, exc_type, exc_val, exc_tb):
+            self.value -= 1
+
+    _, p = _make_proxy(Example())
+    assert p.value == 2
+
+    with p:
+        assert p.value == 3
+
+    assert p.value == 2
+
+
+def test_proxy_class():
+    class Meta(type):
+        def __instancecheck__(cls, instance):
+            return True
+
+        def __subclasscheck__(cls, subclass):
+            return True
+
+    class Parent:
+        pass
+
+    class Example(Parent, metaclass=Meta):
+        pass
+
+    class Child(Example):
+        pass
+
+    _, p = _make_proxy(Example)
+    assert type(p()) is Example
+    assert isinstance(1, p)
+    assert issubclass(int, p)
+    assert p.__mro__ == (Example, Parent, object)
+    assert p.__bases__ == (Parent,)
+    assert p.__subclasses__() == [Child]
+
+
+def test_proxy_attributes():
+    class Example:
+        def __init__(self):
+            object.__setattr__(self, "values", {})
+
+        def __getattribute__(self, name):
+            if name == "ham":
+                return "eggs"
+
+            return super().__getattribute__(name)
+
+        def __getattr__(self, name):
+            return self.values.get(name)
+
+        def __setattr__(self, name, value):
+            self.values[name] = value
+
+        def __delattr__(self, name):
+            del self.values[name]
+
+        def __dir__(self):
+            return sorted(self.values.keys())
+
+    _, p = _make_proxy(Example())
+    assert p.nothing is None
+    assert p.__dict__ == {"values": {}}
+    assert dir(p) == []
+
+    p.x = 1
+    assert p.x == 1
+    assert dir(p) == ["x"]
+
+    del p.x
+    assert dir(p) == []
+
+    assert p.ham == "eggs"
+    p.ham = "spam"
+    assert p.ham == "eggs"
+    assert p.values["ham"] == "spam"
+
+
+def test_proxy_await():
+    async def get():
+        return 1
+
+    _, p = _make_proxy(get())
+
+    async def main():
+        return await p
+
+    out = asyncio.get_event_loop().run_until_complete(main())
+    assert out == 1
+
+
+def test_proxy_aiter():
+    class Example:
+        value = 3
+
+        def __aiter__(self):
+            return self
+
+        async def __anext__(self):
+            if self.value:
+                self.value -= 1
+                return self.value
+
+            raise StopAsyncIteration
+
+    _, p = _make_proxy(Example())
+
+    async def main():
+        out = []
+
+        async for v in p:
+            out.append(v)
+
+        return out
+
+    out = asyncio.get_event_loop().run_until_complete(main())
+    assert out == [2, 1, 0]
+
+
+def test_proxy_async_context_manager():
+    class Example:
+        value = 2
+
+        async def __aenter__(self):
+            self.value += 1
+            return self
+
+        async def __aexit__(self, exc_type, exc_val, exc_tb):
+            self.value -= 1
+
+    _, p = _make_proxy(Example())
+
+    async def main():
+        async with p:
+            assert p.value == 3
+
+        assert p.value == 2
+        return True
+
+    assert asyncio.get_event_loop().run_until_complete(main())