From f562a675ff49fc7fcb92e4e6a5d88ac5ee9818b2 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Wed, 21 Jun 2023 11:16:59 +1000 Subject: [PATCH 01/27] Drop testing of Python 2.7 and 3.5 as no longer supported by GitHub actions. --- .github/workflows/main.yml | 116 ++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b4a29d8a..b34ac764 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,8 @@ jobs: os: - ubuntu-20.04 python-version: - - 2.7 - - 3.5 + # - 2.7 + # - 3.5 - 3.6 - 3.7 - 3.8 @@ -21,7 +21,7 @@ jobs: - "3.10" - 3.11 - 3.12-dev - - pypy-2.7 + # - pypy-2.7 - pypy-3.8 - pypy-3.9 steps: @@ -85,15 +85,15 @@ 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 + # - pypy-2.7 - pypy-3.8 - pypy-3.9 steps: @@ -115,28 +115,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,14 +146,14 @@ 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 + # - pypy-2.7 - pypy-3.8 - pypy-3.9 steps: @@ -198,32 +198,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 +231,7 @@ jobs: - test_linux #- test_aarch64_linux - test_macos - - test_windows_py27 + # - test_windows_py27 - test_windows runs-on: ${{ matrix.os }} strategy: @@ -268,7 +268,7 @@ jobs: needs: - test_linux - test_macos - - test_windows_py27 + # - test_windows_py27 - test_windows runs-on: ubuntu-20.04 steps: From fe909c26f29f954de7648f8bb7fbd1916ee34cc6 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Fri, 23 Jun 2023 12:11:43 +1000 Subject: [PATCH 02/27] It was not possible to update __class__ attribute via the ObjectProxy when using C implementation. --- src/wrapt/_wrappers.c | 15 ++++++++++++++- tests/test_object_proxy.py | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index 7ff1085a..cf355311 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -1464,6 +1464,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) { @@ -1779,7 +1792,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, diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index 50522b27..352c66b4 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -2417,5 +2417,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() From 59680c8bb998defa3be522fef6e49fd276bebe58 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Fri, 23 Jun 2023 12:27:58 +1000 Subject: [PATCH 03/27] Skip pypy tests for ternary pow(). --- tests/test_object_proxy.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index 352c66b4..eec86821 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)) From 34b4b599a02e322eefc964927ba34cf941fbf0f2 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Fri, 23 Jun 2023 12:29:06 +1000 Subject: [PATCH 04/27] Test with pypy-3.10. --- .github/workflows/main.yml | 3 +++ setup.cfg | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b34ac764..49dba940 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,6 +24,7 @@ jobs: # - pypy-2.7 - pypy-3.8 - pypy-3.9 + - pypy-3.10 steps: - name: Checkout code uses: actions/checkout@v3 @@ -96,6 +97,7 @@ jobs: # - pypy-2.7 - pypy-3.8 - pypy-3.9 + - pypy-3.10 steps: - name: Checkout code uses: actions/checkout@v3 @@ -156,6 +158,7 @@ jobs: # - pypy-2.7 - pypy-3.8 - pypy-3.9 + - pypy-3.10 steps: - name: Checkout code uses: actions/checkout@v3 diff --git a/setup.cfg b/setup.cfg index bb377761..1ce4f644 100644 --- a/setup.cfg +++ b/setup.cfg @@ -90,6 +90,7 @@ python = 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 = From b0a3218d3b08170f6094d8d1e9dee520861c6cb5 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Mon, 26 Jun 2023 16:30:54 +1000 Subject: [PATCH 05/27] Update ReadTheDocs configuration file. --- .readthedocs.yaml | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) 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 From fc6bf15c3349d80d3c1495ddcea028a900b82375 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 6 Jul 2023 17:12:01 +1000 Subject: [PATCH 06/27] Make import hook loader an object proxy. --- src/wrapt/_wrappers.c | 27 ++++++++++++++++++++++++++- src/wrapt/importer.py | 36 +++++++++++++++++++----------------- src/wrapt/wrappers.py | 3 +++ 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index cf355311..fe1a7cc5 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_gsetattr__", &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) { @@ -1754,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 }, @@ -2563,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/importer.py b/src/wrapt/importer.py index 1e5e6886..10ab9390 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 .wrappers import ObjectProxy, _ObjectProxyMetaType + # 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/wrappers.py b/src/wrapt/wrappers.py index 48f334ee..b011f699 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -94,6 +94,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__ From bc2decfbd9e9273d17c56ca49f273e794e612a07 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 6 Jul 2023 17:28:05 +1000 Subject: [PATCH 07/27] Fix typos in method name string use in errors. --- src/wrapt/_wrappers.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index fe1a7cc5..e0e1b5bc 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -1146,10 +1146,10 @@ static PyObject *WraptObjectProxy_self_setattr( PyObject *value = NULL; #if PY_MAJOR_VERSION >= 3 - if (!PyArg_ParseTuple(args, "UO:__self_gsetattr__", &name, &value)) + if (!PyArg_ParseTuple(args, "UO:__self_setattr__", &name, &value)) return NULL; #else - if (!PyArg_ParseTuple(args, "SO:___self_setattr__", &name, &value)) + if (!PyArg_ParseTuple(args, "SO:__self_setattr__", &name, &value)) return NULL; #endif From f2ad8e2c933d2e0908170587525b1577b5375c30 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 6 Jul 2023 17:30:49 +1000 Subject: [PATCH 08/27] Remove redundant object import. --- src/wrapt/importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wrapt/importer.py b/src/wrapt/importer.py index 10ab9390..8d1f1396 100644 --- a/src/wrapt/importer.py +++ b/src/wrapt/importer.py @@ -15,7 +15,7 @@ string_types = str, from importlib.util import find_spec -from .wrappers import ObjectProxy, _ObjectProxyMetaType +from .wrappers 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 From 72ceaec101bc22c3f9289f10d0e642c88dcb88fa Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 25 Jul 2023 14:23:32 +1000 Subject: [PATCH 09/27] Setup release for dropping Python 2.7/3.5 support as GitHub not longer supports them for testing. --- setup.cfg | 12 +++--------- src/wrapt/__init__.py | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1ce4f644..04d5f623 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,10 +16,7 @@ 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 @@ -35,7 +32,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 +69,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,7 +82,6 @@ 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 diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index c5363524..df960824 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('1', '15', '0') +__version_info__ = ('1', '16', '0dev1') __version__ = '.'.join(__version_info__) from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper, From 3112bbdf154dd782c23f5ab7f85a4e66932aecb9 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 25 Jul 2023 16:27:32 +1000 Subject: [PATCH 10/27] Add note that 1.16.0 drops Python 2.7/3.5 support. --- docs/changes.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 2e5c6c34..2a143a9e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,12 @@ 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. + Version 1.15.0 -------------- From 54fc35d0b1303ab39b1347169535e79e8a075374 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 25 Jul 2023 16:35:58 +1000 Subject: [PATCH 11/27] Add enable argument to wrap and patch functions. --- docs/changes.rst | 9 ++ src/wrapt/wrappers.py | 16 ++-- tests/test_monkey_patching.py | 171 ++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 8 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 2a143a9e..eb3a5011 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -7,6 +7,15 @@ 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 ``wrap_function_wrapper()`` and ``patch_function_wrapper()`` functions now + accept 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. + Version 1.15.0 -------------- diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index b011f699..1dd85a60 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -894,15 +894,15 @@ def _wrapper(wrapped, instance, args, kwargs): 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 wrap_function_wrapper(module, name, wrapper, enabled=None): + return wrap_object(module, name, FunctionWrapper, (wrapper, enabled)) -def patch_function_wrapper(module, name): +def patch_function_wrapper(module, name, enabled=None): def _wrapper(wrapper): - return wrap_object(module, name, FunctionWrapper, (wrapper,)) + return wrap_object(module, name, FunctionWrapper, (wrapper, enabled)) return _wrapper -def transient_function_wrapper(module, name): +def transient_function_wrapper(module, name, enabled=None): def _decorator(wrapper): def _wrapper(wrapped, instance, args, kwargs): target_wrapped = args[0] @@ -914,14 +914,14 @@ def _wrapper(wrapped, instance, args, kwargs): 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) + replacement = FunctionWrapper(original, target_wrapper, enabled) setattr(parent, attribute, replacement) try: return wrapped(*args, **kwargs) finally: setattr(parent, attribute, original) - return FunctionWrapper(target_wrapped, _execute) - return FunctionWrapper(wrapper, _wrapper) + return FunctionWrapper(target_wrapped, _execute, enabled) + return FunctionWrapper(wrapper, _wrapper, enabled) return _decorator # A weak function proxy. This will work on instance methods, class diff --git a/tests/test_monkey_patching.py b/tests/test_monkey_patching.py index 5e290f5d..8abd4eab 100644 --- a/tests/test_monkey_patching.py +++ b/tests/test_monkey_patching.py @@ -11,9 +11,27 @@ def global_function_1(*args, **kwargs): def global_function_2(*args, **kwargs): return args, kwargs +def global_function_2_enabled_literal_false(*args, **kwargs): + return args, kwargs + +def global_function_2_enabled_literal_true(*args, **kwargs): + return args, kwargs + +def global_function_2_enabled_callable(*args, **kwargs): + return 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 @@ -162,6 +180,87 @@ def wrapper(wrapped, instance, args, kwargs): self.assertEqual(result, (_args, _kwargs)) self.assertEqual(called[0], (_args, _kwargs)) + def test_wrap_function_module_enabled_literal_false(self): + + _args = (1, 2) + _kwargs = {'one': 1, 'two': 2} + + called = [] + + 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) + + module = sys.modules[__name__] + + wrapt.wrap_function_wrapper(module, 'global_function_2_enabled_literal_false', wrapper, enabled=False) + + result = global_function_2_enabled_literal_false(*_args, **_kwargs) + + self.assertEqual(result, (_args, _kwargs)) + self.assertEqual(called, []) + + def test_wrap_function_module_enabled_literal_true(self): + + _args = (1, 2) + _kwargs = {'one': 1, 'two': 2} + + called = [] + + 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) + + module = sys.modules[__name__] + + wrapt.wrap_function_wrapper(module, 'global_function_2_enabled_literal_true', wrapper, enabled=True) + + result = global_function_2_enabled_literal_true(*_args, **_kwargs) + + self.assertEqual(result, (_args, _kwargs)) + self.assertEqual(called[0], (_args, _kwargs)) + + def test_wrap_function_module_enabled_callable(self): + + _args = (1, 2) + _kwargs = {'one': 1, 'two': 2} + + called = [] + + 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) + + module = sys.modules[__name__] + + enable = False + + def enabled(): + return enable + + wrapt.wrap_function_wrapper(module, 'global_function_2_enabled_callable', wrapper, enabled=enabled) + + result = global_function_2_enabled_callable(*_args, **_kwargs) + + self.assertEqual(result, (_args, _kwargs)) + self.assertEqual(called, []) + + enable = True + + result = global_function_2_enabled_callable(*_args, **_kwargs) + + self.assertEqual(result, (_args, _kwargs)) + self.assertEqual(called[0], (_args, _kwargs)) + def test_wrap_instance_method_module_name(self): _args = (1, 2) @@ -275,6 +374,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) From 922d5b29350acfba46b81a8eb42a0ea1236dd6f5 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 25 Jul 2023 16:42:21 +1000 Subject: [PATCH 12/27] Add change notes for changed features and bugs. --- docs/changes.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index eb3a5011..9cf2e360 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -16,6 +16,17 @@ Note that version 1.16.0 drops support for Python 2.7 and 3.5. Python version 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. + +**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 -------------- From 26b1a957557675127882d89c02cb85ef51edec64 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 25 Jul 2023 16:45:52 +1000 Subject: [PATCH 13/27] Revert enabled argument for transient function wrapper as not really useful for that use case. --- src/wrapt/wrappers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index 1dd85a60..632c1c8b 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -902,7 +902,7 @@ def _wrapper(wrapper): return wrap_object(module, name, FunctionWrapper, (wrapper, enabled)) return _wrapper -def transient_function_wrapper(module, name, enabled=None): +def transient_function_wrapper(module, name): def _decorator(wrapper): def _wrapper(wrapped, instance, args, kwargs): target_wrapped = args[0] @@ -914,14 +914,14 @@ def _wrapper(wrapped, instance, args, kwargs): 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, enabled) + replacement = FunctionWrapper(original, target_wrapper) setattr(parent, attribute, replacement) try: return wrapped(*args, **kwargs) finally: setattr(parent, attribute, original) - return FunctionWrapper(target_wrapped, _execute, enabled) - return FunctionWrapper(wrapper, _wrapper, enabled) + return FunctionWrapper(target_wrapped, _execute) + return FunctionWrapper(wrapper, _wrapper) return _decorator # A weak function proxy. This will work on instance methods, class From 8c60238786179a2edd8aca3e2e5580524d1e4aab Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 25 Jul 2023 16:51:09 +1000 Subject: [PATCH 14/27] Revert addition of enabled to wrap_function_wrapper() as usage doesn't require it. --- docs/changes.rst | 12 ++--- src/wrapt/wrappers.py | 4 +- tests/test_monkey_patching.py | 90 ----------------------------------- 3 files changed, 8 insertions(+), 98 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 9cf2e360..dc78371f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -9,12 +9,12 @@ Note that version 1.16.0 drops support for Python 2.7 and 3.5. Python version **New Features** -* The ``wrap_function_wrapper()`` and ``patch_function_wrapper()`` functions now - accept 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. +* 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** diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index 632c1c8b..93184cbc 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -894,8 +894,8 @@ def _wrapper(wrapped, instance, args, kwargs): return FunctionWrapper(target_wrapped, target_wrapper) return FunctionWrapper(wrapper, _wrapper) -def wrap_function_wrapper(module, name, wrapper, enabled=None): - return wrap_object(module, name, FunctionWrapper, (wrapper, enabled)) +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): diff --git a/tests/test_monkey_patching.py b/tests/test_monkey_patching.py index 8abd4eab..d640f5c7 100644 --- a/tests/test_monkey_patching.py +++ b/tests/test_monkey_patching.py @@ -11,15 +11,6 @@ def global_function_1(*args, **kwargs): def global_function_2(*args, **kwargs): return args, kwargs -def global_function_2_enabled_literal_false(*args, **kwargs): - return args, kwargs - -def global_function_2_enabled_literal_true(*args, **kwargs): - return args, kwargs - -def global_function_2_enabled_callable(*args, **kwargs): - return args, kwargs - def global_function_3(*args, **kwargs): return args, kwargs @@ -180,87 +171,6 @@ def wrapper(wrapped, instance, args, kwargs): self.assertEqual(result, (_args, _kwargs)) self.assertEqual(called[0], (_args, _kwargs)) - def test_wrap_function_module_enabled_literal_false(self): - - _args = (1, 2) - _kwargs = {'one': 1, 'two': 2} - - called = [] - - 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) - - module = sys.modules[__name__] - - wrapt.wrap_function_wrapper(module, 'global_function_2_enabled_literal_false', wrapper, enabled=False) - - result = global_function_2_enabled_literal_false(*_args, **_kwargs) - - self.assertEqual(result, (_args, _kwargs)) - self.assertEqual(called, []) - - def test_wrap_function_module_enabled_literal_true(self): - - _args = (1, 2) - _kwargs = {'one': 1, 'two': 2} - - called = [] - - 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) - - module = sys.modules[__name__] - - wrapt.wrap_function_wrapper(module, 'global_function_2_enabled_literal_true', wrapper, enabled=True) - - result = global_function_2_enabled_literal_true(*_args, **_kwargs) - - self.assertEqual(result, (_args, _kwargs)) - self.assertEqual(called[0], (_args, _kwargs)) - - def test_wrap_function_module_enabled_callable(self): - - _args = (1, 2) - _kwargs = {'one': 1, 'two': 2} - - called = [] - - 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) - - module = sys.modules[__name__] - - enable = False - - def enabled(): - return enable - - wrapt.wrap_function_wrapper(module, 'global_function_2_enabled_callable', wrapper, enabled=enabled) - - result = global_function_2_enabled_callable(*_args, **_kwargs) - - self.assertEqual(result, (_args, _kwargs)) - self.assertEqual(called, []) - - enable = True - - result = global_function_2_enabled_callable(*_args, **_kwargs) - - self.assertEqual(result, (_args, _kwargs)) - self.assertEqual(called[0], (_args, _kwargs)) - def test_wrap_instance_method_module_name(self): _args = (1, 2) From 46c4b1b4108c53812c93e75503c8d9bd6c5b3f9e Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 7 Oct 2023 15:54:03 +1100 Subject: [PATCH 15/27] Use Python 3.12 final if it exists and create wheels for it. --- .github/workflows/main.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 49dba940..a6d4221c 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: @@ -20,7 +25,7 @@ jobs: - 3.9 - "3.10" - 3.11 - - 3.12-dev + - 3.12 # - pypy-2.7 - pypy-3.8 - pypy-3.9 @@ -94,6 +99,7 @@ jobs: - 3.9 - "3.10" - 3.11 + - 3.12 # - pypy-2.7 - pypy-3.8 - pypy-3.9 @@ -155,6 +161,7 @@ jobs: - 3.9 - "3.10" - 3.11 + - 3.12 # - pypy-2.7 - pypy-3.8 - pypy-3.9 From 59da500f4dbdcb1ddbccd96fcf7f372d2320f1ed Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 7 Oct 2023 16:56:00 +1100 Subject: [PATCH 16/27] Update version tag to be rc1. --- src/wrapt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index df960824..58db0759 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('1', '16', '0dev1') +__version_info__ = ('1', '16', '0rc1') __version__ = '.'.join(__version_info__) from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper, From 0c9ad919240f7c49cf5ff39e1f690cc9b35aa671 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 7 Oct 2023 17:21:51 +1100 Subject: [PATCH 17/27] Update GitHub action for building wheels. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6d4221c..2aa76d97 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -259,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: From bc3529c95ba9ade3876a25ca2c3280b799531eb2 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 7 Oct 2023 18:02:37 +1100 Subject: [PATCH 18/27] Add classification for Python 3.12. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 04d5f623..7cd75d26 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ classifiers = 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 = From 70c86a5e1fbf3eec185107551df17f1d4be1187e Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 5 Nov 2023 09:06:49 +1100 Subject: [PATCH 19/27] Increment version for release candidate. --- src/wrapt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index 58db0759..f2d93254 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('1', '16', '0rc1') +__version_info__ = ('1', '16', '0rc2') __version__ = '.'.join(__version_info__) from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper, From f7e9caba4ae245a039c0c49570b5a874b6b34a35 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 5 Nov 2023 09:07:25 +1100 Subject: [PATCH 20/27] Avoid use of deprecated setup file keyword. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 7cd75d26..9c282c93 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ 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 = From 8567e8ba723cf0c89dda77a2e615d46f8f5a9ee6 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 5 Nov 2023 10:53:35 +1100 Subject: [PATCH 21/27] Fix object references in test harnesses. --- tests/test_function_wrapper.py | 1 - tests/test_object_proxy.py | 60 +++++++++++++++---------------- tests/test_weak_function_proxy.py | 2 +- 3 files changed, 31 insertions(+), 32 deletions(-) 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_object_proxy.py b/tests/test_object_proxy.py index eec86821..91a010b3 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -1852,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')) @@ -1867,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, {}) @@ -1884,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) @@ -1900,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) @@ -1915,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")) @@ -1933,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, {}) @@ -1952,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) @@ -1962,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') @@ -1972,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') @@ -1989,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() @@ -2007,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') @@ -2025,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() @@ -2043,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') @@ -2061,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() @@ -2079,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') @@ -2097,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() @@ -2115,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') @@ -2135,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() @@ -2156,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') @@ -2176,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() @@ -2195,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') @@ -2211,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')) @@ -2229,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, {}) @@ -2249,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) @@ -2268,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) @@ -2286,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")) @@ -2307,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, {}) @@ -2329,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) @@ -2344,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') @@ -2358,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') 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'))) From f7a28f43b3dc6ade1084b529142c4aa9e7c79232 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 5 Nov 2023 11:40:56 +1100 Subject: [PATCH 22/27] Refactor code so pure Python version usable even when extension compiled. --- src/wrapt/__init__.py | 9 +- src/wrapt/__wrapt__.py | 14 +++ src/wrapt/decorators.py | 2 +- src/wrapt/importer.py | 2 +- src/wrapt/patches.py | 141 ++++++++++++++++++++++++ src/wrapt/weakrefs.py | 98 +++++++++++++++++ src/wrapt/wrappers.py | 232 ---------------------------------------- 7 files changed, 261 insertions(+), 237 deletions(-) create mode 100644 src/wrapt/__wrapt__.py create mode 100644 src/wrapt/patches.py create mode 100644 src/wrapt/weakrefs.py diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index f2d93254..f9353305 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -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) 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/decorators.py b/src/wrapt/decorators.py index c3f25472..d6f6cca6 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 .variants 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 8d1f1396..690f44a0 100644 --- a/src/wrapt/importer.py +++ b/src/wrapt/importer.py @@ -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 diff --git a/src/wrapt/patches.py b/src/wrapt/patches.py new file mode 100644 index 00000000..9dc912ce --- /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 .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 diff --git a/src/wrapt/weakrefs.py b/src/wrapt/weakrefs.py new file mode 100644 index 00000000..10beb0c4 --- /dev/null +++ b/src/wrapt/weakrefs.py @@ -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) diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index 93184cbc..0916bdbe 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -785,235 +785,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, 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 - -# 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) From 77a41f5de2db5fb60c6b5456097602c9d1099eb0 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 5 Nov 2023 11:51:03 +1100 Subject: [PATCH 23/27] Update change notes for making pure Python ObjectProxy available. --- docs/changes.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index dc78371f..7f3a9703 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -22,6 +22,21 @@ Note that version 1.16.0 drops support for Python 2.7 and 3.5. Python version 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 From 15765b6b1b5dbb1c902c9926d53b3f7a7d86c8e1 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 5 Nov 2023 11:52:44 +1100 Subject: [PATCH 24/27] Fix change notes formatting. --- docs/changes.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 7f3a9703..ecd7f39c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -31,11 +31,11 @@ Note that version 1.16.0 drops support for Python 2.7 and 3.5. Python version 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. + 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** From 081871e4b80c29e56059e2852eb532cbaf03a7f7 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 5 Nov 2023 11:55:09 +1100 Subject: [PATCH 25/27] Files weren't saved after code refactoring. --- src/wrapt/__init__.py | 2 +- src/wrapt/decorators.py | 2 +- src/wrapt/importer.py | 2 +- src/wrapt/patches.py | 2 +- src/wrapt/weakrefs.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index f9353305..d27ca924 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,7 +1,7 @@ __version_info__ = ('1', '16', '0rc2') __version__ = '.'.join(__version_info__) -from .variants import (ObjectProxy, CallableObjectProxy, FunctionWrapper, +from .__wrapt__ import (ObjectProxy, CallableObjectProxy, FunctionWrapper, BoundFunctionWrapper, PartialCallableObjectProxy) from .patches import (resolve_path, apply_patch, wrap_object, wrap_object_attribute, diff --git a/src/wrapt/decorators.py b/src/wrapt/decorators.py index d6f6cca6..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 .variants 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 690f44a0..23fcbd2f 100644 --- a/src/wrapt/importer.py +++ b/src/wrapt/importer.py @@ -15,7 +15,7 @@ string_types = str, from importlib.util import find_spec -from .variants import ObjectProxy +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 diff --git a/src/wrapt/patches.py b/src/wrapt/patches.py index 9dc912ce..e22adf7c 100644 --- a/src/wrapt/patches.py +++ b/src/wrapt/patches.py @@ -8,7 +8,7 @@ else: string_types = str, -from .variants import FunctionWrapper +from .__wrapt__ import FunctionWrapper # Helper functions for applying wrappers to existing functions. diff --git a/src/wrapt/weakrefs.py b/src/wrapt/weakrefs.py index 10beb0c4..f931b60d 100644 --- a/src/wrapt/weakrefs.py +++ b/src/wrapt/weakrefs.py @@ -1,7 +1,7 @@ import functools import weakref -from .variants import ObjectProxy, _FunctionWrapperBase +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 From 8d0836d7c4f08f442d38d5af6984fac98f132170 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 5 Nov 2023 12:14:06 +1100 Subject: [PATCH 26/27] Remove obsolete imports. --- src/wrapt/wrappers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index 0916bdbe..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 From 075216a141de6de9b0608e3fbb47e697d7f08825 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 9 Nov 2023 10:17:53 +1100 Subject: [PATCH 27/27] Update version to 1.16.0 ready for release. --- src/wrapt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index d27ca924..ed31a943 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('1', '16', '0rc2') +__version_info__ = ('1', '16', '0') __version__ = '.'.join(__version_info__) from .__wrapt__ import (ObjectProxy, CallableObjectProxy, FunctionWrapper,