Skip to content

Commit

Permalink
fix: update wrapt to 1.14.1 to avoid issues with ImportFinder on Pyth…
Browse files Browse the repository at this point in the history
…on 3.10 (backport #3789) (#3813)

* fix: update wrapt to 1.14.1 to avoid issues with ImportFinder on Python 3.10 (#3789)

Changelog: https://github.com/GrahamDumpleton/wrapt/blob/f2f1a680113d500f525de78da91ae19235efef16/docs/changes.rst

Updating to address issue with new Python requiring find_spec for import hooks

## Reproduction

Running the Starlette test suite with Python 3.10.2 as we do for it's framework test suite:

```shell
pytest --ddtrace-patch-all tests -k 'not test_request_headers and not test_subdomain_route and not test_websocket_headers and not test_staticfiles_with_invalid_dir_permissions_returns_401'
```

Produces the following error:

```
_____________________________________________________________________________________________________________ test_wsgi_exc_info[asyncio] _____________________________________________________________________________________________________________

name = 'netrc', path = None, target = None

>   ???
E   AttributeError: 'ImportHookFinder' object has no attribute 'find_spec'

<frozen importlib._bootstrap>:939: AttributeError

During handling of the above exception, another exception occurred:

test_client_factory = functools.partial(<class 'starlette.testclient.TestClient'>, backend='asyncio', backend_options={})

>   ???

/src/starlette/tests/middleware/test_wsgi.py:79:
```

This is because `wrapt`s `ImportHookFinder` did not define a `find_spec` method which was not required up until newer versions of Python.

## Checklist
- [ ] Library documentation is updated.
- [ ] [Corp site](https://github.com/DataDog/documentation/) documentation is updated (link to the PR).

## Reviewer Checklist
- [x] Title is accurate.
- [ ] Description motivates each change.
- [x] No unnecessary changes were introduced in this PR.
- [x] PR cannot be broken up into smaller PRs.
- [x] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary.
- [ ] Tests provided or description of manual testing performed is included in the code or PR.
- [x] Release note has been added for fixes and features, or else `changelog/no-changelog` label added.
- [ ] All relevant GitHub issues are correctly linked.
- [ ] Backports are identified and tagged with Mergifyio.
- [ ] Add to milestone.

(cherry picked from commit 5bd3f79)

* add release note

Co-authored-by: Brett Langdon <brett.langdon@datadoghq.com>
Co-authored-by: Kyle Verhoog <kyle@verhoog.ca>
  • Loading branch information
3 people committed Jun 27, 2022
1 parent 7a1c280 commit a1c377a
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 52 deletions.
2 changes: 1 addition & 1 deletion ddtrace/vendor/__init__.py
Expand Up @@ -13,7 +13,7 @@
Website: https://wrapt.readthedocs.io/en/latest/
Source: https://github.com/GrahamDumpleton/wrapt/
Version: 1.12.1
Version: 1.14.1
License: BSD 2-Clause "Simplified" License
Notes:
Expand Down
4 changes: 2 additions & 2 deletions ddtrace/vendor/wrapt/LICENSE
@@ -1,4 +1,4 @@
Copyright (c) 2013-2019, Graham Dumpleton
Copyright (c) 2013-2022, Graham Dumpleton
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand All @@ -21,4 +21,4 @@ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
POSSIBILITY OF SUCH DAMAGE.
13 changes: 12 additions & 1 deletion ddtrace/vendor/wrapt/__init__.py
@@ -1,4 +1,4 @@
__version_info__ = ('1', '12', '1')
__version_info__ = ('1', '14', '1')
__version__ = '.'.join(__version_info__)

from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper,
Expand All @@ -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
124 changes: 118 additions & 6 deletions ddtrace/vendor/wrapt/_wrappers.c
Expand Up @@ -1961,21 +1961,21 @@ static int WraptPartialCallableObjectProxy_init(

if (!PyObject_Length(args)) {
PyErr_SetString(PyExc_TypeError,
"__init__ of partial needs an argument");
"__init__ of partial needs an argument");
return -1;
}

if (PyObject_Length(args) < 1) {
PyErr_SetString(PyExc_TypeError,
"partial type takes at least one argument");
"partial type takes at least one argument");
return -1;
}

wrapped = PyTuple_GetItem(args, 0);

if (!PyCallable_Check(wrapped)) {
PyErr_SetString(PyExc_TypeError,
"the first argument must be callable");
"the first argument must be callable");
return -1;
}

Expand All @@ -1985,7 +1985,7 @@ static int WraptPartialCallableObjectProxy_init(
return -1;

result = WraptPartialCallableObjectProxy_raw_init(self, wrapped,
fnargs, kwds);
fnargs, kwds);

Py_DECREF(fnargs);

Expand Down Expand Up @@ -2299,12 +2299,15 @@ static PyObject *WraptFunctionWrapperBase_call(
PyObject *result = NULL;

static PyObject *function_str = NULL;
static PyObject *classmethod_str = NULL;

if (!function_str) {
#if PY_MAJOR_VERSION >= 3
function_str = PyUnicode_InternFromString("function");
classmethod_str = PyUnicode_InternFromString("classmethod");
#else
function_str = PyString_InternFromString("function");
classmethod_str = PyString_InternFromString("classmethod");
#endif
}

Expand Down Expand Up @@ -2334,8 +2337,10 @@ static PyObject *WraptFunctionWrapperBase_call(
kwds = param_kwds;
}

if (self->instance == Py_None && (self->binding == function_str ||
if ((self->instance == Py_None) && (self->binding == function_str ||
PyObject_RichCompareBool(self->binding, function_str,
Py_EQ) == 1 || self->binding == classmethod_str ||
PyObject_RichCompareBool(self->binding, classmethod_str,
Py_EQ) == 1)) {

PyObject *instance = NULL;
Expand Down Expand Up @@ -2510,6 +2515,101 @@ static PyObject *WraptFunctionWrapperBase_descr_get(

/* ------------------------------------------------------------------------- */

static PyObject *WraptFunctionWrapperBase_set_name(
WraptFunctionWrapperObject *self, PyObject *args, PyObject *kwds)
{
PyObject *method = NULL;
PyObject *result = NULL;

if (!self->object_proxy.wrapped) {
PyErr_SetString(PyExc_ValueError, "wrapper has not been initialized");
return NULL;
}

method = PyObject_GetAttrString(self->object_proxy.wrapped,
"__set_name__");

if (!method) {
PyErr_Clear();
Py_INCREF(Py_None);
return Py_None;
}

result = PyObject_Call(method, args, kwds);

Py_DECREF(method);

return result;
}

/* ------------------------------------------------------------------------- */

static PyObject *WraptFunctionWrapperBase_instancecheck(
WraptFunctionWrapperObject *self, PyObject *instance)
{
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;
}

check = PyObject_IsInstance(instance, self->object_proxy.wrapped);

if (check < 0) {
return NULL;
}

result = check ? Py_True : Py_False;

Py_INCREF(result);
return result;
}

/* ------------------------------------------------------------------------- */

static PyObject *WraptFunctionWrapperBase_subclasscheck(
WraptFunctionWrapperObject *self, PyObject *args)
{
PyObject *subclass = 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", &subclass))
return NULL;

object = PyObject_GetAttrString(subclass, "__wrapped__");

if (!object)
PyErr_Clear();

check = PyObject_IsSubclass(object ? object: subclass,
self->object_proxy.wrapped);

Py_XDECREF(object);

if (check == -1)
return NULL;

result = check ? Py_True : Py_False;

Py_INCREF(result);

return result;
}

/* ------------------------------------------------------------------------- */

static PyObject *WraptFunctionWrapperBase_get_self_instance(
WraptFunctionWrapperObject *self, void *closure)
{
Expand Down Expand Up @@ -2580,6 +2680,18 @@ 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_O, 0},
{ "__subclasscheck__", (PyCFunction)WraptFunctionWrapperBase_subclasscheck,
METH_VARARGS, 0 },
{ NULL, NULL },
};

/* ------------------------------------------------------------------------- */;

static PyGetSetDef WraptFunctionWrapperBase_getset[] = {
{ "__module__", (getter)WraptObjectProxy_get_module,
(setter)WraptObjectProxy_set_module, 0 },
Expand Down Expand Up @@ -2633,7 +2745,7 @@ PyTypeObject WraptFunctionWrapperBase_Type = {
offsetof(WraptObjectProxyObject, weakreflist), /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
0, /*tp_methods*/
WraptFunctionWrapperBase_methods, /*tp_methods*/
0, /*tp_members*/
WraptFunctionWrapperBase_getset, /*tp_getset*/
0, /*tp_base*/
Expand Down
38 changes: 38 additions & 0 deletions ddtrace/vendor/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))
43 changes: 34 additions & 9 deletions ddtrace/vendor/wrapt/decorators.py
Expand Up @@ -31,10 +31,11 @@ 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

from .arguments import formatargspec

try:
from inspect import signature
except ImportError:
Expand Down Expand Up @@ -173,7 +174,7 @@ def __call__(self, wrapped):
# function so the wrapper is effectively indistinguishable from the
# original wrapped function.

def decorator(wrapper=None, enabled=None, adapter=None):
def decorator(wrapper=None, enabled=None, adapter=None, proxy=FunctionWrapper):
# The decorator should be supplied with a single positional argument
# which is the wrapper function to be used to implement the
# decorator. This may be preceded by a step whereby the keyword
Expand All @@ -183,7 +184,7 @@ def decorator(wrapper=None, enabled=None, adapter=None):
# decorator. In that case parts of the function '__code__' and
# '__defaults__' attributes are used from the adapter function
# rather than those of the wrapped function. This allows for the
# argument specification from inspect.getargspec() and similar
# argument specification from inspect.getfullargspec() and similar
# functions to be overridden with a prototype for a different
# function than what was wrapped. The 'enabled' argument provides a
# way to enable/disable the use of the decorator. If the type of
Expand All @@ -194,6 +195,8 @@ def decorator(wrapper=None, enabled=None, adapter=None):
# if 'enabled' is callable it will be called to obtain the value to
# be checked. If False, the wrapper will not be called and instead
# the original wrapped function will be called directly instead.
# The 'proxy' argument provides a way of passing a custom version of
# the FunctionWrapper class used in decorating the function.

if wrapper is not None:
# Helper function for creating wrapper of the appropriate
Expand All @@ -206,16 +209,37 @@ def _build(wrapped, wrapper, enabled=None, adapter=None):

if not callable(adapter):
ns = {}

# Check if the signature argument specification has
# annotations. If it does then we need to remember
# it but also drop it when attempting to manufacture
# a standin adapter function. This is necessary else
# it will try and look up any types referenced in
# the annotations in the empty namespace we use,
# which will fail.

annotations = {}

if not isinstance(adapter, string_types):
if len(adapter) == 7:
annotations = adapter[-1]
adapter = adapter[:-1]
adapter = formatargspec(*adapter)

exec_('def adapter{}: pass'.format(adapter), ns, ns)
adapter = ns['adapter']

# Override the annotations for the manufactured
# adapter function so they match the original
# adapter signature argument specification.

if annotations:
adapter.__annotations__ = annotations

return AdapterWrapper(wrapped=wrapped, wrapper=wrapper,
enabled=enabled, adapter=adapter)

return FunctionWrapper(wrapped=wrapped, wrapper=wrapper,
enabled=enabled)
return proxy(wrapped=wrapped, wrapper=wrapper, enabled=enabled)

# The wrapper has been provided so return the final decorator.
# The decorator is itself one of our function wrappers so we
Expand Down Expand Up @@ -360,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__(None, instance)
Expand All @@ -384,7 +408,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))
Expand All @@ -408,7 +432,8 @@ def _capture(target_wrapped):
# decorator again wrapped in a partial using the collected
# arguments.

return partial(decorator, enabled=enabled, adapter=adapter)
return partial(decorator, enabled=enabled, adapter=adapter,
proxy=proxy)

# Decorator for implementing thread synchronization. It can be used as a
# decorator, in which case the synchronization context is determined by
Expand Down

0 comments on commit a1c377a

Please sign in to comment.