Skip to content

Commit 27163b9

Browse files
Kyle-Verhoogmajorgreys
authored andcommitted
django: reintroduce wrapt for view method patching (#1622)
Since GrahamDumpleton/wrapt#153 was fixed, we can use wrapt again for the view method patching.
1 parent a149adb commit 27163b9

File tree

7 files changed

+76
-105
lines changed

7 files changed

+76
-105
lines changed

ddtrace/compat.py

Lines changed: 0 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -140,104 +140,6 @@ def make_async_decorator(tracer, fn, *params, **kw_params):
140140
return fn
141141

142142

143-
# static version of getattr backported from Python 3.7
144-
try:
145-
from inspect import getattr_static
146-
except ImportError:
147-
import types
148-
149-
_sentinel = object()
150-
151-
def _static_getmro(klass):
152-
return type.__dict__["__mro__"].__get__(klass)
153-
154-
def _check_instance(obj, attr):
155-
instance_dict = {}
156-
try:
157-
instance_dict = object.__getattribute__(obj, "__dict__")
158-
except AttributeError:
159-
pass
160-
return dict.get(instance_dict, attr, _sentinel)
161-
162-
def _check_class(klass, attr):
163-
for entry in _static_getmro(klass):
164-
if _shadowed_dict(type(entry)) is _sentinel:
165-
try:
166-
return entry.__dict__[attr]
167-
except KeyError:
168-
pass
169-
return _sentinel
170-
171-
def _is_type(obj):
172-
try:
173-
_static_getmro(obj)
174-
except TypeError:
175-
return False
176-
return True
177-
178-
def _shadowed_dict(klass):
179-
dict_attr = type.__dict__["__dict__"]
180-
for entry in _static_getmro(klass):
181-
try:
182-
class_dict = dict_attr.__get__(entry)["__dict__"]
183-
except KeyError:
184-
pass
185-
else:
186-
if not (
187-
type(class_dict) is types.GetSetDescriptorType # noqa: E721
188-
and class_dict.__name__ == "__dict__" # noqa: E721,E261,W504
189-
and class_dict.__objclass__ is entry # noqa: E261,W504
190-
):
191-
return class_dict
192-
return _sentinel
193-
194-
def getattr_static(obj, attr, default=_sentinel):
195-
"""Retrieve attributes without triggering dynamic lookup via the
196-
descriptor protocol, __getattr__ or __getattribute__.
197-
198-
Note: this function may not be able to retrieve all attributes
199-
that getattr can fetch (like dynamically created attributes)
200-
and may find attributes that getattr can't (like descriptors
201-
that raise AttributeError). It can also return descriptor objects
202-
instead of instance members in some cases. See the
203-
documentation for details.
204-
"""
205-
instance_result = _sentinel
206-
if not _is_type(obj):
207-
klass = type(obj)
208-
dict_attr = _shadowed_dict(klass)
209-
if dict_attr is _sentinel or type(dict_attr) is types.MemberDescriptorType: # noqa: E261,E721,W504
210-
instance_result = _check_instance(obj, attr)
211-
else:
212-
klass = obj
213-
214-
klass_result = _check_class(klass, attr)
215-
216-
if instance_result is not _sentinel and klass_result is not _sentinel:
217-
if (
218-
_check_class(type(klass_result), "__get__") is not _sentinel
219-
and _check_class(type(klass_result), "__set__") is not _sentinel # noqa: W504,E261,E721
220-
):
221-
return klass_result
222-
223-
if instance_result is not _sentinel:
224-
return instance_result
225-
if klass_result is not _sentinel:
226-
return klass_result
227-
228-
if obj is klass:
229-
# for types we check the metaclass too
230-
for entry in _static_getmro(type(klass)):
231-
if _shadowed_dict(type(entry)) is _sentinel:
232-
try:
233-
return entry.__dict__[attr]
234-
except KeyError:
235-
pass
236-
if default is not _sentinel:
237-
return default
238-
raise AttributeError(attr)
239-
240-
241143
# DEV: There is `six.u()` which does something similar, but doesn't have the guard around `hasattr(s, 'decode')`
242144
def to_unicode(s):
243145
""" Return a unicode string for the given bytes or string instance. """

ddtrace/contrib/django/patch.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
from ddtrace import config, Pin
1414
from ddtrace.vendor import debtcollector, wrapt
15-
from ddtrace.compat import getattr_static
1615
from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
1716
from ddtrace.contrib import func_name, dbapi
1817
from ddtrace.ext import http, sql as sqlx, SpanTypes
@@ -443,17 +442,13 @@ def instrument_view(django, view):
443442
for name in list(http_method_names) + list(lifecycle_methods):
444443
try:
445444
# View methods can be staticmethods
446-
func = getattr_static(view, name, None)
445+
func = getattr(view, name, None)
447446
if not func or isinstance(func, wrapt.ObjectProxy):
448447
continue
449448

450449
resource = "{0}.{1}".format(func_name(view), name)
451450
op_name = "django.view.{0}".format(name)
452-
453-
# Set attribute here rather than using wrapt.wrappers.wrap_function_wrapper
454-
# since it will not resolve attribute to staticmethods
455-
wrapper = wrapt.FunctionWrapper(func, traced_func(django, name=op_name, resource=resource))
456-
setattr(view, name, wrapper)
451+
wrap(view, name, traced_func(django, name=op_name, resource=resource))
457452
except Exception:
458453
log.debug("Failed to instrument Django view %r function %s", view, name, exc_info=True)
459454

tests/contrib/django/django1_app/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@
1717
url(r"^partial-view/$", views.partial_view, name="partial-view"),
1818
url(r"^lambda-view/$", views.lambda_view, name="lambda-view"),
1919
url(r"^error-500/$", views.error_500, name="error-500"),
20+
url(r"^composed-template-view/$", views.ComposedTemplateView.as_view(), name="composed-template-view"),
21+
url(r"^composed-get-view/$", views.ComposedGetView.as_view(), name="composed-get-view"),
2022
]

