Skip to content

Commit

Permalink
Refactor code so pure Python version usable even when extension compi…
Browse files Browse the repository at this point in the history
…led.
  • Loading branch information
GrahamDumpleton committed Nov 5, 2023
1 parent 8567e8b commit f7a28f4
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 237 deletions.
9 changes: 6 additions & 3 deletions src/wrapt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
__version_info__ = ('1', '16', '0rc2')
__version__ = '.'.join(__version_info__)

from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper,
BoundFunctionWrapper, WeakFunctionProxy, PartialCallableObjectProxy,
resolve_path, apply_patch, wrap_object, wrap_object_attribute,
from .variants import (ObjectProxy, CallableObjectProxy, FunctionWrapper,
BoundFunctionWrapper, PartialCallableObjectProxy)

from .patches import (resolve_path, apply_patch, wrap_object, wrap_object_attribute,
function_wrapper, wrap_function_wrapper, patch_function_wrapper,
transient_function_wrapper)

from .weakrefs import WeakFunctionProxy

from .decorators import (adapter_factory, AdapterFactory, decorator,
synchronized)

Expand Down
14 changes: 14 additions & 0 deletions src/wrapt/__wrapt__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import os

from .wrappers import (ObjectProxy, CallableObjectProxy,
PartialCallableObjectProxy, FunctionWrapper,
BoundFunctionWrapper, _FunctionWrapperBase)

try:
if not os.environ.get('WRAPT_DISABLE_EXTENSIONS'):
from ._wrappers import (ObjectProxy, CallableObjectProxy,
PartialCallableObjectProxy, FunctionWrapper,
BoundFunctionWrapper, _FunctionWrapperBase)

except ImportError:
pass
2 changes: 1 addition & 1 deletion src/wrapt/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def exec_(_code_, _globs_=None, _locs_=None):
except ImportError:
pass

