Skip to content

Commit 74bcb03

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 b9c6b5a commit 74bcb03

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
@@ -117,104 +117,6 @@ def make_async_decorator(tracer, fn, *params, **kw_params):
117117
return fn
118118

119119

120-
# static version of getattr backported from Python 3.7
121-
try:
122-
from inspect import getattr_static
123-
except ImportError:
124-
import types
125-
126-
_sentinel = object()
127-
128-
def _static_getmro(klass):
129-
return type.__dict__["__mro__"].__get__(klass)
130-
131-
def _check_instance(obj, attr):
132-
instance_dict = {}
133-
try:
134-
instance_dict = object.__getattribute__(obj, "__dict__")
135-
except AttributeError:
136-
pass
137-
return dict.get(instance_dict, attr, _sentinel)
138-
139-
def _check_class(klass, attr):
140-
for entry in _static_getmro(klass):
141-
if _shadowed_dict(type(entry)) is _sentinel:
142-
try:
143-
return entry.__dict__[attr]
144-
except KeyError:
145-
pass
146-
return _sentinel
147-
148-
def _is_type(obj):
149-
try:
150-
_static_getmro(obj)
151-
except TypeError:
152-
return False
153-
return True
154-
155-
def _shadowed_dict(klass):
156-
dict_attr = type.__dict__["__dict__"]
157-
for entry in _static_getmro(klass):
158-
try:
159-
class_dict = dict_attr.__get__(entry)["__dict__"]
160-
except KeyError:
161-
pass
162-
else:
163-
if not (
164-
type(class_dict) is types.GetSetDescriptorType # noqa: E721
165-
and class_dict.__name__ == "__dict__" # noqa: E721,E261,W504
166-
and class_dict.__objclass__ is entry # noqa: E261,W504
167-
):
168-
return class_dict
169-
return _sentinel
170-
171-
def getattr_static(obj, attr, default=_sentinel):
172-
"""Retrieve attributes without triggering dynamic lookup via the
173-
descriptor protocol, __getattr__ or __getattribute__.
174-
175-
Note: this function may not be able to retrieve all attributes
176-
that getattr can fetch (like dynamically created attributes)
177-
and may find attributes that getattr can't (like descriptors
178-
that raise AttributeError). It can also return descriptor objects
179-
instead of instance members in some cases. See the
180-
documentation for details.
181-
"""
182-
instance_result = _sentinel
183-
if not _is_type(obj):
184-
klass = type(obj)
185-
dict_attr = _shadowed_dict(klass)
186-
if dict_attr is _sentinel or type(dict_attr) is types.MemberDescriptorType: # noqa: E261,E721,W504
187-
instance_result = _check_instance(obj, attr)
188-
else:
189-
klass = obj
190-
191-
klass_result = _check_class(klass, attr)
192-
193-
if instance_result is not _sentinel and klass_result is not _sentinel:
194-
if (
195-
_check_class(type(klass_result), "__get__") is not _sentinel
196-
and _check_class(type(klass_result), "__set__") is not _sentinel # noqa: W504,E261,E721
197-
):
198-
return klass_result
199-
200-
if instance_result is not _sentinel:
201-
return instance_result
202-
if klass_result is not _sentinel:
203-
return klass_result
204-
205-
if obj is klass:
206-
# for types we check the metaclass too
207-
for entry in _static_getmro(type(klass)):
208-
if _shadowed_dict(type(entry)) is _sentinel:
209-
try:
210-
return entry.__dict__[attr]
211-
except KeyError:
212-
pass
213-
if default is not _sentinel:
214-
return default
215-
raise AttributeError(attr)
216-
217-
218120
# DEV: There is `six.u()` which does something similar, but doesn't have the guard around `hasattr(s, 'decode')`
219121
def to_unicode(s):
220122
""" 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
@@ -13,7 +13,6 @@
1313

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

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

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)