tests/contrib/django/django_app/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ def path_view(request):
3030
re_path(r"re-path.*/", repath_view),
3131
path("path/", path_view),
3232
path("include/", include("tests.contrib.django.django_app.extra_urls")),
33+
url(r"^composed-template-view/$", views.ComposedTemplateView.as_view(), name="composed-template-view"),
34+
url(r"^composed-get-view/$", views.ComposedGetView.as_view(), name="composed-get-view"),
3335
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
custom dispatch {{ dispatch_call_counter }}

tests/contrib/django/test_django.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,3 +1226,36 @@ def test_urlpatterns_repath(client, test_spans):
12261226

12271227
# Ensure the view was traced
12281228
assert len(list(test_spans.filter_spans(name="django.view"))) == 1
1229+
1230+
1231+
def test_custom_dispatch_template_view(client, test_spans):
1232+
"""
1233+
Test that a template view with a custom dispatch method inherited from a
1234+
mixin is called.
1235+
"""
1236+
resp = client.get("/composed-template-view/")
1237+
assert resp.status_code == 200
1238+
assert resp.content.strip() == b"custom dispatch 2"
1239+
1240+
spans = test_spans.get_spans()
1241+
assert [s.resource for s in spans if s.resource.endswith("dispatch")] == [
1242+
"tests.contrib.django.views.ComposedTemplateView.dispatch",
1243+
]
1244+
1245+
1246+
def test_custom_dispatch_get_view(client, test_spans):
1247+
"""
1248+
Test that a get method on a view with a custom dispatch method inherited
1249+
from a mixin is called.
1250+
"""
1251+
resp = client.get("/composed-get-view/")
1252+
assert resp.status_code == 200
1253+
assert resp.content.strip() == b"custom get"
1254+
1255+
spans = test_spans.get_spans()
1256+
assert [s.resource for s in spans if s.resource.endswith("dispatch")] == [
1257+
"tests.contrib.django.views.ComposedGetView.dispatch",
1258+
]
1259+
assert [s.resource for s in spans if s.resource.endswith("get")] == [
1260+
"tests.contrib.django.views.ComposedGetView.get",
1261+
]

tests/contrib/django/views.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,39 @@ def item_description(self, item):
8282

8383
def index(request):
8484
return HttpResponse("Hello, test app.")
85+
86+
87+
class CustomDispatchMixin(View):
88+
def dispatch(self, request):
89+
self.dispatch_call_counter += 1
90+
return super(CustomDispatchMixin, self).dispatch(request)
91+
92+
93+
class AnotherCustomDispatchMixin(View):
94+
def dispatch(self, request):
95+
self.dispatch_call_counter += 1
96+
return super(AnotherCustomDispatchMixin, self).dispatch(request)
97+
98+
99+
class ComposedTemplateView(TemplateView, CustomDispatchMixin, AnotherCustomDispatchMixin):
100+
template_name = "custom_dispatch.html"
101+
dispatch_call_counter = 0
102+
103+
def get_context_data(self, **kwargs):
104+
context = super(ComposedTemplateView, self).get_context_data(**kwargs)
105+
context["dispatch_call_counter"] = self.dispatch_call_counter
106+
return context
107+
108+
109+
class CustomGetView(View):
110+
def get(self, request):
111+
return HttpResponse("custom get")
112+
113+
114+
class ComposedGetView(CustomGetView, CustomDispatchMixin):
115+
dispatch_call_counter = 0
116+
117+
def get(self, request):
118+
if self.dispatch_call_counter == 1:
119+
return super(ComposedGetView, self).get(request)
120+
raise Exception("Custom dispatch not called.")

0 commit comments

Comments
 (0)