from .wrappers import (FunctionWrapper, BoundFunctionWrapper, ObjectProxy,
from .variants import (FunctionWrapper, BoundFunctionWrapper, ObjectProxy,
CallableObjectProxy)

# Adapter wrapper for the wrapped function which will overlay certain
Expand Down
2 changes: 1 addition & 1 deletion src/wrapt/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
string_types = str,
from importlib.util import find_spec

from .wrappers import ObjectProxy
from .variants import ObjectProxy

# The dictionary registering any post import hooks to be triggered once
# the target module has been imported. Once a module has been imported
Expand Down
141 changes: 141 additions & 0 deletions src/wrapt/patches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import inspect
import sys

PY2 = sys.version_info[0] == 2

if PY2:
string_types = basestring,
else:
string_types = str,

from .variants import FunctionWrapper

# Helper functions for applying wrappers to existing functions.

def resolve_path(module, name):
if isinstance(module, string_types):
__import__(module)
module = sys.modules[module]

parent = module

path = name.split('.')
attribute = path[0]

# We can't just always use getattr() because in doing
# that on a class it will cause binding to occur which
# will complicate things later and cause some things not
# to work. For the case of a class we therefore access
# the __dict__ directly. To cope though with the wrong
# class being given to us, or a method being moved into
# a base class, we need to walk the class hierarchy to
# work out exactly which __dict__ the method was defined
# in, as accessing it from __dict__ will fail if it was
# not actually on the class given. Fallback to using
# getattr() if we can't find it. If it truly doesn't
# exist, then that will fail.

def lookup_attribute(parent, attribute):
if inspect.isclass(parent):
for cls in inspect.getmro(parent):
if attribute in vars(cls):
return vars(cls)[attribute]
else:
return getattr(parent, attribute)
else:
return getattr(parent, attribute)

original = lookup_attribute(parent, attribute)

for attribute in path[1:]:
parent = original
original = lookup_attribute(parent, attribute)

return (parent, attribute, original)

def apply_patch(parent, attribute, replacement):
setattr(parent, attribute, replacement)

def wrap_object(module, name, factory, args=(), kwargs={}):
(parent, attribute, original) = resolve_path(module, name)
wrapper = factory(original, *args, **kwargs)
apply_patch(parent, attribute, wrapper)
return wrapper

# Function for applying a proxy object to an attribute of a class
# instance. The wrapper works by defining an attribute of the same name
# on the class which is a descriptor and which intercepts access to the
# instance attribute. Note that this cannot be used on attributes which
# are themselves defined by a property object.

class AttributeWrapper(object):

def __init__(self, attribute, factory, args, kwargs):
self.attribute = attribute
self.factory = factory
self.args = args
self.kwargs = kwargs

def __get__(self, instance, owner):
value = instance.__dict__[self.attribute]
return self.factory(value, *self.args, **self.kwargs)

def __set__(self, instance, value):
instance.__dict__[self.attribute] = value

def __delete__(self, instance):
del instance.__dict__[self.attribute]

def wrap_object_attribute(module, name, factory, args=(), kwargs={}):
path, attribute = name.rsplit('.', 1)
parent = resolve_path(module, path)[2]
wrapper = AttributeWrapper(attribute, factory, args, kwargs)
apply_patch(parent, attribute, wrapper)
return wrapper

# Functions for creating a simple decorator using a FunctionWrapper,
# plus short cut functions for applying wrappers to functions. These are
# for use when doing monkey patching. For a more featured way of
# creating decorators see the decorator decorator instead.

def function_wrapper(wrapper):
def _wrapper(wrapped, instance, args, kwargs):
target_wrapped = args[0]
if instance is None:
target_wrapper = wrapper
elif inspect.isclass(instance):
target_wrapper = wrapper.__get__(None, instance)
else:
target_wrapper = wrapper.__get__(instance, type(instance))
return FunctionWrapper(target_wrapped, target_wrapper)
return FunctionWrapper(wrapper, _wrapper)

def wrap_function_wrapper(module, name, wrapper):
return wrap_object(module, name, FunctionWrapper, (wrapper,))

def patch_function_wrapper(module, name, enabled=None):
def _wrapper(wrapper):
return wrap_object(module, name, FunctionWrapper, (wrapper, enabled))
return _wrapper

def transient_function_wrapper(module, name):
def _decorator(wrapper):
def _wrapper(wrapped, instance, args, kwargs):
target_wrapped = args[0]
if instance is None:
target_wrapper = wrapper
elif inspect.isclass(instance):
target_wrapper = wrapper.__get__(None, instance)
else:
target_wrapper = wrapper.__get__(instance, type(instance))
def _execute(wrapped, instance, args, kwargs):
(parent, attribute, original) = resolve_path(module, name)
replacement = FunctionWrapper(original, target_wrapper)
setattr(parent, attribute, replacement)
try:
return wrapped(*args, **kwargs)
finally:
setattr(parent, attribute, original)
return FunctionWrapper(target_wrapped, _execute)
return FunctionWrapper(wrapper, _wrapper)
return _decorator
98 changes: 98 additions & 0 deletions src/wrapt/weakrefs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import functools
import weakref

from .variants import ObjectProxy, _FunctionWrapperBase

# A weak function proxy. This will work on instance methods, class
# methods, static methods and regular functions. Special treatment is
# needed for the method types because the bound method is effectively a
# transient object and applying a weak reference to one will immediately
# result in it being destroyed and the weakref callback called. The weak
# reference is therefore applied to the instance the method is bound to
# and the original function. The function is then rebound at the point
# of a call via the weak function proxy.

def _weak_function_proxy_callback(ref, proxy, callback):
if proxy._self_expired:
return

proxy._self_expired = True

# This could raise an exception. We let it propagate back and let
# the weakref.proxy() deal with it, at which point it generally
# prints out a short error message direct to stderr and keeps going.

if callback is not None:
callback(proxy)

class WeakFunctionProxy(ObjectProxy):

__slots__ = ('_self_expired', '_self_instance')

def __init__(self, wrapped, callback=None):
# We need to determine if the wrapped function is actually a
# bound method. In the case of a bound method, we need to keep a
# reference to the original unbound function and the instance.
# This is necessary because if we hold a reference to the bound
# function, it will be the only reference and given it is a
# temporary object, it will almost immediately expire and
# the weakref callback triggered. So what is done is that we
# hold a reference to the instance and unbound function and
# when called bind the function to the instance once again and
# then call it. Note that we avoid using a nested function for
# the callback here so as not to cause any odd reference cycles.

_callback = callback and functools.partial(
_weak_function_proxy_callback, proxy=self,
callback=callback)

self._self_expired = False

if isinstance(wrapped, _FunctionWrapperBase):
self._self_instance = weakref.ref(wrapped._self_instance,
_callback)

if wrapped._self_parent is not None:
super(WeakFunctionProxy, self).__init__(
weakref.proxy(wrapped._self_parent, _callback))

else:
super(WeakFunctionProxy, self).__init__(
weakref.proxy(wrapped, _callback))

return

try:
self._self_instance = weakref.ref(wrapped.__self__, _callback)

super(WeakFunctionProxy, self).__init__(
weakref.proxy(wrapped.__func__, _callback))

except AttributeError:
self._self_instance = None

super(WeakFunctionProxy, self).__init__(
weakref.proxy(wrapped, _callback))

def __call__(*args, **kwargs):
def _unpack_self(self, *args):
return self, args

self, args = _unpack_self(*args)

# We perform a boolean check here on the instance and wrapped
# function as that will trigger the reference error prior to
# calling if the reference had expired.

instance = self._self_instance and self._self_instance()
function = self.__wrapped__ and self.__wrapped__

# If the wrapped function was originally a bound function, for
# which we retained a reference to the instance and the unbound
# function we need to rebind the function and then call it. If
# not just called the wrapped function.

if instance is None:
return self.__wrapped__(*args, **kwargs)

return function.__get__(instance, type(instance))(*args, **kwargs)
Loading

0 comments on commit f7a28f4

Please sign in to comment.