From ead46800140b42b7a82693683e2b65fab07a101d Mon Sep 17 00:00:00 2001 From: Thor Whalen Date: Thu, 20 May 2021 15:02:28 -0700 Subject: [PATCH 01/15] Minor typos and grammar - "which which" (which was repeated twice) - which -> that (https://www.grammarly.com/blog/which-vs-that/) --- src/wrapt/decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wrapt/decorators.py b/src/wrapt/decorators.py index 506303d7..239ebc3f 100644 --- a/src/wrapt/decorators.py +++ b/src/wrapt/decorators.py @@ -360,7 +360,7 @@ def _capture(target_wrapped): # This one is a bit strange because binding was actually # performed on the wrapper created by our decorator # factory. We need to apply that binding to the decorator - # wrapper function which which the decorator factory + # wrapper function that the decorator factory # was applied to. target_wrapper = wrapper.__get__(None, instance) @@ -384,7 +384,7 @@ def _capture(target_wrapped): # This one is a bit strange because binding was actually # performed on the wrapper created by our decorator # factory. We need to apply that binding to the decorator - # wrapper function which which the decorator factory + # wrapper function that the decorator factory # was applied to. target_wrapper = wrapper.__get__(instance, type(instance)) From aab91b70e720c36059436774233884cdcc7deb28 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 30 Oct 2021 08:55:38 +1100 Subject: [PATCH 02/15] Force ReadTheDocs to use Python 3.9. --- .readthedocs.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..ed160cc3 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,23 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# 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.9 +# install: +# - requirements: docs/requirements.txt From e6cbaff3ee4166904bca0379ea565ddd302d50e2 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 30 Oct 2021 08:58:09 +1100 Subject: [PATCH 03/15] Hosted ReadTheDocs only supports up to Python 3.8. --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ed160cc3..58356671 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,6 +18,6 @@ sphinx: # Optionally set the version of Python and requirements required to build your docs python: - version: 3.9 + version: 3.8 # install: # - requirements: docs/requirements.txt From 2417bdff6985b9b4558631133dd69244ec28754f Mon Sep 17 00:00:00 2001 From: Thor Whalen Date: Mon, 15 Nov 2021 09:53:14 -0800 Subject: [PATCH 04/15] fix: correct typo properly->property --- src/wrapt/wrappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index 2bc6136f..2ef64349 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -87,7 +87,7 @@ def __init__(self, wrapped): pass # Python 3.10 onwards also does not allow itself to be overridden - # using a properly and it must instead be set explicitly. + # using a property and it must instead be set explicitly. try: object.__setattr__(self, '__annotations__', wrapped.__annotations__) From 0d473b32f3fe428708598132387734922f516d9c Mon Sep 17 00:00:00 2001 From: hongweipeng Date: Fri, 24 Dec 2021 21:53:34 +0800 Subject: [PATCH 05/15] Add support for isinstance() checks. #201 --- src/wrapt/_wrappers.c | 35 ++++++++++++++++++++++++++++++++++ src/wrapt/wrappers.py | 5 +++++ tests/test_inheritance_py37.py | 28 ++++++++++++++++++++++++--- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index b677fff6..00658615 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -2541,6 +2541,39 @@ static PyObject *WraptFunctionWrapperBase_set_name( return result; } +/* ------------------------------------------------------------------------- */ +static PyObject *WraptFunctionWrapperBase_instancecheck( + WraptFunctionWrapperObject *self, PyObject *args) +{ + PyObject *instance = NULL; + PyObject *object = NULL; + PyObject *result = NULL; + + int check = 0; + if (!self->object_proxy.wrapped) { + PyErr_SetString(PyExc_ValueError, "wrapper has not been initialized"); + return NULL; + } + + if (!PyArg_ParseTuple(args, "O", &instance)) { + return NULL; + } + + object = PyObject_GetAttrString(self, "__wrapped__"); + if (!object) { + PyErr_Clear(); + } + + check = PyObject_IsInstance(instance, object); + Py_XDECREF(object); + if (check < 0) { + return NULL; + } + result = check ? Py_True : Py_False; + + Py_INCREF(result); + return result; +} /* ------------------------------------------------------------------------- */ @@ -2656,6 +2689,8 @@ static PyObject *WraptFunctionWrapperBase_get_self_parent( static PyMethodDef WraptFunctionWrapperBase_methods[] = { { "__set_name__", (PyCFunction)WraptFunctionWrapperBase_set_name, METH_VARARGS | METH_KEYWORDS, 0 }, + { "__instancecheck__", (PyCFunction)WraptFunctionWrapperBase_instancecheck, + METH_VARARGS, 0 }, { "__subclasscheck__", (PyCFunction)WraptFunctionWrapperBase_subclasscheck, METH_VARARGS, 0 }, { NULL, NULL }, diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index 2bc6136f..d909a85c 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -588,6 +588,11 @@ def __set_name__(self, owner, name): if hasattr(self.__wrapped__, "__set_name__"): self.__wrapped__.__set_name__(owner, name) + def __instancecheck__(self, instance): + # This is a special method used by isinstance() to make checks + # instance of the `__wrapped__`. + return isinstance(instance, self.__wrapped__) + def __subclasscheck__(self, subclass): # This is a special method used by issubclass() to make checks # about inheritance of classes. We need to upwrap any object diff --git a/tests/test_inheritance_py37.py b/tests/test_inheritance_py37.py index b11e08bf..755b5990 100644 --- a/tests/test_inheritance_py37.py +++ b/tests/test_inheritance_py37.py @@ -12,7 +12,7 @@ class TestClassInheritance(unittest.TestCase): def test_basic_inheritance(self): @wrapt.decorator def wrapper(wrapped, instance, args, kwargs): - return wrapped(args, kwargs) + return wrapped(*args, **kwargs) class B1: def method(self): @@ -43,6 +43,18 @@ def method(self): self.assertTrue(issubclass(C1, (B1, C1, D1))) self.assertTrue(issubclass(D1, (B1, C1, D1))) + self.assertTrue(isinstance(C1(), B1)) + self.assertTrue(isinstance(C1(), C1)) + self.assertTrue(isinstance(D1(), B1)) + self.assertTrue(isinstance(D1(), C1)) + + def function(): + pass + class F(wrapt.FunctionWrapper): + pass + instance = F(function, wrapper) + self.assertTrue(isinstance(instance, wrapt.FunctionWrapper)) + def test_abc_inheritance(self): # XXX The checks commented out below all fail because the # helpers for issubclass() via __subclasscheck__() in ABCMeta @@ -58,7 +70,7 @@ def test_abc_inheritance(self): @wrapt.decorator def wrapper(wrapped, instance, args, kwargs): - return wrapped(args, kwargs) + return wrapped(*args, **kwargs) class A1(metaclass=abc.ABCMeta): @abc.abstractmethod @@ -103,6 +115,11 @@ def method(self): # self.assertTrue(issubclass(C1, (A1, B1, C1, D1))) self.assertTrue(issubclass(D1, (A1, B1, C1, D1))) + self.assertTrue(isinstance(C1(), B1)) + self.assertTrue(isinstance(C1(), C1)) + self.assertTrue(isinstance(D1(), B1)) + self.assertTrue(isinstance(D1(), C1)) + def test_py_abc_inheritance(self): # In contrast to above when C implementation for ABCMeta helpers # are used, these all pass as have use the Python implementation @@ -110,7 +127,7 @@ def test_py_abc_inheritance(self): @wrapt.decorator def wrapper(wrapped, instance, args, kwargs): - return wrapped(args, kwargs) + return wrapped(*args, **kwargs) class A1(metaclass=_py_abc.ABCMeta): @abc.abstractmethod @@ -155,5 +172,10 @@ def method(self): self.assertTrue(issubclass(C1, (A1, B1, C1, D1))) self.assertTrue(issubclass(D1, (A1, B1, C1, D1))) + self.assertTrue(isinstance(C1(), B1)) + self.assertTrue(isinstance(C1(), C1)) + self.assertTrue(isinstance(D1(), B1)) + self.assertTrue(isinstance(D1(), C1)) + if __name__ == '__main__': unittest.main() From 55caf77dd24f2ce2731475e655c0e073eef78b84 Mon Sep 17 00:00:00 2001 From: hongweipeng Date: Fri, 24 Dec 2021 23:17:29 +0800 Subject: [PATCH 06/15] review --- src/wrapt/_wrappers.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index 00658615..575bdaa8 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -2541,11 +2541,12 @@ static PyObject *WraptFunctionWrapperBase_set_name( return result; } + /* ------------------------------------------------------------------------- */ + static PyObject *WraptFunctionWrapperBase_instancecheck( - WraptFunctionWrapperObject *self, PyObject *args) + WraptFunctionWrapperObject *self, PyObject *instance) { - PyObject *instance = NULL; PyObject *object = NULL; PyObject *result = NULL; @@ -2555,10 +2556,6 @@ static PyObject *WraptFunctionWrapperBase_instancecheck( return NULL; } - if (!PyArg_ParseTuple(args, "O", &instance)) { - return NULL; - } - object = PyObject_GetAttrString(self, "__wrapped__"); if (!object) { PyErr_Clear(); @@ -2690,7 +2687,7 @@ static PyMethodDef WraptFunctionWrapperBase_methods[] = { { "__set_name__", (PyCFunction)WraptFunctionWrapperBase_set_name, METH_VARARGS | METH_KEYWORDS, 0 }, { "__instancecheck__", (PyCFunction)WraptFunctionWrapperBase_instancecheck, - METH_VARARGS, 0 }, + METH_O, 0}, { "__subclasscheck__", (PyCFunction)WraptFunctionWrapperBase_subclasscheck, METH_VARARGS, 0 }, { NULL, NULL }, From 1c84deb279921d4aa9b2ed0542270d1b7e35fdde Mon Sep 17 00:00:00 2001 From: Cyril Jouve Date: Sun, 31 Oct 2021 13:06:14 +0100 Subject: [PATCH 07/15] compatibility with python 3.11 --- .github/workflows/main.yml | 6 ++--- setup.cfg | 2 ++ src/wrapt/decorators.py | 46 ++++++++++++++++++++++++++++++-- tests/compat.py | 5 ++++ tests/test_adapter.py | 44 +++++++++++++++--------------- tests/test_adapter_py3.py | 9 +++---- tests/test_function.py | 6 ++--- tests/test_inner_classmethod.py | 11 ++++---- tests/test_inner_staticmethod.py | 11 ++++---- tests/test_instancemethod.py | 18 ++++++------- tests/test_nested_function.py | 7 +++-- tests/test_outer_classmethod.py | 12 ++++----- tests/test_outer_staticmethod.py | 11 ++++---- 13 files changed, 115 insertions(+), 73 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0af078d9..26669bed 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: - 3.8 - 3.9 - "3.10" - #- 3.11-dev + - 3.11-dev - pypy-2.7 - pypy-3.6 - pypy-3.7 @@ -57,7 +57,7 @@ jobs: - 3.8 - 3.9 - "3.10" - #- 3.11-dev + - 3.11-dev - pypy-2.7 #- pypy-3.6 - pypy-3.7 @@ -117,7 +117,7 @@ jobs: - 3.8 - 3.9 - "3.10" - #- 3.11-dev + - 3.11-dev - pypy-2.7 - pypy-3.6 - pypy-3.7 diff --git a/setup.cfg b/setup.cfg index 40915a01..eded06fb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -93,6 +93,8 @@ python = deps = coverage pytest +install_command = + py311,py311-{without,install,disable}-extensions: 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/decorators.py b/src/wrapt/decorators.py index 389a764b..eb0bd747 100644 --- a/src/wrapt/decorators.py +++ b/src/wrapt/decorators.py @@ -31,10 +31,52 @@ def exec_(_code_, _globs_=None, _locs_=None): del builtins from functools import partial -from inspect import ismethod, isclass, formatargspec -from collections import namedtuple +from inspect import isclass from threading import Lock, RLock +try: + from inspect import formatargspec +except ImportError: # > py3.10 + from inspect import formatannotation + def formatargspec(args, varargs=None, varkw=None, defaults=None, + kwonlyargs=(), kwonlydefaults={}, annotations={}, + formatarg=str, + formatvarargs=lambda name: '*' + name, + formatvarkw=lambda name: '**' + name, + formatvalue=lambda value: '=' + repr(value), + formatreturns=lambda text: ' -> ' + text, + formatannotation=formatannotation): + + def formatargandannotation(arg): + result = formatarg(arg) + if arg in annotations: + result += ': ' + formatannotation(annotations[arg]) + return result + specs = [] + if defaults: + firstdefault = len(args) - len(defaults) + for i, arg in enumerate(args): + spec = formatargandannotation(arg) + if defaults and i >= firstdefault: + spec = spec + formatvalue(defaults[i - firstdefault]) + specs.append(spec) + if varargs is not None: + specs.append(formatvarargs(formatargandannotation(varargs))) + else: + if kwonlyargs: + specs.append('*') + if kwonlyargs: + for kwonlyarg in kwonlyargs: + spec = formatargandannotation(kwonlyarg) + if kwonlydefaults and kwonlyarg in kwonlydefaults: + spec += formatvalue(kwonlydefaults[kwonlyarg]) + specs.append(spec) + if varkw is not None: + specs.append(formatvarkw(formatargandannotation(varkw))) + result = '(' + ', '.join(specs) + ')' + if 'return' in annotations: + result += formatreturns(formatannotation(annotations['return'])) + return result try: from inspect import signature except ImportError: diff --git a/tests/compat.py b/tests/compat.py index 0eed716e..83b61f94 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -26,3 +26,8 @@ def exec_(_code_, _globs_=None, _locs_=None): exec_("""def reraise(tp, value, tb=None): raise tp, value, tb """) + +try: + from inspect import getfullargspec +except ImportError: + from inspect import getargspec as getfullargspec diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 14ba6604..906210a8 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -6,7 +6,7 @@ import wrapt -from compat import PY2, PY3, exec_ +from compat import PY2, exec_, getfullargspec DECORATORS_CODE = """ import wrapt @@ -72,15 +72,15 @@ def test_argspec(self): def _adapter(arg1, arg2, arg3=None, *args, **kwargs): pass - function1a_argspec = inspect.getargspec(_adapter) - function1d_argspec = inspect.getargspec(function1d) + function1a_argspec = getfullargspec(_adapter) + function1d_argspec = getfullargspec(function1d) self.assertEqual(function1a_argspec, function1d_argspec) # Now bind the function to an instance. The argspec should # still match. bound_function1d = function1d.__get__(object(), object) - bound_function1d_argspec = inspect.getargspec(bound_function1d) + bound_function1d_argspec = getfullargspec(bound_function1d) self.assertEqual(function1a_argspec, bound_function1d_argspec) def test_signature(self): @@ -107,7 +107,7 @@ class TestDynamicAdapter(unittest.TestCase): def test_dynamic_adapter_function(self): def _adapter(arg1, arg2, arg3=None, *args, **kwargs): pass - argspec = inspect.getargspec(_adapter) + argspec = getfullargspec(_adapter) @wrapt.decorator(adapter=argspec) def _wrapper_1(wrapped, instance, args, kwargs): @@ -117,9 +117,9 @@ def _wrapper_1(wrapped, instance, args, kwargs): def _function_1(): pass - self.assertEqual(inspect.getargspec(_function_1), argspec) + self.assertEqual(getfullargspec(_function_1), argspec) - args = inspect.formatargspec(*argspec) + args = '(arg1, arg2, arg3=None, *args, **kwargs)' @wrapt.decorator(adapter=args) def _wrapper_2(wrapped, instance, args, kwargs): @@ -129,12 +129,12 @@ def _wrapper_2(wrapped, instance, args, kwargs): def _function_2(): pass - self.assertEqual(inspect.getargspec(_function_2), argspec) + self.assertEqual(getfullargspec(_function_2), argspec) def test_dynamic_adapter_instancemethod(self): def _adapter(self, arg1, arg2, arg3=None, *args, **kwargs): pass - argspec = inspect.getargspec(_adapter) + argspec = getfullargspec(_adapter) @wrapt.decorator(adapter=argspec) def _wrapper_1(wrapped, instance, args, kwargs): @@ -147,10 +147,10 @@ def function(self): instance1 = Class1() - self.assertEqual(inspect.getargspec(Class1.function), argspec) - self.assertEqual(inspect.getargspec(instance1.function), argspec) + self.assertEqual(getfullargspec(Class1.function), argspec) + self.assertEqual(getfullargspec(instance1.function), argspec) - args = inspect.formatargspec(*argspec) + args = '(self, arg1, arg2, arg3=None, *args, **kwargs)' @wrapt.decorator(adapter=args) def _wrapper_2(wrapped, instance, args, kwargs): @@ -163,13 +163,13 @@ def function(self): instance2 = Class2() - self.assertEqual(inspect.getargspec(Class2.function), argspec) - self.assertEqual(inspect.getargspec(instance2.function), argspec) + self.assertEqual(getfullargspec(Class2.function), argspec) + self.assertEqual(getfullargspec(instance2.function), argspec) def test_dynamic_adapter_classmethod(self): def _adapter(cls, arg1, arg2, arg3=None, *args, **kwargs): pass - argspec = inspect.getargspec(_adapter) + argspec = getfullargspec(_adapter) @wrapt.decorator(adapter=argspec) def _wrapper_1(wrapped, instance, args, kwargs): @@ -183,10 +183,10 @@ def function(cls): instance1 = Class1() - self.assertEqual(inspect.getargspec(Class1.function), argspec) - self.assertEqual(inspect.getargspec(instance1.function), argspec) + self.assertEqual(getfullargspec(Class1.function), argspec) + self.assertEqual(getfullargspec(instance1.function), argspec) - args = inspect.formatargspec(*argspec) + args = '(cls, arg1, arg2, arg3=None, *args, **kwargs)' @wrapt.decorator(adapter=args) def _wrapper_2(wrapped, instance, args, kwargs): @@ -200,12 +200,12 @@ def function(self): instance2 = Class2() - self.assertEqual(inspect.getargspec(Class2.function), argspec) - self.assertEqual(inspect.getargspec(instance2.function), argspec) + self.assertEqual(getfullargspec(Class2.function), argspec) + self.assertEqual(getfullargspec(instance2.function), argspec) def test_adapter_factory(self): def factory(wrapped): - argspec = inspect.getargspec(wrapped) + argspec = getfullargspec(wrapped) argspec.args.insert(0, 'arg0') return argspec @@ -217,7 +217,7 @@ def _wrapper_1(wrapped, instance, args, kwargs): def _function_1(arg1, arg2): pass - argspec = inspect.getargspec(_function_1) + argspec = getfullargspec(_function_1) self.assertEqual(argspec.args, ['arg0', 'arg1', 'arg2']) diff --git a/tests/test_adapter_py3.py b/tests/test_adapter_py3.py index 02c360f7..9291bd82 100644 --- a/tests/test_adapter_py3.py +++ b/tests/test_adapter_py3.py @@ -3,13 +3,12 @@ import inspect import unittest import imp -import collections from typing import Iterable import wrapt -from compat import PY2, PY3, exec_ +from compat import PY2, exec_ DECORATORS_CODE = """ import wrapt @@ -128,7 +127,7 @@ def _adapter2(arg1, arg2, arg3=None, *args, **kwargs) -> int: pass argspec2 = inspect.getfullargspec(_adapter2) - args = inspect.formatargspec(*argspec2) + args = '(arg1, arg2, arg3=None, *args, **kwargs) -> int' @wrapt.decorator(adapter=args) def _wrapper_2(wrapped, instance, args, kwargs): @@ -166,7 +165,7 @@ def _adapter2(self, arg1, arg2, arg3=None, *args, **kwargs) -> int: pass argspec2 = inspect.getfullargspec(_adapter2) - args = inspect.formatargspec(*argspec2) + args = '(self, arg1, arg2, arg3=None, *args, **kwargs) -> int' @wrapt.decorator(adapter=args) def _wrapper_2(wrapped, instance, args, kwargs): @@ -209,7 +208,7 @@ def _adapter2(cls, arg1, arg2, arg3=None, *args, **kwargs) -> int: pass argspec2 = inspect.getfullargspec(_adapter2) - args = inspect.formatargspec(*argspec2) + args = '(cls, arg1, arg2, arg3=None, *args, **kwargs) -> int' @wrapt.decorator(adapter=args) def _wrapper_2(wrapped, instance, args, kwargs): diff --git a/tests/test_function.py b/tests/test_function.py index 64a71083..73a3b432 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -6,7 +6,7 @@ import wrapt -from compat import PY2, PY3, exec_ +from compat import exec_, getfullargspec DECORATORS_CODE = """ import wrapt @@ -57,8 +57,8 @@ def test_doc_string(self): def test_argspec(self): # Test preservation of function argument specification. - function1o_argspec = inspect.getargspec(function1o) - function1d_argspec = inspect.getargspec(function1d) + function1o_argspec = getfullargspec(function1o) + function1d_argspec = getfullargspec(function1d) self.assertEqual(function1o_argspec, function1d_argspec) def test_getmembers(self): diff --git a/tests/test_inner_classmethod.py b/tests/test_inner_classmethod.py index abf4ca16..d442d4f0 100644 --- a/tests/test_inner_classmethod.py +++ b/tests/test_inner_classmethod.py @@ -1,12 +1,11 @@ from __future__ import print_function import unittest -import inspect import imp import wrapt -from compat import PY2, PY3, exec_ +from compat import exec_, getfullargspec DECORATORS_CODE = """ import wrapt @@ -95,15 +94,15 @@ def test_instance_doc_string(self): def test_class_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(Original.function) - function_argspec = inspect.getargspec(Class.function) + original_argspec = getfullargspec(Original.function) + function_argspec = getfullargspec(Class.function) self.assertEqual(original_argspec, function_argspec) def test_instance_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(Original().function) - function_argspec = inspect.getargspec(Class().function) + original_argspec = getfullargspec(Original().function) + function_argspec = getfullargspec(Class().function) self.assertEqual(original_argspec, function_argspec) def test_class_isinstance(self): diff --git a/tests/test_inner_staticmethod.py b/tests/test_inner_staticmethod.py index 01a54bae..31948653 100644 --- a/tests/test_inner_staticmethod.py +++ b/tests/test_inner_staticmethod.py @@ -1,12 +1,11 @@ from __future__ import print_function import unittest -import inspect import imp import wrapt -from compat import PY2, PY3, exec_ +from compat import exec_, getfullargspec DECORATORS_CODE = """ import wrapt @@ -95,15 +94,15 @@ def test_instance_doc_string(self): def test_class_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(Original.function) - function_argspec = inspect.getargspec(Class.function) + original_argspec = getfullargspec(Original.function) + function_argspec = getfullargspec(Class.function) self.assertEqual(original_argspec, function_argspec) def test_instance_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(Original().function) - function_argspec = inspect.getargspec(Class().function) + original_argspec = getfullargspec(Original().function) + function_argspec = getfullargspec(Class().function) self.assertEqual(original_argspec, function_argspec) def test_class_isinstance(self): diff --git a/tests/test_instancemethod.py b/tests/test_instancemethod.py index b6356bd0..9ad6bb83 100644 --- a/tests/test_instancemethod.py +++ b/tests/test_instancemethod.py @@ -6,7 +6,7 @@ import wrapt -from compat import PY2, PY3, exec_ +from compat import exec_, getfullargspec DECORATORS_CODE = """ import wrapt @@ -95,15 +95,15 @@ def test_instance_doc_string(self): def test_class_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(OldClass1o.function) - function_argspec = inspect.getargspec(OldClass1d.function) + original_argspec = getfullargspec(OldClass1o.function) + function_argspec = getfullargspec(OldClass1d.function) self.assertEqual(original_argspec, function_argspec) def test_instance_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(OldClass1o().function) - function_argspec = inspect.getargspec(OldClass1d().function) + original_argspec = getfullargspec(OldClass1o().function) + function_argspec = getfullargspec(OldClass1d().function) self.assertEqual(original_argspec, function_argspec) def test_getmembers(self): @@ -198,15 +198,15 @@ def test_instance_doc_string(self): def test_class_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(NewClass1o.function) - function_argspec = inspect.getargspec(NewClass1d.function) + original_argspec = getfullargspec(NewClass1o.function) + function_argspec = getfullargspec(NewClass1d.function) self.assertEqual(original_argspec, function_argspec) def test_instance_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(NewClass1o().function) - function_argspec = inspect.getargspec(NewClass1d().function) + original_argspec = getfullargspec(NewClass1o().function) + function_argspec = getfullargspec(NewClass1d().function) self.assertEqual(original_argspec, function_argspec) def test_class_isinstance(self): diff --git a/tests/test_nested_function.py b/tests/test_nested_function.py index 4f55a36b..a6c3bcd8 100644 --- a/tests/test_nested_function.py +++ b/tests/test_nested_function.py @@ -1,12 +1,11 @@ from __future__ import print_function import unittest -import inspect import imp import wrapt -from compat import PY2, PY3, exec_ +from compat import exec_, getfullargspec DECORATORS_CODE = """ import wrapt @@ -66,8 +65,8 @@ def test_doc_string(self): def test_argspec(self): # Test preservation of function argument specification. - function1o_argspec = inspect.getargspec(function1o()) - function1d_argspec = inspect.getargspec(function1d()) + function1o_argspec = getfullargspec(function1o()) + function1d_argspec = getfullargspec(function1d()) self.assertEqual(function1o_argspec, function1d_argspec) def test_isinstance(self): diff --git a/tests/test_outer_classmethod.py b/tests/test_outer_classmethod.py index fbf37f7a..c4c2d345 100644 --- a/tests/test_outer_classmethod.py +++ b/tests/test_outer_classmethod.py @@ -1,13 +1,11 @@ from __future__ import print_function -import sys import unittest -import inspect import imp import wrapt -from compat import PY2, PY3, PYXY, exec_ +from compat import PYXY, exec_, getfullargspec DECORATORS_CODE = """ import wrapt @@ -96,15 +94,15 @@ def test_instance_doc_string(self): def test_class_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(Original.function) - function_argspec = inspect.getargspec(Class.function) + original_argspec = getfullargspec(Original.function) + function_argspec = getfullargspec(Class.function) self.assertEqual(original_argspec, function_argspec) def test_instance_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(Original().function) - function_argspec = inspect.getargspec(Class().function) + original_argspec = getfullargspec(Original().function) + function_argspec = getfullargspec(Class().function) self.assertEqual(original_argspec, function_argspec) def test_class_isinstance(self): diff --git a/tests/test_outer_staticmethod.py b/tests/test_outer_staticmethod.py index 0a0d0f77..2a8c1938 100644 --- a/tests/test_outer_staticmethod.py +++ b/tests/test_outer_staticmethod.py @@ -1,12 +1,11 @@ from __future__ import print_function import unittest -import inspect import imp import wrapt -from compat import PY2, PY3, exec_ +from compat import exec_, getfullargspec DECORATORS_CODE = """ import wrapt @@ -95,15 +94,15 @@ def test_instance_doc_string(self): def test_class_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(Original.function) - function_argspec = inspect.getargspec(Class.function) + original_argspec = getfullargspec(Original.function) + function_argspec = getfullargspec(Class.function) self.assertEqual(original_argspec, function_argspec) def test_instance_argspec(self): # Test preservation of instance method argument specification. - original_argspec = inspect.getargspec(Original().function) - function_argspec = inspect.getargspec(Class().function) + original_argspec = getfullargspec(Original().function) + function_argspec = getfullargspec(Class().function) self.assertEqual(original_argspec, function_argspec) def test_class_isinstance(self): From 49c9f8822dfd80a8896b9985d9a5f8ae898bfdff Mon Sep 17 00:00:00 2001 From: Cyril Jouve Date: Mon, 17 Jan 2022 23:17:31 +0100 Subject: [PATCH 08/15] implement formatargspec with Signature --- src/wrapt/decorators.py | 69 ++++++++++++++------------------ tests/test_formatargspec_py35.py | 35 ++++++++++++++++ tests/test_formatargspec_py38.py | 37 +++++++++++++++++ 3 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 tests/test_formatargspec_py35.py create mode 100644 tests/test_formatargspec_py38.py diff --git a/src/wrapt/decorators.py b/src/wrapt/decorators.py index eb0bd747..a92e7e6f 100644 --- a/src/wrapt/decorators.py +++ b/src/wrapt/decorators.py @@ -35,48 +35,37 @@ def exec_(_code_, _globs_=None, _locs_=None): from threading import Lock, RLock try: + from inspect import Parameter, Signature +except ImportError: # < py3.5 from inspect import formatargspec -except ImportError: # > py3.10 - from inspect import formatannotation +else: def formatargspec(args, varargs=None, varkw=None, defaults=None, - kwonlyargs=(), kwonlydefaults={}, annotations={}, - formatarg=str, - formatvarargs=lambda name: '*' + name, - formatvarkw=lambda name: '**' + name, - formatvalue=lambda value: '=' + repr(value), - formatreturns=lambda text: ' -> ' + text, - formatannotation=formatannotation): - - def formatargandannotation(arg): - result = formatarg(arg) - if arg in annotations: - result += ': ' + formatannotation(annotations[arg]) - return result - specs = [] - if defaults: - firstdefault = len(args) - len(defaults) - for i, arg in enumerate(args): - spec = formatargandannotation(arg) - if defaults and i >= firstdefault: - spec = spec + formatvalue(defaults[i - firstdefault]) - specs.append(spec) - if varargs is not None: - specs.append(formatvarargs(formatargandannotation(varargs))) - else: - if kwonlyargs: - specs.append('*') - if kwonlyargs: - for kwonlyarg in kwonlyargs: - spec = formatargandannotation(kwonlyarg) - if kwonlydefaults and kwonlyarg in kwonlydefaults: - spec += formatvalue(kwonlydefaults[kwonlyarg]) - specs.append(spec) - if varkw is not None: - specs.append(formatvarkw(formatargandannotation(varkw))) - result = '(' + ', '.join(specs) + ')' - if 'return' in annotations: - result += formatreturns(formatannotation(annotations['return'])) - return result + kwonlyargs=(), kwonlydefaults={}, annotations={}): + if kwonlydefaults is None: + kwonlydefaults = {} + ndefaults = len(defaults) if defaults else 0 + parameters = [ + Parameter( + arg, + Parameter.POSITIONAL_OR_KEYWORD, + default=defaults[i] if i >= 0 else Parameter.empty, + annotation=annotations.get(arg, Parameter.empty), + ) for i, arg in enumerate(args, ndefaults - len(args)) + ] + if varargs: + parameters.append(Parameter(varargs, Parameter.VAR_POSITIONAL)) + parameters.extend( + Parameter( + kwonlyarg, + Parameter.KEYWORD_ONLY, + default=kwonlydefaults.get(kwonlyarg, Parameter.empty), + annotation=annotations.get(kwonlyarg, Parameter.empty), + ) for kwonlyarg in kwonlyargs + ) + if varkw: + parameters.append(Parameter(varkw, Parameter.VAR_KEYWORD)) + return_annotation = annotations.get('return', Signature.empty) + return str(Signature(parameters, return_annotation=return_annotation)) try: from inspect import signature except ImportError: diff --git a/tests/test_formatargspec_py35.py b/tests/test_formatargspec_py35.py new file mode 100644 index 00000000..df607ba1 --- /dev/null +++ b/tests/test_formatargspec_py35.py @@ -0,0 +1,35 @@ +import unittest +import sys +from inspect import getfullargspec + +from wrapt.decorators import formatargspec + +class TestFormatargspec35(unittest.TestCase): + + def assertFormatEqual(self, func, ref): + formatted = formatargspec(*getfullargspec(func)) + self.assertEqual(formatted, ref) + + def test_formatargspec(self): + def foo1(): pass + self.assertFormatEqual(foo1, '()') + + def foo2(a, b='c'): pass + self.assertFormatEqual(foo2, ("(a, b='c')")) + + def foo3(a, b, *args, **kwargs): pass + self.assertFormatEqual(foo3, '(a, b, *args, **kwargs)') + + def foo4(a: int, b) -> list: pass + if sys.version_info[:2] < (3, 7): + formatted4 = '(a:int, b) -> list' + else: + formatted4 = '(a: int, b) -> list' + self.assertFormatEqual(foo4, formatted4) + + # examples from https://www.python.org/dev/peps/pep-3102/ + def sortwords(*wordlist, case_sensitive=False): pass + self.assertFormatEqual(sortwords, '(*wordlist, case_sensitive=False)') + + def compare(a, b, *, key=None): pass + self.assertFormatEqual(compare, '(a, b, *, key=None)') diff --git a/tests/test_formatargspec_py38.py b/tests/test_formatargspec_py38.py new file mode 100644 index 00000000..5a6a93e6 --- /dev/null +++ b/tests/test_formatargspec_py38.py @@ -0,0 +1,37 @@ +import unittest +import sys +from inspect import getfullargspec + +from wrapt.decorators import formatargspec + +class TestFormatargspec38(unittest.TestCase): + + def assertFormatEqual(self, func, ref): + formatted = formatargspec(*getfullargspec(func)) + self.assertEqual(formatted, ref) + + def test_formatargspec(self): + # exemples from https://www.python.org/dev/peps/pep-0570/ + def name1(p1, p2, /, p_or_kw, *, kw): pass + self.assertFormatEqual(name1, '(p1, p2, p_or_kw, *, kw)') + + def name2(p1, p2=None, /, p_or_kw=None, *, kw): pass + self.assertFormatEqual(name2, '(p1, p2=None, p_or_kw=None, *, kw)') + + def name3(p1, p2=None, /, *, kw): pass + self.assertFormatEqual(name3, '(p1, p2=None, *, kw)') + + def name4(p1, p2=None, /): pass + self.assertFormatEqual(name4, '(p1, p2=None)') + + def name5(p1, p2, /, p_or_kw): pass + self.assertFormatEqual(name5, '(p1, p2, p_or_kw)') + + def name6(p1, p2, /): pass + self.assertFormatEqual(name6, '(p1, p2)') + + def name7(p_or_kw, *, kw): pass + self.assertFormatEqual(name7, '(p_or_kw, *, kw)') + + def name8(*, kw): pass + self.assertFormatEqual(name8, '(*, kw)') From a1a06529f41cffcc9fd5132fcf1923ea538f3d3c Mon Sep 17 00:00:00 2001 From: odidev Date: Fri, 4 Mar 2022 12:23:01 +0500 Subject: [PATCH 09/15] Add Linux AArch64 wheel build support Signed-off-by: odidev --- .github/workflows/main.yml | 48 +++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0af078d9..b86adbbb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,6 +42,41 @@ jobs: name: coverage path: .coverage.* + test_aarch64_linux: + name: Test (${{ matrix.python.os }}, ${{ matrix.python.python-version }}, aarch64) + runs-on: ${{ matrix.python.os }} + strategy: + matrix: + python: + - {os: ubuntu-latest, python-version: 3.6, pyver: py36} + - {os: ubuntu-latest, python-version: 3.7, pyver: py37} + - {os: ubuntu-latest, python-version: 3.8, pyver: py38} + - {os: ubuntu-latest, python-version: 3.9, pyver: py39} + - {os: ubuntu-latest, python-version: "3.10", pyver: py310} + env: + py: python${{ matrix.python.python-version }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up QEMU + id: qemu + uses: docker/setup-qemu-action@v1 + - name: Test with tox + run: | + docker run --rm -v ${{ github.workspace }}:/io:rw --workdir=/io \ + arm64v8/ubuntu \ + bash -exc 'apt-get update && \ + apt install software-properties-common -y && \ + add-apt-repository ppa:deadsnakes/ppa -y && \ + apt install -y ${{ env.py }} && \ + apt install -y ${{ env.py }}-venv && \ + ${{ env.py }} -m venv .env && \ + source .env/bin/activate && \ + pip install -U pip wheel setuptools && \ + pip install tox tox-gh-actions && \ + tox -e ${{ matrix.python.pyver }} && \ + deactivate' + test_macos: name: Test (${{ matrix.os }}, ${{ matrix.python-version }}) runs-on: ${{ matrix.os }} @@ -187,9 +222,10 @@ jobs: path: dist/*.whl bdist_wheel: - name: Build wheels (3.6+) on ${{ matrix.os }} + name: Build wheels (3.6+) on ${{ matrix.os }} for ${{ matrix.arch }} needs: - test_linux + - test_aarch64_linux - test_macos - test_windows_py27 - test_windows @@ -197,8 +233,17 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] + arch: [auto] + include: + - os: ubuntu-latest + arch: aarch64 + - os: macos-latest + arch: arm64 steps: - uses: actions/checkout@v2 + - name: Set up QEMU + if: ${{ matrix.arch == 'aarch64' }} + uses: docker/setup-qemu-action@v1 - name: Build wheels uses: pypa/cibuildwheel@v2.2.0 with: @@ -207,6 +252,7 @@ jobs: WRAPT_INSTALL_EXTENSIONS: true CIBW_SKIP: pp* CIBW_BUILD_VERBOSITY: 1 + CIBW_ARCHS: ${{ matrix.arch }} - uses: actions/upload-artifact@v2 with: name: dist From 147257537f1233759dd89a291d6e40b95450ed5e Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 5 Mar 2022 11:41:54 +1100 Subject: [PATCH 10/15] Increment version to 1.14.0dev1 for new changes. --- 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 934bf868..54d358d0 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('1', '13', '3') +__version_info__ = ('1', '14', '0dev1') __version__ = '.'.join(__version_info__) from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper, From 2f141c969ce74bb4fb3da9cf66ed48183a8eb17a Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 5 Mar 2022 13:39:08 +1100 Subject: [PATCH 11/15] Relocate formatargspec() standing and update wheel builds. --- .github/workflows/main.yml | 2 +- docs/changes.rst | 16 ++++++++++++++ src/wrapt/__init__.py | 11 +++++++++ src/wrapt/arguments.py | 38 ++++++++++++++++++++++++++++++++ src/wrapt/decorators.py | 34 ++-------------------------- tests/conftest.py | 2 ++ tests/test_formatargspec_py35.py | 2 +- tests/test_formatargspec_py38.py | 2 +- 8 files changed, 72 insertions(+), 35 deletions(-) create mode 100644 src/wrapt/arguments.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bc5f409f..f31e98df 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -245,7 +245,7 @@ jobs: if: ${{ matrix.arch == 'aarch64' }} uses: docker/setup-qemu-action@v1 - name: Build wheels - uses: pypa/cibuildwheel@v2.2.0 + uses: pypa/cibuildwheel@v2.3.1 with: output-dir: dist env: diff --git a/docs/changes.rst b/docs/changes.rst index 261174c0..1dd17741 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,22 @@ Release Notes ============= +Version 1.14.0 +-------------- + +**Bugs Fixed** + +* Python 3.11 dropped `inspect.formatargspec()` which was used in creating + signature changing decorators. Now bundling a version of this function + which uses `Parameter` and `Signature` from `inspect` module when available. + The replacement function is exposed as `wrapt.formatargspec()` if need it + for your own code. + +**New Features** + +* Binary wheels provided on PyPi for `aarch64` Linux systems and macOS + native silicon where supported by Python when using `pypa/cibuildwheel`. + Version 1.13.3 -------------- diff --git a/src/wrapt/__init__.py b/src/wrapt/__init__.py index 54d358d0..11314891 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -13,4 +13,15 @@ from .importer import (register_post_import_hook, when_imported, notify_module_loaded, discover_post_import_hooks) +# Import of inspect.getcallargs() included for backward compatibility. An +# implementation of this was previously bundled and made available here for +# Python <2.7. Avoid using this in future. + from inspect import getcallargs + +# Variant of inspect.formatargspec() included here for forward compatibility. +# This is being done because Python 3.11 dropped inspect.formatargspec() but +# code for handling signature changing decorators relied on it. Exposing the +# bundled implementation here in case any user of wrapt was also needing it. + +from .arguments import formatargspec diff --git a/src/wrapt/arguments.py b/src/wrapt/arguments.py new file mode 100644 index 00000000..032bc059 --- /dev/null +++ b/src/wrapt/arguments.py @@ -0,0 +1,38 @@ +# The inspect.formatargspec() function was dropped in Python 3.11 but we need +# need it for when constructing signature changing decorators based on result of +# inspect.getargspec() or inspect.getfullargspec(). The code here implements +# inspect.formatargspec() base on Parameter and Signature from inspect module, +# which were added in Python 3.6. Thanks to Cyril Jouve for the implementation. + +try: + from inspect import Parameter, Signature +except ImportError: + from inspect import formatargspec +else: + def formatargspec(args, varargs=None, varkw=None, defaults=None, + kwonlyargs=(), kwonlydefaults={}, annotations={}): + if kwonlydefaults is None: + kwonlydefaults = {} + ndefaults = len(defaults) if defaults else 0 + parameters = [ + Parameter( + arg, + Parameter.POSITIONAL_OR_KEYWORD, + default=defaults[i] if i >= 0 else Parameter.empty, + annotation=annotations.get(arg, Parameter.empty), + ) for i, arg in enumerate(args, ndefaults - len(args)) + ] + if varargs: + parameters.append(Parameter(varargs, Parameter.VAR_POSITIONAL)) + parameters.extend( + Parameter( + kwonlyarg, + Parameter.KEYWORD_ONLY, + default=kwonlydefaults.get(kwonlyarg, Parameter.empty), + annotation=annotations.get(kwonlyarg, Parameter.empty), + ) for kwonlyarg in kwonlyargs + ) + if varkw: + parameters.append(Parameter(varkw, Parameter.VAR_KEYWORD)) + return_annotation = annotations.get('return', Signature.empty) + return str(Signature(parameters, return_annotation=return_annotation)) \ No newline at end of file diff --git a/src/wrapt/decorators.py b/src/wrapt/decorators.py index a92e7e6f..8b6e6751 100644 --- a/src/wrapt/decorators.py +++ b/src/wrapt/decorators.py @@ -34,38 +34,8 @@ def exec_(_code_, _globs_=None, _locs_=None): from inspect import isclass from threading import Lock, RLock -try: - from inspect import Parameter, Signature -except ImportError: # < py3.5 - from inspect import formatargspec -else: - def formatargspec(args, varargs=None, varkw=None, defaults=None, - kwonlyargs=(), kwonlydefaults={}, annotations={}): - if kwonlydefaults is None: - kwonlydefaults = {} - ndefaults = len(defaults) if defaults else 0 - parameters = [ - Parameter( - arg, - Parameter.POSITIONAL_OR_KEYWORD, - default=defaults[i] if i >= 0 else Parameter.empty, - annotation=annotations.get(arg, Parameter.empty), - ) for i, arg in enumerate(args, ndefaults - len(args)) - ] - if varargs: - parameters.append(Parameter(varargs, Parameter.VAR_POSITIONAL)) - parameters.extend( - Parameter( - kwonlyarg, - Parameter.KEYWORD_ONLY, - default=kwonlydefaults.get(kwonlyarg, Parameter.empty), - annotation=annotations.get(kwonlyarg, Parameter.empty), - ) for kwonlyarg in kwonlyargs - ) - if varkw: - parameters.append(Parameter(varkw, Parameter.VAR_KEYWORD)) - return_annotation = annotations.get('return', Signature.empty) - return str(Signature(parameters, return_annotation=return_annotation)) +from .arguments import formatargspec + try: from inspect import signature except ImportError: diff --git a/tests/conftest.py b/tests/conftest.py index dcb04b98..f5f73179 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,8 @@ def pytest_pycollect_makemodule(path, parent): return construct_dummy(path, parent) if '_py310' in path.basename and version < (3, 10): return construct_dummy(path, parent) + if '_py311' in path.basename and version < (3, 11): + return construct_dummy(path, parent) if '_py3' in path.basename and version < (3, 0): return construct_dummy(path, parent) if '_py2' in path.basename and version >= (3, 0): diff --git a/tests/test_formatargspec_py35.py b/tests/test_formatargspec_py35.py index df607ba1..6e47507e 100644 --- a/tests/test_formatargspec_py35.py +++ b/tests/test_formatargspec_py35.py @@ -2,7 +2,7 @@ import sys from inspect import getfullargspec -from wrapt.decorators import formatargspec +from wrapt import formatargspec class TestFormatargspec35(unittest.TestCase): diff --git a/tests/test_formatargspec_py38.py b/tests/test_formatargspec_py38.py index 5a6a93e6..0d4cfb79 100644 --- a/tests/test_formatargspec_py38.py +++ b/tests/test_formatargspec_py38.py @@ -2,7 +2,7 @@ import sys from inspect import getfullargspec -from wrapt.decorators import formatargspec +from wrapt import formatargspec class TestFormatargspec38(unittest.TestCase): From 9399d7cacb373f3548d5106351fdacd6c9cc1404 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 5 Mar 2022 15:07:54 +1100 Subject: [PATCH 12/15] Clean up C version of instance check and add change note. --- docs/changes.rst | 6 ++++++ src/wrapt/_wrappers.c | 9 +++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1dd17741..06946e60 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,12 @@ Version 1.14.0 The replacement function is exposed as `wrapt.formatargspec()` if need it for your own code. +* When using a decorator on a class, `isinstance()` checks wouldn't previously + work as expected and you had to manually use `Type.__wrapped__` to access + the real type when doing instance checks. The `__instancecheck__` hook is now + implemented such that you don't have to use `Type.__wrapped__` instead of + `Type` as last argument to `isinstance()`. + **New Features** * Binary wheels provided on PyPi for `aarch64` Linux systems and macOS diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index 575bdaa8..67c5d5e1 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -2551,21 +2551,18 @@ static PyObject *WraptFunctionWrapperBase_instancecheck( PyObject *result = NULL; int check = 0; + if (!self->object_proxy.wrapped) { PyErr_SetString(PyExc_ValueError, "wrapper has not been initialized"); return NULL; } - object = PyObject_GetAttrString(self, "__wrapped__"); - if (!object) { - PyErr_Clear(); - } + check = PyObject_IsInstance(instance, self->object_proxy.wrapped); - check = PyObject_IsInstance(instance, object); - Py_XDECREF(object); if (check < 0) { return NULL; } + result = check ? Py_True : Py_False; Py_INCREF(result); From 53323ba4b92f88dab5a8ec3262a1c1f1e979a3e2 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 5 Mar 2022 16:19:25 +1100 Subject: [PATCH 13/15] Use new Python module import system hooks to eliminate deprecation warnings. --- docs/changes.rst | 4 +++ src/wrapt/importer.py | 65 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 06946e60..fd519c5f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,6 +18,10 @@ Version 1.14.0 implemented such that you don't have to use `Type.__wrapped__` instead of `Type` as last argument to `isinstance()`. +* Eliminated deprecation warnings related to Python module import system, which + would have turned into broken code in Python 3.12. This was used by the post + import hook mechanism. + **New Features** * Binary wheels provided on PyPi for `aarch64` Linux systems and macOS diff --git a/src/wrapt/importer.py b/src/wrapt/importer.py index 2c9db6fb..755029c1 100644 --- a/src/wrapt/importer.py +++ b/src/wrapt/importer.py @@ -10,9 +10,10 @@ if PY2: string_types = basestring, + find_spec = None else: - import importlib string_types = str, + from importlib.util import find_spec from .decorators import synchronized @@ -158,6 +159,16 @@ def load_module(self, fullname): return module + # 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 exec_module(self, module): + self.loader.exec_module(module) + notify_module_loaded(module) + class ImportHookFinder: def __init__(self): @@ -187,7 +198,7 @@ def find_module(self, fullname, path=None): # Now call back into the import system again. try: - if PY2: + if not find_spec: # For Python 2 we don't have much choice but to # call back in to __import__(). This will # actually cause the module to be imported. If no @@ -208,14 +219,52 @@ def find_module(self, fullname, path=None): # our own loader which will then in turn call the # real loader to import the module and invoke the # post import hooks. - try: - import importlib.util - loader = importlib.util.find_spec(fullname).loader - except (ImportError, AttributeError): - loader = importlib.find_loader(fullname, path) - if loader: + + loader = getattr(find_spec(fullname), "loader", None) + + if loader and not isinstance(loader, _ImportHookChainedLoader): return _ImportHookChainedLoader(loader) + finally: + del self.in_progress[fullname] + + def find_spec(self, fullname, path=None, target=None): + # Since Python 3.4, you are meant to implement find_spec() method + # instead of find_module() and since Python 3.10 you get deprecation + # warnings if you don't define find_spec(). + + # If the module being imported is not one we have registered + # post import hooks for, we can return immediately. We will + # take no further part in the importing of this module. + + if not fullname in _post_import_hooks: + return None + + # When we are interested in a specific module, we will call back + # into the import system a second time to defer to the import + # finder that is supposed to handle the importing of the module. + # We set an in progress flag for the target module so that on + # the second time through we don't trigger another call back + # into the import system and cause a infinite loop. + + if fullname in self.in_progress: + return None + + self.in_progress[fullname] = True + + # Now call back into the import system again. + + try: + # This should only be Python 3 so find_spec() should always + # exist so don't need to check. + + spec = find_spec(fullname) + loader = getattr(spec, "loader", None) + + if loader and not isinstance(loader, _ImportHookChainedLoader): + spec.loader = _ImportHookChainedLoader(loader) + + return spec finally: del self.in_progress[fullname] From 8f180bf981fc7a92094cfecfd7a9e5f591d4bd4b Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 5 Mar 2022 16:44:33 +1100 Subject: [PATCH 14/15] Update version to 1.14.0rc1 to do a pre 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 11314891..2061f764 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('1', '14', '0dev1') +__version_info__ = ('1', '14', '0rc1') __version__ = '.'.join(__version_info__) from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper, From 7e2a14079d9b3112371facf3e31e20600e0136d4 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 10 Mar 2022 17:46:33 +1100 Subject: [PATCH 15/15] Increment version to 1.14.0 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 2061f764..aaab4ac0 100644 --- a/src/wrapt/__init__.py +++ b/src/wrapt/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('1', '14', '0rc1') +__version_info__ = ('1', '14', '0') __version__ = '.'.join(__version_info__) from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper,