Skip to content

Commit 3917cc5

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 cc7a11c commit 3917cc5

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
@@ -445,17 +444,13 @@ def instrument_view(django, view):
445444
for name in list(http_method_names) + list(lifecycle_methods):
446445
try:
447446
# View methods can be staticmethods
448-
func = getattr_static(view, name, None)
447+
func = getattr(view, name, None)
449448
if not func or isinstance(func, wrapt.ObjectProxy):
450449
continue
451450

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

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
@@ -46,4 +46,6 @@ def authenticated_view(request):
4646
re_path(r"re-path.*/", repath_view),
4747
path("path/", path_view),
4848
path("include/", include("tests.contrib.django.django_app.extra_urls")),
49+
url(r"^composed-template-view/$", views.ComposedTemplateView.as_view(), name="composed-template-view"),
50+
url(r"^composed-get-view/$", views.ComposedGetView.as_view(), name="composed-get-view"),
4951
]
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
@@ -1259,3 +1259,36 @@ def test_user_name_excluded(client, test_spans):
12591259
root = test_spans.get_root_span()
12601260
assert "django.user.name" not in root.meta
12611261
assert root.meta.get("django.user.is_authenticated") == "True"
1262+
1263+
1264+
def test_custom_dispatch_template_view(client, test_spans):
1265+
"""
1266+
Test that a template view with a custom dispatch method inherited from a
1267+
mixin is called.
1268+
"""
1269+
resp = client.get("/composed-template-view/")
1270+
assert resp.status_code == 200
1271+
assert resp.content.strip() == b"custom dispatch 2"
1272+
1273+
spans = test_spans.get_spans()
1274+
assert [s.resource for s in spans if s.resource.endswith("dispatch")] == [
1275+
"tests.contrib.django.views.ComposedTemplateView.dispatch",
1276+
]
1277+
1278+
1279+
def test_custom_dispatch_get_view(client, test_spans):
1280+
"""
1281+
Test that a get method on a view with a custom dispatch method inherited
1282+
from a mixin is called.
1283+
"""
1284+
resp = client.get("/composed-get-view/")
1285+
assert resp.status_code == 200
1286+
assert resp.content.strip() == b"custom get"
1287+
1288+
spans = test_spans.get_spans()
1289+
assert [s.resource for s in spans if s.resource.endswith("dispatch")] == [
1290+
"tests.contrib.django.views.ComposedGetView.dispatch",
1291+
]
1292+
assert [s.resource for s in spans if s.resource.endswith("get")] == [
1293+
"tests.contrib.django.views.ComposedGetView.get",
1294+
]

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)