Skip to content

Commit dcf18b4

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 c7ec459 commit dcf18b4

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

149149

150-
# static version of getattr backported from Python 3.7
151-
try:
152-
from inspect import getattr_static
153-
except ImportError:
154-
import types
155-
156-
_sentinel = object()
157-
158-
def _static_getmro(klass):
159-
return type.__dict__["__mro__"].__get__(klass)
160-
161-
def _check_instance(obj, attr):
162-
instance_dict = {}
163-
try:
164-
instance_dict = object.__getattribute__(obj, "__dict__")
165-
except AttributeError:
166-
pass
167-
return dict.get(instance_dict, attr, _sentinel)
168-
169-
def _check_class(klass, attr):
170-
for entry in _static_getmro(klass):
171-
if _shadowed_dict(type(entry)) is _sentinel:
172-
try:
173-
return entry.__dict__[attr]
174-
except KeyError:
175-
pass
176-
return _sentinel
177-
178-
def _is_type(obj):
179-
try:
180-
_static_getmro(obj)
181-
except TypeError:
182-
return False
183-
return True
184-
185-
def _shadowed_dict(klass):
186-
dict_attr = type.__dict__["__dict__"]
187-
for entry in _static_getmro(klass):
188-
try:
189-
class_dict = dict_attr.__get__(entry)["__dict__"]
190-
except KeyError:
191-
pass
192-
else:
193-
if not (
194-
type(class_dict) is types.GetSetDescriptorType # noqa: E721
195-
and class_dict.__name__ == "__dict__" # noqa: E721,E261,W504
196-
and class_dict.__objclass__ is entry # noqa: E261,W504
197-
):
198-
return class_dict
199-
return _sentinel
200-
201-
def getattr_static(obj, attr, default=_sentinel):
202-
"""Retrieve attributes without triggering dynamic lookup via the
203-
descriptor protocol, __getattr__ or __getattribute__.
204-
205-
Note: this function may not be able to retrieve all attributes
206-
that getattr can fetch (like dynamically created attributes)
207-
and may find attributes that getattr can't (like descriptors
208-
that raise AttributeError). It can also return descriptor objects
209-
instead of instance members in some cases. See the
210-
documentation for details.
211-
"""
212-
instance_result = _sentinel
213-
if not _is_type(obj):
214-
klass = type(obj)
215-
dict_attr = _shadowed_dict(klass)
216-
if dict_attr is _sentinel or type(dict_attr) is types.MemberDescriptorType: # noqa: E261,E721,W504
217-
instance_result = _check_instance(obj, attr)
218-
else:
219-
klass = obj
220-
221-
klass_result = _check_class(klass, attr)
222-
223-
if instance_result is not _sentinel and klass_result is not _sentinel:
224-
if (
225-
_check_class(type(klass_result), "__get__") is not _sentinel
226-
and _check_class(type(klass_result), "__set__") is not _sentinel # noqa: W504,E261,E721
227-
):
228-
return klass_result
229-
230-
if instance_result is not _sentinel:
231-
return instance_result
232-
if klass_result is not _sentinel:
233-
return klass_result
234-
235-
if obj is klass:
236-
# for types we check the metaclass too
237-
for entry in _static_getmro(type(klass)):
238-
if _shadowed_dict(type(entry)) is _sentinel:
239-
try:
240-
return entry.__dict__[attr]
241-
except KeyError:
242-
pass
243-
if default is not _sentinel:
244-
return default
245-
raise AttributeError(attr)
246-
247-
248150
# DEV: There is `six.u()` which does something similar, but doesn't have the guard around `hasattr(s, 'decode')`
249151
def to_unicode(s):
250152
""" 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
@@ -446,17 +445,13 @@ def instrument_view(django, view):
446445
for name in list(http_method_names) + list(lifecycle_methods):
447446
try:
448447
# View methods can be staticmethods
449-
func = getattr_static(view, name, None)
448+
func = getattr(view, name, None)
450449
if not func or isinstance(func, wrapt.ObjectProxy):
451450
continue
452451

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

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)