diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2aa76d97..3b9e673e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,16 +17,12 @@ jobs: os: - ubuntu-20.04 python-version: - # - 2.7 - # - 3.5 - - 3.6 - - 3.7 - 3.8 - 3.9 - "3.10" - 3.11 - 3.12 - # - pypy-2.7 + - 3.13 - pypy-3.8 - pypy-3.9 - pypy-3.10 @@ -49,6 +45,34 @@ jobs: name: coverage path: .coverage.* + test_linux_no_gil: + name: Test - NO GIL (${{ matrix.os }}, ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + os: + - ubuntu-20.04 + python-version: [3.13] + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup Python ${{ matrix.python-version }} + uses: deadsnakes/action@v3.2.0 + with: + python-version: ${{ matrix.python-version }} + nogil: true + - 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 + - name: Store partial coverage reports + uses: actions/upload-artifact@v3 + with: + name: coverage + path: .coverage.* + # test_aarch64_linux: # name: Test (${{ matrix.python.os }}, ${{ matrix.python.python-version }}, aarch64) # runs-on: ${{ matrix.python.os }} @@ -91,16 +115,12 @@ jobs: os: - macos-latest python-version: - # - 2.7 - # - 3.5 - - 3.6 - - 3.7 - 3.8 - 3.9 - "3.10" - 3.11 - 3.12 - # - pypy-2.7 + - 3.13 - pypy-3.8 - pypy-3.9 - pypy-3.10 @@ -123,29 +143,6 @@ 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: name: Test (${{ matrix.os }}, ${{ matrix.python-version }}) runs-on: ${{ matrix.os }} @@ -154,15 +151,12 @@ jobs: os: - windows-latest python-version: - # - 3.5 - - 3.6 - - 3.7 - 3.8 - 3.9 - "3.10" - 3.11 - 3.12 - # - pypy-2.7 + - 3.13 - pypy-3.8 - pypy-3.9 - pypy-3.10 @@ -208,40 +202,13 @@ 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: - name: Build wheels (3.6+) on ${{ matrix.os }} for ${{ matrix.arch }} + name: Build wheels (3.8+) on ${{ matrix.os }} for ${{ matrix.arch }} needs: - test_linux + - test_linux_no_gil #- test_aarch64_linux - test_macos - # - test_windows_py27 - test_windows runs-on: ${{ matrix.os }} strategy: @@ -259,7 +226,7 @@ jobs: if: ${{ matrix.arch == 'aarch64' }} uses: docker/setup-qemu-action@v2 - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2.21.3 with: output-dir: dist env: @@ -267,38 +234,39 @@ jobs: CIBW_SKIP: pp* CIBW_BUILD_VERBOSITY: 1 CIBW_ARCHS: ${{ matrix.arch }} + CIBW_FREE_THREADED_SUPPORT: 1 - uses: actions/upload-artifact@v3 with: name: dist path: dist/*.whl - coveralls: - name: Generate code coverage report - if: ${{ false }} # disable for now - needs: - - test_linux - - test_macos - # - test_windows_py27 - - test_windows - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Setup Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: 3.9 - - name: Install coverage package - run: python -m pip install -U coverage - - name: Download partial coverage reports - uses: actions/download-artifact@v3 - with: - name: coverage - - name: Combine coverage - run: python -m coverage combine - - name: Report coverage - run: python -m coverage report - - name: Export coverage to XML - run: python -m coverage xml - - name: Upload coverage statistics to Coveralls - uses: AndreMiras/coveralls-python-action@develop + # coveralls: + # name: Generate code coverage report + # if: ${{ false }} # disable for now + # needs: + # - test_linux + # - test_linux_no_gil + # - test_macos + # - test_windows + # runs-on: ubuntu-20.04 + # steps: + # - name: Checkout code + # uses: actions/checkout@v3 + # - name: Setup Python 3.9 + # uses: actions/setup-python@v4 + # with: + # python-version: 3.9 + # - name: Install coverage package + # run: python -m pip install -U coverage + # - name: Download partial coverage reports + # uses: actions/download-artifact@v3 + # with: + # name: coverage + # - name: Combine coverage + # run: python -m coverage combine + # - name: Report coverage + # run: python -m coverage report + # - name: Export coverage to XML + # run: python -m coverage xml + # - name: Upload coverage statistics to Coveralls + # uses: AndreMiras/coveralls-python-action@develop diff --git a/.gitignore b/.gitignore index 5772125b..ce210084 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.py[cod] +venv # C extensions *.so diff --git a/README.rst b/README.rst index af0d61c7..58988f37 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ wrapt ===== -|Docs| |PyPI| +|PyPI| The aim of the **wrapt** module is to provide a transparent object proxy for Python, which can be used as the basis for the construction of function @@ -137,8 +137,3 @@ and unit tests, can be obtained from github. .. |PyPI| image:: https://img.shields.io/pypi/v/wrapt.svg?logo=python&cacheSeconds=3600 :target: https://pypi.python.org/pypi/wrapt - -.. |Docs| image:: https://readthedocs.org/projects/docs/badge/?version=latest - :alt: Documentation Status - :scale: 100% - :target: https://wrapt.readthedocs.io/en/latest/?badge=latest diff --git a/docs/changes.rst b/docs/changes.rst index ecd7f39c..60e8bd06 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,39 @@ Release Notes ============= +Version 1.17.0 +-------------- + +Note that version 1.17.0 drops support for Python 3.6 and 3.7. Python version +3.8 or later is required. + +**New Features** + +* Add `__format__()` method to `ObjectProxy` class to allow formatting of + wrapped object. + +* Added C extension internal flag to indicate that `wrapt` should be safe for + Python 3.13 free threading mode. Releases will include free threading variants + of Python wheels. Note that as free threading is new, one should be cautious + about using it in production until it has been more widely tested. + +**Bugs Fixed** + +* When a normal function or builtin function which had `wrapt.decorator` or a + function wrapper applied, was assigned as a class attribute, and the function + attribute called via the class or an instance of the class, an additional + argument was being passed, inserted as the first argument, which was the class + or instance. This was not the correct behaviour and the class or instance + should not have been passed as the first argument. + +* When an instance of a callable class object was wrapped which didn't not have + a `__get__()` method for binding, and it was called in context whhere binding + would be attempted, it would fail with error that `__get__()` did not exist + when instead it should have been called directly, ignoring that binding was + not possible. + +* The `__round__` hook for the object proxy didn't accept `ndigits` argument. + Version 1.16.0 -------------- diff --git a/docs/conf.py b/docs/conf.py index 255558b5..2d485832 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '1.16' +version = '1.17' # The full version, including alpha/beta/rc tags. -release = '1.16.0' +release = '1.17.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.cfg b/setup.cfg index 9c282c93..80536bf1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,7 @@ [metadata] name = wrapt version = attr: wrapt.__version__ +readme = "README.rst" author = Graham Dumpleton author_email = Graham.Dumpleton@gmail.com url = https://github.com/GrahamDumpleton/wrapt @@ -17,13 +18,12 @@ classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: BSD License Programming Language :: Python :: 3 - 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 :: 3.13 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy project_urls = @@ -33,7 +33,7 @@ project_urls = [options] zip_safe = false -python_requires = >= 3.6 +python_requires = >= 3.8 packages = find: package_dir = =src @@ -70,19 +70,19 @@ norecursedirs = .tox venv [tox:tox] envlist = - py{36,37,38,39,310,311,312} - py{36,37,38,39,310,311,312}-{without,install,disable}-extensions, + py{38,39,310,311,312,313,314} + py{38,39,310,311,312,313,314}-{without,install,disable}-extensions, pypy-without-extensions [gh-actions] python = - 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 3.9: py39, py39-without-extensions, py39-install-extensions, py39-disable-extensions 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 + 3.13: py313, py313-without-extensions, py313-install-extensions, py313-disable-extensions + 3.14: py314, py314-without-extensions, py314-install-extensions, py314-disable-extensions pypy-3.8: pypy-without-extensions pypy-3.9: pypy-without-extensions pypy-3.10: pypy-without-extensions @@ -92,7 +92,7 @@ deps = coverage pytest install_command = - py311,py311-{without,install,disable}-extensions: python -m pip install --no-binary coverage {opts} {packages} + python -m pip install --no-binary coverage {opts} {packages} commands = python -m coverage run --rcfile {toxinidir}/setup.cfg -m pytest -v {posargs} {toxinidir}/tests setenv = diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index ed31a943..e634fd34 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('1', '16', '0') +__version_info__ = ('1', '17', '0') __version__ = '.'.join(__version_info__) from .__wrapt__ import (ObjectProxy, CallableObjectProxy, FunctionWrapper, diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index e0e1b5bc..433af1b6 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -38,6 +38,7 @@ typedef struct { PyObject *enabled; PyObject *binding; PyObject *parent; + PyObject *owner; } WraptFunctionWrapperObject; PyTypeObject WraptFunctionWrapperBase_Type; @@ -127,7 +128,7 @@ static int WraptObjectProxy_init(WraptObjectProxyObject *self, { PyObject *wrapped = NULL; - static char *kwlist[] = { "wrapped", NULL }; + char *const kwlist[] = { "wrapped", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:ObjectProxy", kwlist, &wrapped)) { @@ -1283,6 +1284,24 @@ static PyObject *WraptObjectProxy_bytes( /* ------------------------------------------------------------------------- */ +static PyObject *WraptObjectProxy_format( + WraptObjectProxyObject *self, PyObject *args) +{ + PyObject *format_spec = NULL; + + if (!self->wrapped) { + PyErr_SetString(PyExc_ValueError, "wrapper has not been initialized"); + return NULL; + } + + if (!PyArg_ParseTuple(args, "|O:format", &format_spec)) + return NULL; + + return PyObject_Format(self->wrapped, format_spec); +} + +/* ------------------------------------------------------------------------- */ + static PyObject *WraptObjectProxy_reversed( WraptObjectProxyObject *self, PyObject *args) { @@ -1299,26 +1318,34 @@ static PyObject *WraptObjectProxy_reversed( #if PY_MAJOR_VERSION >= 3 static PyObject *WraptObjectProxy_round( - WraptObjectProxyObject *self, PyObject *args) + WraptObjectProxyObject *self, PyObject *args, PyObject *kwds) { + PyObject *ndigits = NULL; + PyObject *module = NULL; PyObject *dict = NULL; PyObject *round = NULL; PyObject *result = NULL; + char *const kwlist[] = { "ndigits", NULL }; + if (!self->wrapped) { PyErr_SetString(PyExc_ValueError, "wrapper has not been initialized"); return NULL; } + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O:ObjectProxy", + kwlist, &ndigits)) { + return NULL; + } + module = PyImport_ImportModule("builtins"); if (!module) return NULL; - dict = PyModule_GetDict(module); - round = PyDict_GetItemString(dict, "round"); + round = PyObject_GetAttrString(module, "round"); if (!round) { Py_DECREF(module); @@ -1328,7 +1355,7 @@ static PyObject *WraptObjectProxy_round( Py_INCREF(round); Py_DECREF(module); - result = PyObject_CallFunctionObjArgs(round, self->wrapped, NULL); + result = PyObject_CallFunctionObjArgs(round, self->wrapped, ndigits, NULL); Py_DECREF(round); @@ -1796,9 +1823,11 @@ static PyMethodDef WraptObjectProxy_methods[] = { { "__getattr__", (PyCFunction)WraptObjectProxy_getattr, METH_VARARGS , 0 }, { "__bytes__", (PyCFunction)WraptObjectProxy_bytes, METH_NOARGS, 0 }, + { "__format__", (PyCFunction)WraptObjectProxy_format, METH_VARARGS, 0 }, { "__reversed__", (PyCFunction)WraptObjectProxy_reversed, METH_NOARGS, 0 }, #if PY_MAJOR_VERSION >= 3 - { "__round__", (PyCFunction)WraptObjectProxy_round, METH_NOARGS, 0 }, + { "__round__", (PyCFunction)WraptObjectProxy_round, + METH_VARARGS | METH_KEYWORDS, 0 }, #endif { "__complex__", (PyCFunction)WraptObjectProxy_complex, METH_NOARGS, 0 }, #if PY_MAJOR_VERSION > 3 || (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 7) @@ -2211,6 +2240,7 @@ static PyObject *WraptFunctionWrapperBase_new(PyTypeObject *type, self->enabled = NULL; self->binding = NULL; self->parent = NULL; + self->owner = NULL; return (PyObject *)self; } @@ -2219,7 +2249,8 @@ static PyObject *WraptFunctionWrapperBase_new(PyTypeObject *type, static int WraptFunctionWrapperBase_raw_init(WraptFunctionWrapperObject *self, PyObject *wrapped, PyObject *instance, PyObject *wrapper, - PyObject *enabled, PyObject *binding, PyObject *parent) + PyObject *enabled, PyObject *binding, PyObject *parent, + PyObject *owner) { int result = 0; @@ -2246,6 +2277,10 @@ static int WraptFunctionWrapperBase_raw_init(WraptFunctionWrapperObject *self, Py_INCREF(parent); Py_XDECREF(self->parent); self->parent = parent; + + Py_INCREF(owner); + Py_XDECREF(self->owner); + self->owner = owner; } return result; @@ -2262,31 +2297,32 @@ static int WraptFunctionWrapperBase_init(WraptFunctionWrapperObject *self, PyObject *enabled = Py_None; PyObject *binding = NULL; PyObject *parent = Py_None; + PyObject *owner = Py_None; - static PyObject *function_str = NULL; + static PyObject *callable_str = NULL; - static char *kwlist[] = { "wrapped", "instance", "wrapper", - "enabled", "binding", "parent", NULL }; + char *const kwlist[] = { "wrapped", "instance", "wrapper", + "enabled", "binding", "parent", "owner", NULL }; - if (!function_str) { + if (!callable_str) { #if PY_MAJOR_VERSION >= 3 - function_str = PyUnicode_InternFromString("function"); + callable_str = PyUnicode_InternFromString("callable"); #else - function_str = PyString_InternFromString("function"); + callable_str = PyString_InternFromString("callable"); #endif } if (!PyArg_ParseTupleAndKeywords(args, kwds, - "OOO|OOO:FunctionWrapperBase", kwlist, &wrapped, &instance, - &wrapper, &enabled, &binding, &parent)) { + "OOO|OOOO:FunctionWrapperBase", kwlist, &wrapped, &instance, + &wrapper, &enabled, &binding, &parent, &owner)) { return -1; } if (!binding) - binding = function_str; + binding = callable_str; return WraptFunctionWrapperBase_raw_init(self, wrapped, instance, wrapper, - enabled, binding, parent); + enabled, binding, parent, owner); } /* ------------------------------------------------------------------------- */ @@ -2301,6 +2337,7 @@ static int WraptFunctionWrapperBase_traverse(WraptFunctionWrapperObject *self, Py_VISIT(self->enabled); Py_VISIT(self->binding); Py_VISIT(self->parent); + Py_VISIT(self->owner); return 0; } @@ -2316,6 +2353,7 @@ static int WraptFunctionWrapperBase_clear(WraptFunctionWrapperObject *self) Py_CLEAR(self->enabled); Py_CLEAR(self->binding); Py_CLEAR(self->parent); + Py_CLEAR(self->owner); return 0; } @@ -2341,15 +2379,21 @@ static PyObject *WraptFunctionWrapperBase_call( PyObject *result = NULL; static PyObject *function_str = NULL; + static PyObject *callable_str = NULL; static PyObject *classmethod_str = NULL; + static PyObject *instancemethod_str = NULL; if (!function_str) { #if PY_MAJOR_VERSION >= 3 function_str = PyUnicode_InternFromString("function"); + callable_str = PyUnicode_InternFromString("callable"); classmethod_str = PyUnicode_InternFromString("classmethod"); + instancemethod_str = PyUnicode_InternFromString("instancemethod"); #else function_str = PyString_InternFromString("function"); + callable_str = PyString_InternFromString("callable"); classmethod_str = PyString_InternFromString("classmethod"); + instancemethod_str = PyString_InternFromString("instancemethod"); #endif } @@ -2381,6 +2425,10 @@ static PyObject *WraptFunctionWrapperBase_call( if ((self->instance == Py_None) && (self->binding == function_str || PyObject_RichCompareBool(self->binding, function_str, + Py_EQ) == 1 || self->binding == instancemethod_str || + PyObject_RichCompareBool(self->binding, instancemethod_str, + Py_EQ) == 1 || self->binding == callable_str || + PyObject_RichCompareBool(self->binding, callable_str, Py_EQ) == 1 || self->binding == classmethod_str || PyObject_RichCompareBool(self->binding, classmethod_str, Py_EQ) == 1)) { @@ -2423,6 +2471,10 @@ static PyObject *WraptFunctionWrapperBase_descr_get( static PyObject *bound_type_str = NULL; static PyObject *function_str = NULL; + static PyObject *callable_str = NULL; + static PyObject *builtin_str = NULL; + static PyObject *class_str = NULL; + static PyObject *instancemethod_str = NULL; if (!bound_type_str) { #if PY_MAJOR_VERSION >= 3 @@ -2437,32 +2489,35 @@ static PyObject *WraptFunctionWrapperBase_descr_get( if (!function_str) { #if PY_MAJOR_VERSION >= 3 function_str = PyUnicode_InternFromString("function"); + callable_str = PyUnicode_InternFromString("callable"); + builtin_str = PyUnicode_InternFromString("builtin"); + class_str = PyUnicode_InternFromString("class"); + instancemethod_str = PyUnicode_InternFromString("instancemethod"); #else function_str = PyString_InternFromString("function"); + callable_str = PyString_InternFromString("callable"); + builtin_str = PyString_InternFromString("builtin"); + class_str = PyString_InternFromString("class"); + instancemethod_str = PyString_InternFromString("instancemethod"); #endif } if (self->parent == Py_None) { -#if PY_MAJOR_VERSION < 3 - if (PyObject_IsInstance(self->object_proxy.wrapped, - (PyObject *)&PyClass_Type) || PyObject_IsInstance( - self->object_proxy.wrapped, (PyObject *)&PyType_Type)) { + if (self->binding == builtin_str || PyObject_RichCompareBool( + self->binding, builtin_str, Py_EQ) == 1) { Py_INCREF(self); return (PyObject *)self; } -#else - if (PyObject_IsInstance(self->object_proxy.wrapped, - (PyObject *)&PyType_Type)) { + + if (self->binding == class_str || PyObject_RichCompareBool( + self->binding, class_str, Py_EQ) == 1) { Py_INCREF(self); return (PyObject *)self; } -#endif if (Py_TYPE(self->object_proxy.wrapped)->tp_descr_get == NULL) { - PyErr_Format(PyExc_AttributeError, - "'%s' object has no attribute '__get__'", - Py_TYPE(self->object_proxy.wrapped)->tp_name); - return NULL; + Py_INCREF(self); + return (PyObject *)self; } descriptor = (Py_TYPE(self->object_proxy.wrapped)->tp_descr_get)( @@ -2485,7 +2540,7 @@ static PyObject *WraptFunctionWrapperBase_descr_get( result = PyObject_CallFunctionObjArgs(bound_type ? bound_type : (PyObject *)&WraptBoundFunctionWrapper_Type, descriptor, obj, self->wrapper, self->enabled, self->binding, - self, NULL); + self, type, NULL); Py_XDECREF(bound_type); Py_DECREF(descriptor); @@ -2495,6 +2550,10 @@ static PyObject *WraptFunctionWrapperBase_descr_get( if (self->instance == Py_None && (self->binding == function_str || PyObject_RichCompareBool(self->binding, function_str, + Py_EQ) == 1 || self->binding == instancemethod_str || + PyObject_RichCompareBool(self->binding, instancemethod_str, + Py_EQ) == 1 || self->binding == callable_str || + PyObject_RichCompareBool(self->binding, callable_str, Py_EQ) == 1)) { PyObject *wrapped = NULL; @@ -2543,7 +2602,7 @@ static PyObject *WraptFunctionWrapperBase_descr_get( result = PyObject_CallFunctionObjArgs(bound_type ? bound_type : (PyObject *)&WraptBoundFunctionWrapper_Type, descriptor, obj, self->wrapper, self->enabled, self->binding, - self->parent, NULL); + self->parent, type, NULL); Py_XDECREF(bound_type); Py_DECREF(descriptor); @@ -2719,6 +2778,20 @@ static PyObject *WraptFunctionWrapperBase_get_self_parent( return self->parent; } +/* ------------------------------------------------------------------------- */ + +static PyObject *WraptFunctionWrapperBase_get_self_owner( + WraptFunctionWrapperObject *self, void *closure) +{ + if (!self->owner) { + Py_INCREF(Py_None); + return Py_None; + } + + Py_INCREF(self->owner); + return self->owner; +} + /* ------------------------------------------------------------------------- */; static PyMethodDef WraptFunctionWrapperBase_methods[] = { @@ -2748,6 +2821,8 @@ static PyGetSetDef WraptFunctionWrapperBase_getset[] = { NULL, 0 }, { "_self_parent", (getter)WraptFunctionWrapperBase_get_self_parent, NULL, 0 }, + { "_self_owner", (getter)WraptFunctionWrapperBase_get_self_owner, + NULL, 0 }, { NULL }, }; @@ -2815,6 +2890,7 @@ static PyObject *WraptBoundFunctionWrapper_call( PyObject *result = NULL; static PyObject *function_str = NULL; + static PyObject *callable_str = NULL; if (self->enabled != Py_None) { if (PyCallable_Check(self->enabled)) { @@ -2840,8 +2916,10 @@ static PyObject *WraptBoundFunctionWrapper_call( if (!function_str) { #if PY_MAJOR_VERSION >= 3 function_str = PyUnicode_InternFromString("function"); + callable_str = PyUnicode_InternFromString("callable"); #else function_str = PyString_InternFromString("function"); + callable_str = PyString_InternFromString("callable"); #endif } @@ -2851,9 +2929,49 @@ static PyObject *WraptBoundFunctionWrapper_call( */ if (self->binding == function_str || PyObject_RichCompareBool( - self->binding, function_str, Py_EQ) == 1) { + self->binding, function_str, Py_EQ) == 1 || + self->binding == callable_str || PyObject_RichCompareBool( + self->binding, callable_str, Py_EQ) == 1) { + + // if (self->instance == Py_None) { + // /* + // * This situation can occur where someone is calling the + // * instancemethod via the class type and passing the + // * instance as the first argument. We need to shift the args + // * before making the call to the wrapper and effectively + // * bind the instance to the wrapped function using a partial + // * so the wrapper doesn't see anything as being different. + // */ + + // if (PyTuple_Size(args) == 0) { + // PyErr_SetString(PyExc_TypeError, + // "missing 1 required positional argument"); + // return NULL; + // } + + // instance = PyTuple_GetItem(args, 0); + + // if (!instance) + // return NULL; + + // wrapped = PyObject_CallFunctionObjArgs( + // (PyObject *)&WraptPartialCallableObjectProxy_Type, + // self->object_proxy.wrapped, instance, NULL); - if (self->instance == Py_None) { + // if (!wrapped) + // return NULL; + + // param_args = PyTuple_GetSlice(args, 1, PyTuple_Size(args)); + + // if (!param_args) { + // Py_DECREF(wrapped); + // return NULL; + // } + + // args = param_args; + // } + + if (self->instance == Py_None && PyTuple_Size(args) != 0) { /* * This situation can occur where someone is calling the * instancemethod via the class type and passing the @@ -2863,35 +2981,35 @@ static PyObject *WraptBoundFunctionWrapper_call( * so the wrapper doesn't see anything as being different. */ - if (PyTuple_Size(args) == 0) { - PyErr_SetString(PyExc_TypeError, - "missing 1 required positional argument"); - return NULL; - } - instance = PyTuple_GetItem(args, 0); if (!instance) return NULL; - wrapped = PyObject_CallFunctionObjArgs( - (PyObject *)&WraptPartialCallableObjectProxy_Type, - self->object_proxy.wrapped, instance, NULL); + if (PyObject_IsInstance(instance, self->owner) == 1) { + wrapped = PyObject_CallFunctionObjArgs( + (PyObject *)&WraptPartialCallableObjectProxy_Type, + self->object_proxy.wrapped, instance, NULL); - if (!wrapped) - return NULL; + if (!wrapped) + return NULL; - param_args = PyTuple_GetSlice(args, 1, PyTuple_Size(args)); + param_args = PyTuple_GetSlice(args, 1, PyTuple_Size(args)); - if (!param_args) { - Py_DECREF(wrapped); - return NULL; - } + if (!param_args) { + Py_DECREF(wrapped); + return NULL; + } - args = param_args; + args = param_args; + } + else { + instance = self->instance; + } } - else + else { instance = self->instance; + } if (!wrapped) { Py_INCREF(self->object_proxy.wrapped); @@ -3022,19 +3140,31 @@ static int WraptFunctionWrapper_init(WraptFunctionWrapperObject *self, PyObject *binding = NULL; PyObject *instance = NULL; + static PyObject *function_str = NULL; static PyObject *classmethod_str = NULL; static PyObject *staticmethod_str = NULL; - static PyObject *function_str = NULL; + static PyObject *callable_str = NULL; + static PyObject *builtin_str = NULL; + static PyObject *class_str = NULL; + static PyObject *instancemethod_str = NULL; int result = 0; - static char *kwlist[] = { "wrapped", "wrapper", "enabled", NULL }; + char *const kwlist[] = { "wrapped", "wrapper", "enabled", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O:FunctionWrapper", kwlist, &wrapped, &wrapper, &enabled)) { return -1; } + if (!function_str) { +#if PY_MAJOR_VERSION >= 3 + function_str = PyUnicode_InternFromString("function"); +#else + function_str = PyString_InternFromString("function"); +#endif + } + if (!classmethod_str) { #if PY_MAJOR_VERSION >= 3 classmethod_str = PyUnicode_InternFromString("classmethod"); @@ -3051,44 +3181,86 @@ static int WraptFunctionWrapper_init(WraptFunctionWrapperObject *self, #endif } - if (!function_str) { + if (!callable_str) { #if PY_MAJOR_VERSION >= 3 - function_str = PyUnicode_InternFromString("function"); + callable_str = PyUnicode_InternFromString("callable"); #else - function_str = PyString_InternFromString("function"); + callable_str = PyString_InternFromString("callable"); #endif } - if (PyObject_IsInstance(wrapped, (PyObject *)&PyClassMethod_Type)) { - binding = classmethod_str; + if (!builtin_str) { +#if PY_MAJOR_VERSION >= 3 + builtin_str = PyUnicode_InternFromString("builtin"); +#else + builtin_str = PyString_InternFromString("builtin"); +#endif } - else if (PyObject_IsInstance(wrapped, (PyObject *)&PyStaticMethod_Type)) { - binding = staticmethod_str; + + if (!class_str) { +#if PY_MAJOR_VERSION >= 3 + class_str = PyUnicode_InternFromString("class"); +#else + class_str = PyString_InternFromString("class"); +#endif } - else if ((instance = PyObject_GetAttrString(wrapped, "__self__")) != 0) { -#if PY_MAJOR_VERSION < 3 - if (PyObject_IsInstance(instance, (PyObject *)&PyClass_Type) || - PyObject_IsInstance(instance, (PyObject *)&PyType_Type)) { - binding = classmethod_str; - } + + if (!instancemethod_str) { +#if PY_MAJOR_VERSION >= 3 + instancemethod_str = PyUnicode_InternFromString("instancemethod"); #else - if (PyObject_IsInstance(instance, (PyObject *)&PyType_Type)) { - binding = classmethod_str; - } + instancemethod_str = PyString_InternFromString("instancemethod"); #endif - else - binding = function_str; + } - Py_DECREF(instance); + if (PyObject_IsInstance(wrapped, (PyObject *)&WraptFunctionWrapperBase_Type)) { + binding = PyObject_GetAttrString(wrapped, "_self_binding"); } - else { - PyErr_Clear(); - binding = function_str; + if (!binding) { + if (PyCFunction_Check(wrapped)) { + binding = builtin_str; + } + else if (PyObject_IsInstance(wrapped, (PyObject *)&PyFunction_Type)) { + binding = function_str; + } + else if (PyObject_IsInstance(wrapped, (PyObject *)&PyClassMethod_Type)) { + binding = classmethod_str; + } + else if (PyObject_IsInstance(wrapped, (PyObject *)&PyType_Type)) { + binding = class_str; + } + else if (PyObject_IsInstance(wrapped, (PyObject *)&PyStaticMethod_Type)) { + binding = staticmethod_str; + } + else if ((instance = PyObject_GetAttrString(wrapped, "__self__")) != 0) { + #if PY_MAJOR_VERSION < 3 + if (PyObject_IsInstance(instance, (PyObject *)&PyClass_Type) || + PyObject_IsInstance(instance, (PyObject *)&PyType_Type)) { + binding = classmethod_str; + } + #else + if (PyObject_IsInstance(instance, (PyObject *)&PyType_Type)) { + binding = classmethod_str; + } + #endif + else if (PyObject_IsInstance(wrapped, (PyObject *)&PyMethod_Type)) { + binding = instancemethod_str; + } + else + binding = callable_str; + + Py_DECREF(instance); + } + else { + PyErr_Clear(); + + binding = callable_str; + } } result = WraptFunctionWrapperBase_raw_init(self, wrapped, Py_None, - wrapper, enabled, binding, Py_None); + wrapper, enabled, binding, Py_None, Py_None); return result; } @@ -3222,6 +3394,10 @@ moduleinit(void) PyModule_AddObject(module, "BoundFunctionWrapper", (PyObject *)&WraptBoundFunctionWrapper_Type); +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); +#endif + return module; } diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index dfc3440d..f70ee792 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -126,12 +126,15 @@ def __repr__(self): type(self.__wrapped__).__name__, id(self.__wrapped__)) + def __format__(self, format_spec): + return format(self.__wrapped__, format_spec) + def __reversed__(self): return reversed(self.__wrapped__) if not PY2: - def __round__(self): - return round(self.__wrapped__) + def __round__(self, ndigits=None): + return round(self.__wrapped__, ndigits) if sys.hexversion >= 0x03070000: def __mro_entries__(self, bases): @@ -490,10 +493,10 @@ def _unpack_self(self, *args): class _FunctionWrapperBase(ObjectProxy): __slots__ = ('_self_instance', '_self_wrapper', '_self_enabled', - '_self_binding', '_self_parent') + '_self_binding', '_self_parent', '_self_owner') def __init__(self, wrapped, instance, wrapper, enabled=None, - binding='function', parent=None): + binding='callable', parent=None, owner=None): super(_FunctionWrapperBase, self).__init__(wrapped) @@ -502,60 +505,68 @@ def __init__(self, wrapped, instance, wrapper, enabled=None, object.__setattr__(self, '_self_enabled', enabled) object.__setattr__(self, '_self_binding', binding) object.__setattr__(self, '_self_parent', parent) + object.__setattr__(self, '_self_owner', owner) def __get__(self, instance, owner): - # This method is actually doing double duty for both unbound and - # bound derived wrapper classes. It should possibly be broken up - # and the distinct functionality moved into the derived classes. - # Can't do that straight away due to some legacy code which is - # relying on it being here in this base class. + # This method is actually doing double duty for both unbound and bound + # derived wrapper classes. It should possibly be broken up and the + # distinct functionality moved into the derived classes. Can't do that + # straight away due to some legacy code which is relying on it being + # here in this base class. # - # The distinguishing attribute which determines whether we are - # being called in an unbound or bound wrapper is the parent - # attribute. If binding has never occurred, then the parent will - # be None. + # The distinguishing attribute which determines whether we are being + # called in an unbound or bound wrapper is the parent attribute. If + # binding has never occurred, then the parent will be None. # - # First therefore, is if we are called in an unbound wrapper. In - # this case we perform the binding. + # First therefore, is if we are called in an unbound wrapper. In this + # case we perform the binding. # - # We have one special case to worry about here. This is where we - # are decorating a nested class. In this case the wrapped class - # would not have a __get__() method to call. In that case we - # simply return self. + # We have two special cases to worry about here. These are where we are + # decorating a class or builtin function as neither provide a __get__() + # method to call. In this case we simply return self. # - # Note that we otherwise still do binding even if instance is - # None and accessing an unbound instance method from a class. - # This is because we need to be able to later detect that - # specific case as we will need to extract the instance from the - # first argument of those passed in. + # Note that we otherwise still do binding even if instance is None and + # accessing an unbound instance method from a class. This is because we + # need to be able to later detect that specific case as we will need to + # extract the instance from the first argument of those passed in. if self._self_parent is None: - if not inspect.isclass(self.__wrapped__): - descriptor = self.__wrapped__.__get__(instance, owner) + # Technically can probably just check for existence of __get__ on + # the wrapped object, but this is more explicit. + + if self._self_binding == 'builtin': + return self + + if self._self_binding == "class": + return self + + binder = getattr(self.__wrapped__, '__get__', None) - return self.__bound_function_wrapper__(descriptor, instance, - self._self_wrapper, self._self_enabled, - self._self_binding, self) + if binder is None: + return self - return self + descriptor = binder(instance, owner) - # Now we have the case of binding occurring a second time on what - # was already a bound function. In this case we would usually - # return ourselves again. This mirrors what Python does. + return self.__bound_function_wrapper__(descriptor, instance, + self._self_wrapper, self._self_enabled, + self._self_binding, self, owner) + + # Now we have the case of binding occurring a second time on what was + # already a bound function. In this case we would usually return + # ourselves again. This mirrors what Python does. # - # The special case this time is where we were originally bound - # with an instance of None and we were likely an instance - # method. In that case we rebind against the original wrapped - # function from the parent again. + # The special case this time is where we were originally bound with an + # instance of None and we were likely an instance method. In that case + # we rebind against the original wrapped function from the parent again. - if self._self_instance is None and self._self_binding == 'function': + if self._self_instance is None and self._self_binding in ('function', 'instancemethod', 'callable'): descriptor = self._self_parent.__wrapped__.__get__( instance, owner) return self._self_parent.__bound_function_wrapper__( descriptor, instance, self._self_wrapper, self._self_enabled, self._self_binding, - self._self_parent) + self._self_parent, owner) return self @@ -582,7 +593,7 @@ def _unpack_self(self, *args): # a function that was already bound to an instance. In that case # we want to extract the instance from the function and use it. - if self._self_binding in ('function', 'classmethod'): + if self._self_binding in ('function', 'instancemethod', 'classmethod', 'callable'): if self._self_instance is None: instance = getattr(self.__wrapped__, '__self__', None) if instance is not None: @@ -633,11 +644,11 @@ def _unpack_self(self, *args): self, args = _unpack_self(*args) - # If enabled has been specified, then evaluate it at this point - # and if the wrapper is not to be executed, then simply return - # the bound function rather than a bound wrapper for the bound - # function. When evaluating enabled, if it is callable we call - # it, otherwise we evaluate it as a boolean. + # If enabled has been specified, then evaluate it at this point and if + # the wrapper is not to be executed, then simply return the bound + # function rather than a bound wrapper for the bound function. When + # evaluating enabled, if it is callable we call it, otherwise we + # evaluate it as a boolean. if self._self_enabled is not None: if callable(self._self_enabled): @@ -646,18 +657,27 @@ def _unpack_self(self, *args): elif not self._self_enabled: return self.__wrapped__(*args, **kwargs) - # We need to do things different depending on whether we are - # likely wrapping an instance method vs a static method or class - # method. + # We need to do things different depending on whether we are likely + # wrapping an instance method vs a static method or class method. if self._self_binding == 'function': + if self._self_instance is None and args: + instance, newargs = args[0], args[1:] + if isinstance(instance, self._self_owner): + wrapped = PartialCallableObjectProxy(self.__wrapped__, instance) + return self._self_wrapper(wrapped, instance, newargs, kwargs) + + return self._self_wrapper(self.__wrapped__, self._self_instance, + args, kwargs) + + elif self._self_binding == 'callable': if self._self_instance is None: # This situation can occur where someone is calling the - # instancemethod via the class type and passing the instance - # as the first argument. We need to shift the args before - # making the call to the wrapper and effectively bind the - # instance to the wrapped function using a partial so the - # wrapper doesn't see anything as being different. + # instancemethod via the class type and passing the instance as + # the first argument. We need to shift the args before making + # the call to the wrapper and effectively bind the instance to + # the wrapped function using a partial so the wrapper doesn't + # see anything as being different. if not args: raise TypeError('missing 1 required positional argument') @@ -759,26 +779,43 @@ def __init__(self, wrapped, wrapper, enabled=None): # or patch it in the __dict__ of the class type. # # So to get the best outcome we can, whenever we aren't sure what - # it is, we label it as a 'function'. If it was already bound and + # it is, we label it as a 'callable'. If it was already bound and # that is rebound later, we assume that it will be an instance - # method and try an cope with the possibility that the 'self' + # method and try and cope with the possibility that the 'self' # argument it being passed as an explicit argument and shuffle # the arguments around to extract 'self' for use as the instance. - if isinstance(wrapped, classmethod): - binding = 'classmethod' + binding = None - elif isinstance(wrapped, staticmethod): - binding = 'staticmethod' + if isinstance(wrapped, _FunctionWrapperBase): + binding = wrapped._self_binding - elif hasattr(wrapped, '__self__'): - if inspect.isclass(wrapped.__self__): - binding = 'classmethod' - else: + if not binding: + if inspect.isbuiltin(wrapped): + binding = 'builtin' + + elif inspect.isfunction(wrapped): binding = 'function' - else: - binding = 'function' + elif inspect.isclass(wrapped): + binding = 'class' + + elif isinstance(wrapped, classmethod): + binding = 'classmethod' + + elif isinstance(wrapped, staticmethod): + binding = 'staticmethod' + + elif hasattr(wrapped, '__self__'): + if inspect.isclass(wrapped.__self__): + binding = 'classmethod' + elif inspect.ismethod(wrapped): + binding = 'instancemethod' + else: + binding = 'callable' + + else: + binding = 'callable' super(FunctionWrapper, self).__init__(wrapped, None, wrapper, enabled, binding) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 2213abc9..80f05e9e 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -1,5 +1,6 @@ from __future__ import print_function +import operator import unittest import wrapt @@ -112,5 +113,227 @@ def _function(*args, **kwargs): self.assertEqual(result, (_args, _kwargs)) + def test_decorated_function_as_class_attribute(self): + + @wrapt.decorator + def passthrough(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @passthrough + def function(self): + pass + + class A: + _function = function + + self.assertTrue(A._function._self_parent is function) + self.assertEqual(A._function._self_binding, "function") + self.assertEqual(A._function._self_owner, A) + + a = A() + + A._function(a) + + self.assertTrue(a._function._self_parent is function) + self.assertEqual(a._function._self_binding, "function") + self.assertEqual(a._function._self_owner, A) + + a._function() + + # Test example without using the decorator to show same outcome. + + def xfunction(self): + pass + + class B: + _xfunction = xfunction + + b = B() + + B._xfunction(b) + + b._xfunction() + + def test_decorated_function_as_instance_attribute(self): + + @wrapt.decorator + def passthrough(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @passthrough + def function(self): + pass + + class A: + def __init__(self): + self._function = function + + a = A() + + self.assertTrue(a._function._self_parent is None) + self.assertEqual(a._function._self_binding, "function") + self.assertTrue(a._function._self_owner is None) + + a._function(a) + + bound_a = a._function.__get__(a, A) + self.assertTrue(bound_a._self_parent is function) + self.assertEqual(bound_a._self_binding, "function") + self.assertTrue(bound_a._self_owner is A) + + bound_a() + + # Test example without using the decorator to show same outcome. + + def xfunction(self): + pass + + class B: + def __init__(self): + self._xfunction = xfunction + + b = B() + + b._xfunction(b) + + def test_decorated_builtin_as_class_attribute(self): + + @wrapt.decorator + def passthrough(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + function = passthrough(operator.add) + + class A: + _function = function + + self.assertTrue(A._function._self_parent is None) + self.assertEqual(A._function._self_binding, "builtin") + self.assertTrue(A._function._self_owner is None) + + A._function(1, 2) + + a = A() + + self.assertTrue(a._function._self_parent is None) + self.assertEqual(a._function._self_binding, "builtin") + self.assertTrue(a._function._self_owner is None) + + a._function(1, 2) + + # Test example without using the decorator to show same outcome. + + class B: + _xfunction = operator.add + + B._xfunction(1, 2) + + b = B() + + b._xfunction(1, 2) + + def test_call_semantics_for_assorted_decorator_use_cases(self): + def g1(): + pass + + def g2(self): + pass + + class C1: + def __init__(self): + self.f3 = g1 + + def f1(self): + print("SELF", self) + + f2 = g2 + + c1 = C1() + + c1.f2() + c1.f3() + + class C2: + f2 = C1.f1 + f3 = C1.f2 + + c2 = C2() + + c2.f2() + c2.f3() + + class C3: + f2 = c1.f1 + + c3 = C3() + + c3.f2() + + @wrapt.decorator + def passthrough(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + class C11: + @passthrough + def f1(self): + print("SELF", self) + + c11 = C11() + + class C12: + f2 = C11.f1 + + c12 = C12() + + c12.f2() + + class C13: + f2 = c11.f1 + + c13 = C13() + + c13.f2() + + C11.f1(c11) + C12.f2(c12) + + def test_call_semantics_for_assorted_wrapped_descriptor_use_cases(self): + class A: + def __call__(self): + print("A:__call__") + + a = A() + + class B: + def __call__(self): + print("B:__call__") + def __get__(self, obj, type): + print("B:__get__") + return self + + b = B() + + class C: + f1 = a + f2 = b + + c = C() + + c.f1() + c.f2() + + @wrapt.decorator + def wrapper(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + class D: + f1 = wrapper(a) + f2 = wrapper(b) + + d = D() + + d.f1() + d.f2() + if __name__ == '__main__': unittest.main() diff --git a/tests/test_function_wrapper.py b/tests/test_function_wrapper.py index c353b205..ac7df47f 100644 --- a/tests/test_function_wrapper.py +++ b/tests/test_function_wrapper.py @@ -213,12 +213,7 @@ def function1(self, *args, **kwargs): self.assertEqual(function2._self_wrapper, decorator2) - # We can't identify this as being an instance method in - # Python 3 when it is a class so have to disable the check - # for Python 2. This has flow on effect of not working - # in the case of an instance either. - - self.assertEqual(function2._self_binding, 'function') + self.assertEqual(function2._self_binding, 'instancemethod') def test_classmethod_attributes_external_instance(self): def decorator1(wrapped, instance, args, kwargs): @@ -515,7 +510,7 @@ def wrapper(wrapped, instance, args, kwargs): def test_re_bind_after_none(self): - def function(): + def function(self): pass def wrapper(wrapped, instance, args, kwargs): @@ -529,6 +524,8 @@ def wrapper(wrapped, instance, args, kwargs): _bound_wrapper_1 = _wrapper.__get__(None, type(instance)) + _bound_wrapper_1(instance) + self.assertTrue(_bound_wrapper_1._self_parent is _wrapper) self.assertTrue(isinstance(_bound_wrapper_1, @@ -543,6 +540,8 @@ def wrapper(wrapped, instance, args, kwargs): wrapt.BoundFunctionWrapper)) self.assertEqual(_bound_wrapper_2._self_instance, instance) + _bound_wrapper_2() + self.assertTrue(_bound_wrapper_1 is not _bound_wrapper_2) class TestInvalidWrapper(unittest.TestCase): @@ -555,7 +554,7 @@ def _wrapper(wrapped, instance, args, kwargs): wrapper = wrapt.FunctionWrapper(None, _wrapper) wrapper.__get__(list(), list)() - self.assertRaises(AttributeError, run, ()) + self.assertRaises(TypeError, run, ()) class TestInvalidCalling(unittest.TestCase): diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index 91a010b3..a8a079be 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -1583,6 +1583,13 @@ def test_repr(self): self.assertNotEqual(repr(value).find('ObjectProxy at'), -1) + def test_format(self): + value = 1 + + proxy = wrapt.ObjectProxy(1) + + self.assertEqual("{:0>3}".format(proxy), "{:0>3}".format(value)) + class TestDerivedClassCreation(unittest.TestCase): def test_derived_new(self): @@ -1845,7 +1852,9 @@ def test_fractions_round(self): proxy = wrapt.ObjectProxy(instance) self.assertEqual(round(instance), round(proxy)) - + self.assertEqual(round(instance, 3), round(proxy, 3)) + self.assertEqual(round(instance, ndigits=3), round(proxy, ndigits=3)) + class TestArgumentUnpacking(unittest.TestCase): def test_self_keyword_argument_on_dict(self): diff --git a/tests/test_outer_classmethod.py b/tests/test_outer_classmethod.py index ab807646..c08d34a5 100644 --- a/tests/test_outer_classmethod.py +++ b/tests/test_outer_classmethod.py @@ -128,18 +128,20 @@ def test_class_call_function(self): # first argument with the actual arguments following that. This # was only finally fixed in Python 3.9. For more details see: # https://bugs.python.org/issue19072 + # Starting with Python 3.13 the old behavior is back. + # For more details see https://github.com/python/cpython/issues/89519 _args = (1, 2) _kwargs = {'one': 1, 'two': 2} @wrapt.decorator def _decorator(wrapped, instance, args, kwargs): - if PYXY < (3, 9): - self.assertEqual(instance, None) - self.assertEqual(args, (Class,)+_args) - else: + if (3, 9) <= PYXY < (3, 13): self.assertEqual(instance, Class) self.assertEqual(args, _args) + else: + self.assertEqual(instance, None) + self.assertEqual(args, (Class,)+_args) self.assertEqual(kwargs, _kwargs) self.assertEqual(wrapped.__module__, _function.__module__) @@ -176,12 +178,12 @@ def test_instance_call_function(self): @wrapt.decorator def _decorator(wrapped, instance, args, kwargs): - if PYXY < (3, 9): - self.assertEqual(instance, None) - self.assertEqual(args, (Class,)+_args) - else: + if (3, 9) <= PYXY < (3, 13): self.assertEqual(instance, Class) self.assertEqual(args, _args) + else: + self.assertEqual(instance, None) + self.assertEqual(args, (Class,)+_args) self.assertEqual(kwargs, _kwargs) self.assertEqual(wrapped.__module__, _function.__module__) diff --git a/tests/test_synchronized_lock.py b/tests/test_synchronized_lock.py index 0e43f7af..7c41aa5a 100644 --- a/tests/test_synchronized_lock.py +++ b/tests/test_synchronized_lock.py @@ -165,36 +165,38 @@ def test_synchronized_outer_classmethod(self): # function to the class before calling and just calls it direct, # explicitly passing the class as first argument. For more # details see: https://bugs.python.org/issue19072 + # Starting with Python 3.13 the old behavior is back. + # For more details see https://github.com/python/cpython/issues/89519 - if PYXY < (3, 9): - _lock0 = getattr(C4.function2, '_synchronized_lock', None) - else: + if (3, 9) <= PYXY < (3, 13): _lock0 = getattr(C4, '_synchronized_lock', None) + else: + _lock0 = getattr(C4.function2, '_synchronized_lock', None) self.assertEqual(_lock0, None) c4.function2() - if PYXY < (3, 9): - _lock1 = getattr(C4.function2, '_synchronized_lock', None) - else: + if (3, 9) <= PYXY < (3, 13): _lock1 = getattr(C4, '_synchronized_lock', None) + else: + _lock1 = getattr(C4.function2, '_synchronized_lock', None) self.assertNotEqual(_lock1, None) C4.function2() - if PYXY < (3, 9): - _lock2 = getattr(C4.function2, '_synchronized_lock', None) - else: + if (3, 9) <= PYXY < (3, 13): _lock2 = getattr(C4, '_synchronized_lock', None) + else: + _lock2 = getattr(C4.function2, '_synchronized_lock', None) self.assertNotEqual(_lock2, None) self.assertEqual(_lock2, _lock1) C4.function2() - if PYXY < (3, 9): - _lock3 = getattr(C4.function2, '_synchronized_lock', None) - else: + if (3, 9) <= PYXY < (3, 13): _lock3 = getattr(C4, '_synchronized_lock', None) + else: + _lock3 = getattr(C4.function2, '_synchronized_lock', None) self.assertNotEqual(_lock3, None) self.assertEqual(_lock3, _lock2) diff --git a/tests/test_weak_function_proxy.py b/tests/test_weak_function_proxy.py index 2babe95c..33ec962c 100644 --- a/tests/test_weak_function_proxy.py +++ b/tests/test_weak_function_proxy.py @@ -145,6 +145,7 @@ def callback(proxy): self.assertEqual(proxy(1, 2), (1, 2)) + Class.function = None Class = None gc.collect() @@ -166,6 +167,7 @@ def callback(proxy): self.assertEqual(proxy(1, 2), (1, 2)) + Class.function = None Class = None gc.collect()