diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b4a29d8a..2aa76d97 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,11 @@ on: - - push - - pull_request + workflow_dispatch: + pull_request: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+dev[0-9]+" jobs: @@ -12,18 +17,19 @@ jobs: os: - ubuntu-20.04 python-version: - - 2.7 - - 3.5 + # - 2.7 + # - 3.5 - 3.6 - 3.7 - 3.8 - 3.9 - "3.10" - 3.11 - - 3.12-dev - - pypy-2.7 + - 3.12 + # - pypy-2.7 - pypy-3.8 - pypy-3.9 + - pypy-3.10 steps: - name: Checkout code uses: actions/checkout@v3 @@ -85,17 +91,19 @@ jobs: os: - macos-latest python-version: - - 2.7 - - 3.5 + # - 2.7 + # - 3.5 - 3.6 - 3.7 - 3.8 - 3.9 - "3.10" - 3.11 - - pypy-2.7 + - 3.12 + # - pypy-2.7 - pypy-3.8 - pypy-3.9 + - pypy-3.10 steps: - name: Checkout code uses: actions/checkout@v3 @@ -115,28 +123,28 @@ jobs: name: coverage path: .coverage.* - test_windows_py27: - name: Test (${{ matrix.os }}, ${{ matrix.python-version }}) - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - - windows-latest - python-version: - - 2.7 - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Update pip - run: python -m pip install -U pip wheel setuptools - - name: Install tox - run: python -m pip install "tox<4.0.0" "tox-gh-actions<3.0.0" - - name: Test with tox - run: python -m tox -e py27,py27-without-extensions + # test_windows_py27: + # name: Test (${{ matrix.os }}, ${{ matrix.python-version }}) + # runs-on: ${{ matrix.os }} + # strategy: + # matrix: + # os: + # - windows-latest + # python-version: + # - 2.7 + # steps: + # - name: Checkout code + # uses: actions/checkout@v3 + # - name: Setup Python ${{ matrix.python-version }} + # uses: actions/setup-python@v4 + # with: + # python-version: ${{ matrix.python-version }} + # - name: Update pip + # run: python -m pip install -U pip wheel setuptools + # - name: Install tox + # run: python -m pip install "tox<4.0.0" "tox-gh-actions<3.0.0" + # - name: Test with tox + # run: python -m tox -e py27,py27-without-extensions test_windows: name: Test (${{ matrix.os }}, ${{ matrix.python-version }}) @@ -146,16 +154,18 @@ jobs: os: - windows-latest python-version: - - 3.5 + # - 3.5 - 3.6 - 3.7 - 3.8 - 3.9 - "3.10" - 3.11 - - pypy-2.7 + - 3.12 + # - pypy-2.7 - pypy-3.8 - pypy-3.9 + - pypy-3.10 steps: - name: Checkout code uses: actions/checkout@v3 @@ -198,32 +208,32 @@ jobs: name: dist path: dist/* - bdist_wheel_legacy: - name: Build wheels (2.7-3.5) on ${{ matrix.os }} - needs: - - test_linux - - test_macos - - test_windows_py27 - - test_windows - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-20.04, windows-latest, macos-latest] - steps: - - uses: actions/checkout@v3 - - name: Build wheels - uses: pypa/cibuildwheel@v1.11.1.post1 - with: - output-dir: dist - env: - WRAPT_INSTALL_EXTENSIONS: true - CIBW_BUILD: cp27* cp35* - CIBW_SKIP: cp27-win* - CIBW_BUILD_VERBOSITY: 1 - - uses: actions/upload-artifact@v3 - with: - name: dist - path: dist/*.whl + # bdist_wheel_legacy: + # name: Build wheels (2.7-3.5) on ${{ matrix.os }} + # needs: + # - test_linux + # - test_macos + # - test_windows_py27 + # - test_windows + # runs-on: ${{ matrix.os }} + # strategy: + # matrix: + # os: [ubuntu-20.04, windows-latest, macos-latest] + # steps: + # - uses: actions/checkout@v3 + # - name: Build wheels + # uses: pypa/cibuildwheel@v1.11.1.post1 + # with: + # output-dir: dist + # env: + # WRAPT_INSTALL_EXTENSIONS: true + # CIBW_BUILD: cp27* cp35* + # CIBW_SKIP: cp27-win* + # CIBW_BUILD_VERBOSITY: 1 + # - uses: actions/upload-artifact@v3 + # with: + # name: dist + # path: dist/*.whl bdist_wheel: name: Build wheels (3.6+) on ${{ matrix.os }} for ${{ matrix.arch }} @@ -231,7 +241,7 @@ jobs: - test_linux #- test_aarch64_linux - test_macos - - test_windows_py27 + # - test_windows_py27 - test_windows runs-on: ${{ matrix.os }} strategy: @@ -249,7 +259,7 @@ jobs: if: ${{ matrix.arch == 'aarch64' }} uses: docker/setup-qemu-action@v2 - name: Build wheels - uses: pypa/cibuildwheel@v2.11.4 + uses: pypa/cibuildwheel@v2.16.2 with: output-dir: dist env: @@ -268,7 +278,7 @@ jobs: needs: - test_linux - test_macos - - test_windows_py27 + # - test_windows_py27 - test_windows runs-on: ubuntu-20.04 steps: diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 58356671..f3aa1f5a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,23 +1,22 @@ +# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py -# Build documentation with MkDocs -#mkdocs: -# configuration: mkdocs.yml - -# Optionally build your docs in additional formats such as PDF -#formats: -# - pdf - -# Optionally set the version of Python and requirements required to build your docs -python: - version: 3.8 -# install: -# - requirements: docs/requirements.txt +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt diff --git a/docs/changes.rst b/docs/changes.rst index 2e5c6c34..ecd7f39c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,47 @@ Release Notes ============= +Version 1.16.0 +-------------- + +Note that version 1.16.0 drops support for Python 2.7 and 3.5. Python version +3.6 or later is required. + +**New Features** + +* The ``patch_function_wrapper()`` decorator now accepts an ``enabled`` + argument, which can be a literal boolean value, object that evaluates as + boolean, or a callable object which returns a boolean. In the case of a + callable, determination of whether the wrapper is invoked will be left until + the point of the call. In the other cases, the wrapper will not be applied if + the value evaluates false at the point of applying the wrapper. + +**Features Changed** + +* The import hook loader and finder objects are now implemented as transparent + object proxies so they properly proxy pass access to attributes/functions of + the wrapped loader or finder. + +* Code files in the implementation have been reorganized such that the pure + Python version of the ``ObjectProxy`` class is directly available even if the + C extension variant is being used. This is to allow the pure Python variant to + be used in exceptional cases where the C extension variant is not fully + compatible with the pure Python implementation and the behaviour of the pure + Python variant is what is required. This should only be relied upon if have + absolutely no choice. The pure Python variant is not as performant as the C + extension. + + To access the pure Python variant use ``from wrapt.wrappers import ObjectProxy`` + instead of just ``from wrapt import ObjectProxy``. Note that prior to this + version if you had used ``from wrapt.wrappers import ObjectProxy`` you would + have got the C extension variant of the class rather than the pure Python + version if the C extension variant was available. + +**Bugs Fixed** + +* It was not possible to update the ``__class__`` attribute through the + transparent object proxy when relying on the C implementation. + Version 1.15.0 -------------- diff --git a/setup.cfg b/setup.cfg index bb377761..9c282c93 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,22 +10,20 @@ description = Module for decorators, wrappers and monkey patching. long_description = file: README.rst long_description_content_type = text/x-rst license = BSD -license_file = LICENSE +license_files = LICENSE platform = any keywords = wrapper, proxy, decorator classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: BSD License - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy project_urls = @@ -35,7 +33,7 @@ project_urls = [options] zip_safe = false -python_requires = >= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*, != 3.4.* +python_requires = >= 3.6 packages = find: package_dir = =src @@ -72,14 +70,12 @@ norecursedirs = .tox venv [tox:tox] envlist = - py{27,35,36,37,38,39,310,311,312} - py{27,35,36,37,38,39,310,311,312}-{without,install,disable}-extensions, + py{36,37,38,39,310,311,312} + py{36,37,38,39,310,311,312}-{without,install,disable}-extensions, pypy-without-extensions [gh-actions] python = - 2.7: py27, py27-without-extensions, py27-install-extensions, py27-disable-extensions - 3.5: py35, py35-without-extensions, py35-install-extensions, py35-disable-extensions 3.6: py36, py36-without-extensions, py36-install-extensions, py36-disable-extensions 3.7: py37, py37-without-extensions, py37-install-extensions, py37-disable-extensions 3.8: py38, py38-without-extensions, py38-install-extensions, py38-disable-extensions @@ -87,9 +83,9 @@ python = 3.10: py310, py310-without-extensions, py310-install-extensions, py310-disable-extensions 3.11: py311, py311-without-extensions, py311-install-extensions, py311-disable-extensions 3.12: py312, py312-without-extensions, py312-install-extensions, py312-disable-extensions - pypy-2.7: pypy-without-extensions pypy-3.8: pypy-without-extensions pypy-3.9: pypy-without-extensions + pypy-3.10: pypy-without-extensions [testenv] deps = diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index c5363524..ed31a943 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,12 +1,15 @@ -__version_info__ = ('1', '15', '0') +__version_info__ = ('1', '16', '0') __version__ = '.'.join(__version_info__) -from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper, - BoundFunctionWrapper, WeakFunctionProxy, PartialCallableObjectProxy, - resolve_path, apply_patch, wrap_object, wrap_object_attribute, +from .__wrapt__ 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) diff --git a/src/wrapt/__wrapt__.py b/src/wrapt/__wrapt__.py new file mode 100644 index 00000000..9933b2c9 --- /dev/null +++ b/src/wrapt/__wrapt__.py @@ -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 diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index 7ff1085a..e0e1b5bc 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -1139,6 +1139,30 @@ static int WraptObjectProxy_setitem(WraptObjectProxyObject *self, /* ------------------------------------------------------------------------- */ +static PyObject *WraptObjectProxy_self_setattr( + WraptObjectProxyObject *self, PyObject *args) +{ + PyObject *name = NULL; + PyObject *value = NULL; + +#if PY_MAJOR_VERSION >= 3 + if (!PyArg_ParseTuple(args, "UO:__self_setattr__", &name, &value)) + return NULL; +#else + if (!PyArg_ParseTuple(args, "SO:__self_setattr__", &name, &value)) + return NULL; +#endif + + if (PyObject_GenericSetAttr((PyObject *)self, name, value) != 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +/* ------------------------------------------------------------------------- */ + static PyObject *WraptObjectProxy_dir( WraptObjectProxyObject *self, PyObject *args) { @@ -1464,6 +1488,19 @@ static PyObject *WraptObjectProxy_get_class( /* ------------------------------------------------------------------------- */ +static int WraptObjectProxy_set_class(WraptObjectProxyObject *self, + PyObject *value) +{ + if (!self->wrapped) { + PyErr_SetString(PyExc_ValueError, "wrapper has not been initialized"); + return -1; + } + + return PyObject_SetAttrString(self->wrapped, "__class__", value); +} + +/* ------------------------------------------------------------------------- */ + static PyObject *WraptObjectProxy_get_annotations( WraptObjectProxyObject *self) { @@ -1741,6 +1778,8 @@ static PyMappingMethods WraptObjectProxy_as_mapping = { }; static PyMethodDef WraptObjectProxy_methods[] = { + { "__self_setattr__", (PyCFunction)WraptObjectProxy_self_setattr, + METH_VARARGS , 0 }, { "__dir__", (PyCFunction)WraptObjectProxy_dir, METH_NOARGS, 0 }, { "__enter__", (PyCFunction)WraptObjectProxy_enter, METH_VARARGS | METH_KEYWORDS, 0 }, @@ -1779,7 +1818,7 @@ static PyGetSetDef WraptObjectProxy_getset[] = { { "__doc__", (getter)WraptObjectProxy_get_doc, (setter)WraptObjectProxy_set_doc, 0 }, { "__class__", (getter)WraptObjectProxy_get_class, - NULL, 0 }, + (setter)WraptObjectProxy_set_class, 0 }, { "__annotations__", (getter)WraptObjectProxy_get_annotations, (setter)WraptObjectProxy_set_annotations, 0 }, { "__wrapped__", (getter)WraptObjectProxy_get_wrapped, @@ -2550,7 +2589,6 @@ static PyObject *WraptFunctionWrapperBase_set_name( static PyObject *WraptFunctionWrapperBase_instancecheck( WraptFunctionWrapperObject *self, PyObject *instance) { - PyObject *object = NULL; PyObject *result = NULL; int check = 0; diff --git a/src/wrapt/decorators.py b/src/wrapt/decorators.py index c3f25472..c80a4bb7 100644 --- a/src/wrapt/decorators.py +++ b/src/wrapt/decorators.py @@ -41,7 +41,7 @@ def exec_(_code_, _globs_=None, _locs_=None): except ImportError: pass -from .wrappers import (FunctionWrapper, BoundFunctionWrapper, ObjectProxy, +from .__wrapt__ import (FunctionWrapper, BoundFunctionWrapper, ObjectProxy, CallableObjectProxy) # Adapter wrapper for the wrapped function which will overlay certain diff --git a/src/wrapt/importer.py b/src/wrapt/importer.py index 1e5e6886..23fcbd2f 100644 --- a/src/wrapt/importer.py +++ b/src/wrapt/importer.py @@ -15,6 +15,8 @@ string_types = str, from importlib.util import find_spec +from .__wrapt__ 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 # and the hooks fired, the list of hooks recorded against the target @@ -128,20 +130,20 @@ def load_module(self, fullname): return module -class _ImportHookChainedLoader: +class _ImportHookChainedLoader(ObjectProxy): def __init__(self, loader): - self.loader = loader + super(_ImportHookChainedLoader, self).__init__(loader) if hasattr(loader, "load_module"): - self.load_module = self._load_module + self.__self_setattr__('load_module', self._self_load_module) if hasattr(loader, "create_module"): - self.create_module = self._create_module + self.__self_setattr__('create_module', self._self_create_module) if hasattr(loader, "exec_module"): - self.exec_module = self._exec_module + self.__self_setattr__('exec_module', self._self_exec_module) - def _set_loader(self, module): - # Set module's loader to self.loader unless it's already set to + def _self_set_loader(self, module): + # Set module's loader to self.__wrapped__ unless it's already set to # something else. Import machinery will set it to spec.loader if it is # None, so handle None as well. The module may not support attribute # assignment, in which case we simply skip it. Note that we also deal @@ -156,17 +158,17 @@ class UNDEFINED: pass if getattr(module, "__loader__", UNDEFINED) in (None, self): try: - module.__loader__ = self.loader + module.__loader__ = self.__wrapped__ except AttributeError: pass if (getattr(module, "__spec__", None) is not None and getattr(module.__spec__, "loader", None) is self): - module.__spec__.loader = self.loader + module.__spec__.loader = self.__wrapped__ - def _load_module(self, fullname): - module = self.loader.load_module(fullname) - self._set_loader(module) + def _self_load_module(self, fullname): + module = self.__wrapped__.load_module(fullname) + self._self_set_loader(module) notify_module_loaded(module) return module @@ -174,12 +176,12 @@ def _load_module(self, fullname): # Python 3.4 introduced create_module() and exec_module() instead of # load_module() alone. Splitting the two steps. - def _create_module(self, spec): - return self.loader.create_module(spec) + def _self_create_module(self, spec): + return self.__wrapped__.create_module(spec) - def _exec_module(self, module): - self._set_loader(module) - self.loader.exec_module(module) + def _self_exec_module(self, module): + self._self_set_loader(module) + self.__wrapped__.exec_module(module) notify_module_loaded(module) class ImportHookFinder: diff --git a/src/wrapt/patches.py b/src/wrapt/patches.py new file mode 100644 index 00000000..e22adf7c --- /dev/null +++ b/src/wrapt/patches.py @@ -0,0 +1,141 @@ +import inspect +import sys + +PY2 = sys.version_info[0] == 2 + +if PY2: + string_types = basestring, +else: + string_types = str, + +from .__wrapt__ 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 diff --git a/src/wrapt/weakrefs.py b/src/wrapt/weakrefs.py new file mode 100644 index 00000000..f931b60d --- /dev/null +++ b/src/wrapt/weakrefs.py @@ -0,0 +1,98 @@ +import functools +import weakref + +from .__wrapt__ 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) diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index 48f334ee..dfc3440d 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -1,8 +1,5 @@ -import os import sys -import functools import operator -import weakref import inspect PY2 = sys.version_info[0] == 2 @@ -94,6 +91,9 @@ def __init__(self, wrapped): except AttributeError: pass + def __self_setattr__(self, name, value): + object.__setattr__(self, name, value) + @property def __name__(self): return self.__wrapped__.__name__ @@ -782,235 +782,3 @@ def __init__(self, wrapped, wrapper, enabled=None): super(FunctionWrapper, self).__init__(wrapped, None, wrapper, enabled, binding) - -try: - if not os.environ.get('WRAPT_DISABLE_EXTENSIONS'): - from ._wrappers import (ObjectProxy, CallableObjectProxy, - PartialCallableObjectProxy, FunctionWrapper, - BoundFunctionWrapper, _FunctionWrapperBase) -except ImportError: - pass - -# 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): - def _wrapper(wrapper): - return wrap_object(module, name, FunctionWrapper, (wrapper,)) - 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 - -# 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) diff --git a/tests/test_function_wrapper.py b/tests/test_function_wrapper.py index 3e95ba3b..c353b205 100644 --- a/tests/test_function_wrapper.py +++ b/tests/test_function_wrapper.py @@ -3,7 +3,6 @@ import unittest import wrapt -import wrapt.wrappers from compat import PY2, PY3, exec_ diff --git a/tests/test_monkey_patching.py b/tests/test_monkey_patching.py index 5e290f5d..d640f5c7 100644 --- a/tests/test_monkey_patching.py +++ b/tests/test_monkey_patching.py @@ -14,6 +14,15 @@ def global_function_2(*args, **kwargs): def global_function_3(*args, **kwargs): return args, kwargs +def global_function_3_enabled_literal_false(*args, **kwargs): + return args, kwargs + +def global_function_3_enabled_literal_true(*args, **kwargs): + return args, kwargs + +def global_function_3_enabled_callable(*args, **kwargs): + return args, kwargs + def global_function_4(*args, **kwargs): return args, kwargs @@ -275,6 +284,78 @@ def wrapper(wrapped, instance, args, kwargs): self.assertEqual(result, (_args, _kwargs)) self.assertEqual(called[0], (_args, _kwargs)) + def test_patch_function_module_name_enabled_literal_false(self): + + _args = (1, 2) + _kwargs = {'one': 1, 'two': 2} + + called = [] + + @wrapt.patch_function_wrapper(__name__, 'global_function_3_enabled_literal_false', enabled=False) + def wrapper(wrapped, instance, args, kwargs): + called.append((args, kwargs)) + self.assertEqual(instance, None) + self.assertEqual(args, _args) + self.assertEqual(kwargs, _kwargs) + return wrapped(*args, **kwargs) + + result = global_function_3_enabled_literal_false(*_args, **_kwargs) + + self.assertEqual(result, (_args, _kwargs)) + self.assertEqual(called, []) + + def test_patch_function_module_name_enabled_literal_true(self): + + _args = (1, 2) + _kwargs = {'one': 1, 'two': 2} + + called = [] + + @wrapt.patch_function_wrapper(__name__, 'global_function_3_enabled_literal_true', enabled=True) + def wrapper(wrapped, instance, args, kwargs): + called.append((args, kwargs)) + self.assertEqual(instance, None) + self.assertEqual(args, _args) + self.assertEqual(kwargs, _kwargs) + return wrapped(*args, **kwargs) + + result = global_function_3_enabled_literal_true(*_args, **_kwargs) + + self.assertEqual(result, (_args, _kwargs)) + self.assertEqual(called[0], (_args, _kwargs)) + + def test_patch_function_module_name_enabled_callable(self): + + _args = (1, 2) + _kwargs = {'one': 1, 'two': 2} + + called = [] + + enable = False + + def enabled(): + return enable + + @wrapt.patch_function_wrapper(__name__, 'global_function_3_enabled_callable', enabled=enabled) + def wrapper(wrapped, instance, args, kwargs): + called.append((args, kwargs)) + self.assertEqual(instance, None) + self.assertEqual(args, _args) + self.assertEqual(kwargs, _kwargs) + return wrapped(*args, **kwargs) + + result = global_function_3_enabled_callable(*_args, **_kwargs) + + enable = True + + self.assertEqual(result, (_args, _kwargs)) + self.assertEqual(called, []) + + result = global_function_3_enabled_callable(*_args, **_kwargs) + + self.assertEqual(result, (_args, _kwargs)) + self.assertEqual(called[0], (_args, _kwargs)) + def test_patch_function_module(self): _args = (1, 2) diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index 50522b27..91a010b3 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -1089,10 +1089,13 @@ def test_pow(self): self.assertEqual(pow(three, 2), pow(3, 2)) # Only PyPy implements __rpow__ for ternary pow(). + # Note: No longer test for this as pypy 3.9+ seems + # to have been updated to not support ternary pow() + # in same way. - if is_pypy: - self.assertEqual(pow(three, two, 2), pow(3, 2, 2)) - self.assertEqual(pow(3, two, 2), pow(3, 2, 2)) + # if is_pypy: + # self.assertEqual(pow(three, two, 2), pow(3, 2, 2)) + # self.assertEqual(pow(3, two, 2), pow(3, 2, 2)) self.assertEqual(pow(three, 2, 2), pow(3, 2, 2)) @@ -1849,7 +1852,7 @@ def test_self_keyword_argument_on_dict(self): # A dict when given self as keyword argument uses it to create item in # the dict and no attempt is made to use a positional argument. - d = wrapt.wrappers.CallableObjectProxy(dict)(self='self') + d = wrapt.CallableObjectProxy(dict)(self='self') self.assertEqual(d, dict(self='self')) @@ -1864,7 +1867,7 @@ def __init__(self, *args, **kwargs): self.assertEqual(o._args, ('arg1',)) self.assertEqual(o._kwargs, {}) - o = wrapt.wrappers.CallableObjectProxy(Object)('arg1') + o = wrapt.CallableObjectProxy(Object)('arg1') self.assertEqual(o._args, ('arg1',)) self.assertEqual(o._kwargs, {}) @@ -1881,7 +1884,7 @@ def __init__(self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) with self.assertRaises(TypeError) as e: - wrapt.wrappers.CallableObjectProxy(Object)(self='self') + wrapt.CallableObjectProxy(Object)(self='self') self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) @@ -1897,7 +1900,7 @@ def __init__(self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) with self.assertRaises(TypeError) as e: - wrapt.wrappers.CallableObjectProxy(Object)(arg1='arg1', self='self') + wrapt.CallableObjectProxy(Object)(arg1='arg1', self='self') self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) @@ -1912,7 +1915,7 @@ def __init__(_self, *args, **kwargs): self.assertEqual(o._args, ()) self.assertEqual(o._kwargs, dict(self="self")) - o = wrapt.wrappers.CallableObjectProxy(Object)(self='self') + o = wrapt.CallableObjectProxy(Object)(self='self') self.assertEqual(o._args, ()) self.assertEqual(o._kwargs, dict(self="self")) @@ -1930,7 +1933,7 @@ def __init__(_self, self, *args, **kwargs): self.assertEqual(o._kwargs, {}) self.assertEqual(o._self, 'self') - o = wrapt.wrappers.CallableObjectProxy(Object)(self='self') + o = wrapt.CallableObjectProxy(Object)(self='self') self.assertEqual(o._args, ()) self.assertEqual(o._kwargs, {}) @@ -1949,7 +1952,7 @@ def __init__(_self, self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument '_self'.*", str(e.exception)), None) with self.assertRaises(TypeError) as e: - wrapt.wrappers.CallableObjectProxy(Object)(_self='self') + wrapt.CallableObjectProxy(Object)(_self='self') self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument '_self'.*", str(e.exception)), None) @@ -1959,7 +1962,7 @@ def test_self_keyword_argument_on_dict_1(self): # A dict when given self as keyword argument uses it to create item in # the dict and no attempt is made to use a positional argument. - wrapper = wrapt.wrappers.PartialCallableObjectProxy(dict, arg1='arg1') + wrapper = wrapt.PartialCallableObjectProxy(dict, arg1='arg1') d = wrapper(self='self') @@ -1969,7 +1972,7 @@ def test_self_keyword_argument_on_dict_2(self): # A dict when given self as keyword argument uses it to create item in # the dict and no attempt is made to use a positional argument. - wrapper = wrapt.wrappers.PartialCallableObjectProxy(dict, self='self') + wrapper = wrapt.PartialCallableObjectProxy(dict, self='self') d = wrapper(arg1='arg1') @@ -1986,7 +1989,7 @@ def __init__(self, *args, **kwargs): self.assertEqual(o._args, ('arg1',)) self.assertEqual(o._kwargs, {}) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object, 'arg1') + wrapper = wrapt.PartialCallableObjectProxy(Object, 'arg1') o = wrapper() @@ -2004,7 +2007,7 @@ def __init__(self, *args, **kwargs): self.assertEqual(o._args, ('arg1',)) self.assertEqual(o._kwargs, {}) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object) + wrapper = wrapt.PartialCallableObjectProxy(Object) o = wrapper('arg1') @@ -2022,7 +2025,7 @@ def __init__(self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object, self='self') + wrapper = wrapt.PartialCallableObjectProxy(Object, self='self') with self.assertRaises(TypeError) as e: o = wrapper() @@ -2040,7 +2043,7 @@ def __init__(self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object) + wrapper = wrapt.PartialCallableObjectProxy(Object) with self.assertRaises(TypeError) as e: o = wrapper(self='self') @@ -2058,7 +2061,7 @@ def __init__(self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object, arg1='arg1', self='self') + wrapper = wrapt.PartialCallableObjectProxy(Object, arg1='arg1', self='self') with self.assertRaises(TypeError) as e: o = wrapper() @@ -2076,7 +2079,7 @@ def __init__(self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object) + wrapper = wrapt.PartialCallableObjectProxy(Object) with self.assertRaises(TypeError) as e: o = wrapper(arg1='arg1', self='self') @@ -2094,7 +2097,7 @@ def __init__(_self, *args, **kwargs): self.assertEqual(o._args, ()) self.assertEqual(o._kwargs, dict(self="self")) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object, self='self') + wrapper = wrapt.PartialCallableObjectProxy(Object, self='self') o = wrapper() @@ -2112,7 +2115,7 @@ def __init__(_self, *args, **kwargs): self.assertEqual(o._args, ()) self.assertEqual(o._kwargs, dict(self="self")) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object) + wrapper = wrapt.PartialCallableObjectProxy(Object) o = wrapper(self='self') @@ -2132,7 +2135,7 @@ def __init__(_self, self, *args, **kwargs): self.assertEqual(o._kwargs, {}) self.assertEqual(o._self, 'self') - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object, self='self') + wrapper = wrapt.PartialCallableObjectProxy(Object, self='self') o = wrapper() @@ -2153,7 +2156,7 @@ def __init__(_self, self, *args, **kwargs): self.assertEqual(o._kwargs, {}) self.assertEqual(o._self, 'self') - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object) + wrapper = wrapt.PartialCallableObjectProxy(Object) o = wrapper(self='self') @@ -2173,7 +2176,7 @@ def __init__(_self, self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument '_self'.*", str(e.exception)), None) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object, _self='self') + wrapper = wrapt.PartialCallableObjectProxy(Object, _self='self') with self.assertRaises(TypeError) as e: o = wrapper() @@ -2192,7 +2195,7 @@ def __init__(_self, self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument '_self'.*", str(e.exception)), None) - wrapper = wrapt.wrappers.PartialCallableObjectProxy(Object) + wrapper = wrapt.PartialCallableObjectProxy(Object) with self.assertRaises(TypeError) as e: o = wrapper(_self='self') @@ -2208,7 +2211,7 @@ def test_self_keyword_argument_on_dict(self): def wrapper(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs) - d = wrapt.wrappers.FunctionWrapper(dict, wrapper)(self='self') + d = wrapt.FunctionWrapper(dict, wrapper)(self='self') self.assertEqual(d, dict(self='self')) @@ -2226,7 +2229,7 @@ def __init__(self, *args, **kwargs): self.assertEqual(o._args, ('arg1',)) self.assertEqual(o._kwargs, {}) - o = wrapt.wrappers.FunctionWrapper(Object, wrapper)('arg1') + o = wrapt.FunctionWrapper(Object, wrapper)('arg1') self.assertEqual(o._args, ('arg1',)) self.assertEqual(o._kwargs, {}) @@ -2246,7 +2249,7 @@ def __init__(self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) with self.assertRaises(TypeError) as e: - wrapt.wrappers.FunctionWrapper(Object, wrapper)(self='self') + wrapt.FunctionWrapper(Object, wrapper)(self='self') self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) @@ -2265,7 +2268,7 @@ def __init__(self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) with self.assertRaises(TypeError) as e: - wrapt.wrappers.FunctionWrapper(Object, wrapper)(arg1='arg1', self='self') + wrapt.FunctionWrapper(Object, wrapper)(arg1='arg1', self='self') self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument 'self'.*", str(e.exception)), None) @@ -2283,7 +2286,7 @@ def __init__(_self, *args, **kwargs): self.assertEqual(o._args, ()) self.assertEqual(o._kwargs, dict(self="self")) - o = wrapt.wrappers.FunctionWrapper(Object, wrapper)(self='self') + o = wrapt.FunctionWrapper(Object, wrapper)(self='self') self.assertEqual(o._args, ()) self.assertEqual(o._kwargs, dict(self="self")) @@ -2304,7 +2307,7 @@ def __init__(_self, self, *args, **kwargs): self.assertEqual(o._kwargs, {}) self.assertEqual(o._self, 'self') - o = wrapt.wrappers.FunctionWrapper(Object, wrapper)(self='self') + o = wrapt.FunctionWrapper(Object, wrapper)(self='self') self.assertEqual(o._args, ()) self.assertEqual(o._kwargs, {}) @@ -2326,7 +2329,7 @@ def __init__(_self, self, *args, **kwargs): self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument '_self'.*", str(e.exception)), None) with self.assertRaises(TypeError) as e: - wrapt.wrappers.FunctionWrapper(Object, wrapper)(_self='self') + wrapt.FunctionWrapper(Object, wrapper)(_self='self') self.assertNotEqual(re.match(".*got multiple values for (keyword )?argument '_self'.*", str(e.exception)), None) @@ -2341,7 +2344,7 @@ class Object: def function(cls, self, *args, **kwargs): return self, args, kwargs - function = wrapt.wrappers.FunctionWrapper(function, wrapper) + function = wrapt.FunctionWrapper(function, wrapper) result = Object().function(self='self') @@ -2355,7 +2358,7 @@ class Object: def function(_self, self, *args, **kwargs): return self, args, kwargs - function = wrapt.wrappers.FunctionWrapper(function, wrapper) + function = wrapt.FunctionWrapper(function, wrapper) result = Object().function(self='self') @@ -2417,5 +2420,23 @@ def function(_self, self, *args, **kwargs): self.assertEqual(result, ('self', (), dict(arg1='arg1'))) +class TestOverridingSpecialAttributes(unittest.TestCase): + + def test_overriding_class_attribute(self): + class Object1: pass + class Object2(Object1): pass + + o1 = Object1() + + self.assertEqual(o1.__class__, type(o1)) + + o2 = Object2() + + self.assertEqual(o2.__class__, type(o2)) + + o2.__class__ = type(o1) + + self.assertEqual(o2.__class__, type(o1)) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_weak_function_proxy.py b/tests/test_weak_function_proxy.py index fa4b516b..2babe95c 100644 --- a/tests/test_weak_function_proxy.py +++ b/tests/test_weak_function_proxy.py @@ -196,7 +196,7 @@ def test_self_keyword_argument(self): def function(self, *args, **kwargs): return self, args, kwargs - proxy = wrapt.wrappers.WeakFunctionProxy(function) + proxy = wrapt.WeakFunctionProxy(function) self.assertEqual(proxy(self='self', arg1='arg1'), ('self', (), dict(arg1='arg1')))