Skip to content

Commit a0bdd48

Browse files
authored
Merge pull request #772 from DataDog/0.18-dev
v0.18.0
2 parents 47ecf1d + 9e49744 commit a0bdd48

File tree

144 files changed

+2865
-1181
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+2865
-1181
lines changed

.circleci/config.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ jobs:
134134
- checkout
135135
- *restore_cache_step
136136
- run: tox -e 'boto_contrib-{py27,py34}-boto' --result-json /tmp/boto.1.results
137-
- run: tox -e 'botocore_contrib-{py27,py34}-botocore' --result-json /tmp/boto.2.results
137+
- run: tox -e 'botocore_contrib-{py27,py34,py35,py36}-botocore' --result-json /tmp/boto.2.results
138138
- persist_to_workspace:
139139
root: /tmp
140140
paths:
@@ -420,6 +420,20 @@ jobs:
420420
- grpc.results
421421
- *save_cache_step
422422

423+
molten:
424+
docker:
425+
- *test_runner
426+
resource_class: *resource_class
427+
steps:
428+
- checkout
429+
- *restore_cache_step
430+
- run: tox -e 'molten_contrib-{py36}-molten{070,072}' --result-json /tmp/molten.results
431+
- persist_to_workspace:
432+
root: /tmp
433+
paths:
434+
- molten.results
435+
- *save_cache_step
436+
423437
mysqlconnector:
424438
docker:
425439
- *test_runner
@@ -985,6 +999,9 @@ workflows:
985999
- kombu:
9861000
requires:
9871001
- flake8
1002+
- molten:
1003+
requires:
1004+
- flake8
9881005
- mongoengine:
9891006
requires:
9901007
- flake8
@@ -1082,6 +1099,7 @@ workflows:
10821099
- integration
10831100
- jinja2
10841101
- kombu
1102+
- molten
10851103
- mongoengine
10861104
- msgpack
10871105
- mysqlconnector

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pip-delete-this-directory.txt
3737
# Unit test / coverage reports
3838
htmlcov/
3939
.tox/
40+
.ddtox/
4041
.coverage
4142
.coverage.*
4243
.cache

conftest.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
This file configures a local pytest plugin, which allows us to configure plugin hooks to control the
3+
execution of our tests. Either by loading in fixtures, configuring directories to ignore, etc
4+
5+
Local plugins: https://docs.pytest.org/en/3.10.1/writing_plugins.html#local-conftest-plugins
6+
Hook reference: https://docs.pytest.org/en/3.10.1/reference.html#hook-reference
7+
"""
8+
import os
9+
import re
10+
import sys
11+
12+
import pytest
13+
14+
PY_DIR_PATTERN = re.compile(r'^py[23][0-9]$')
15+
16+
17+
# Determine if the folder should be ignored
18+
# https://docs.pytest.org/en/3.10.1/reference.html#_pytest.hookspec.pytest_ignore_collect
19+
# DEV: We can only ignore folders/modules, we cannot ignore individual files
20+
# DEV: We must wrap with `@pytest.mark.hookwrapper` to inherit from default (e.g. honor `--ignore`)
21+
# https://github.com/pytest-dev/pytest/issues/846#issuecomment-122129189
22+
@pytest.mark.hookwrapper
23+
def pytest_ignore_collect(path, config):
24+
"""
25+
Skip directories defining a required minimum Python version
26+
27+
Example::
28+
29+
File: tests/contrib/vertica/py35/test.py
30+
Python 2.7: Skip
31+
Python 3.4: Skip
32+
Python 3.5: Collect
33+
Python 3.6: Collect
34+
"""
35+
# Execute original behavior first
36+
# DEV: We need to set `outcome.force_result(True)` if we need to override
37+
# these results and skip this directory
38+
outcome = yield
39+
40+
# Was not ignored by default behavior
41+
if not outcome.get_result():
42+
# DEV: `path` is a `LocalPath`
43+
path = str(path)
44+
if not os.path.isdir(path):
45+
path = os.path.dirname(path)
46+
dirname = os.path.basename(path)
47+
48+
# Directory name match `py[23][0-9]`
49+
if PY_DIR_PATTERN.match(dirname):
50+
# Split out version numbers into a tuple: `py35` -> `(3, 5)`
51+
min_required = tuple((int(v) for v in dirname.strip('py')))
52+
53+
# If the current Python version does not meet the minimum required, skip this directory
54+
if sys.version_info[0:2] < min_required:
55+
outcome.force_result(True)

ddtrace/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .tracer import Tracer
55
from .settings import config
66

7-
__version__ = '0.17.1'
7+
__version__ = '0.18.0'
88

99
# a global tracer instance with integration settings
1010
tracer = Tracer()

ddtrace/compat.py

Lines changed: 68 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import platform
22
import sys
3+
import textwrap
4+
5+
import six
6+
7+
__all__ = [
8+
'httplib',
9+
'iteritems',
10+
'PY2',
11+
'Queue',
12+
'stringify',
13+
'StringIO',
14+
'urlencode',
15+
'parse',
16+
'reraise',
17+
]
318

419
PYTHON_VERSION_INFO = sys.version_info
520
PY2 = sys.version_info[0] == 2
@@ -8,32 +23,58 @@
823
PYTHON_VERSION = platform.python_version()
924
PYTHON_INTERPRETER = platform.python_implementation()
1025

11-
stringify = str
12-
13-
if PY2:
14-
from urllib import urlencode
15-
import httplib
16-
stringify = unicode
17-
from Queue import Queue
18-
try:
19-
from cStringIO import StringIO
20-
except ImportError:
21-
from StringIO import StringIO
22-
else:
23-
from queue import Queue
24-
from urllib.parse import urlencode
25-
import http.client as httplib
26-
from io import StringIO
27-
2826
try:
29-
import urlparse as parse
27+
StringIO = six.moves.cStringIO
3028
except ImportError:
31-
from urllib import parse
29+
StringIO = six.StringIO
3230

33-
try:
31+
httplib = six.moves.http_client
32+
urlencode = six.moves.urllib.parse.urlencode
33+
parse = six.moves.urllib.parse
34+
Queue = six.moves.queue.Queue
35+
iteritems = six.iteritems
36+
reraise = six.reraise
37+
38+
stringify = six.text_type
39+
string_type = six.string_types[0]
40+
msgpack_type = six.binary_type
41+
# DEV: `six` doesn't have `float` in `integer_types`
42+
numeric_types = six.integer_types + (float, )
43+
44+
45+
if PYTHON_VERSION_INFO[0:2] >= (3, 4):
3446
from asyncio import iscoroutinefunction
35-
from .compat_async import _make_async_decorator as make_async_decorator
36-
except ImportError:
47+
48+
# Execute from a string to get around syntax errors from `yield from`
49+
# DEV: The idea to do this was stolen from `six`
50+
# https://github.com/benjaminp/six/blob/15e31431af97e5e64b80af0a3f598d382bcdd49a/six.py#L719-L737
51+
six.exec_(textwrap.dedent("""
52+
import functools
53+
import asyncio
54+
55+
56+
def make_async_decorator(tracer, coro, *params, **kw_params):
57+
\"\"\"
58+
Decorator factory that creates an asynchronous wrapper that yields
59+
a coroutine result. This factory is required to handle Python 2
60+
compatibilities.
61+
62+
:param object tracer: the tracer instance that is used
63+
:param function f: the coroutine that must be executed
64+
:param tuple params: arguments given to the Tracer.trace()
65+
:param dict kw_params: keyword arguments given to the Tracer.trace()
66+
\"\"\"
67+
@functools.wraps(coro)
68+
@asyncio.coroutine
69+
def func_wrapper(*args, **kwargs):
70+
with tracer.trace(*params, **kw_params):
71+
result = yield from coro(*args, **kwargs) # noqa: E999
72+
return result
73+
74+
return func_wrapper
75+
"""))
76+
77+
else:
3778
# asyncio is missing so we can't have coroutines; these
3879
# functions are used only to ensure code executions in case
3980
# of an unexpected behavior
@@ -44,30 +85,24 @@ def make_async_decorator(tracer, fn, *params, **kw_params):
4485
return fn
4586

4687

47-
def iteritems(obj, **kwargs):
48-
func = getattr(obj, "iteritems", None)
49-
if not func:
50-
func = obj.items
51-
return func(**kwargs)
52-
53-
88+
# DEV: There is `six.u()` which does something similar, but doesn't have the guard around `hasattr(s, 'decode')`
5489
def to_unicode(s):
5590
""" Return a unicode string for the given bytes or string instance. """
5691
# No reason to decode if we already have the unicode compatible object we expect
57-
# DEV: `stringify` will be a `str` for python 3 and `unicode` for python 2
92+
# DEV: `six.text_type` will be a `str` for python 3 and `unicode` for python 2
5893
# DEV: Double decoding a `unicode` can cause a `UnicodeEncodeError`
5994
# e.g. `'\xc3\xbf'.decode('utf-8').decode('utf-8')`
60-
if isinstance(s, stringify):
95+
if isinstance(s, six.text_type):
6196
return s
6297

6398
# If the object has a `decode` method, then decode into `utf-8`
6499
# e.g. Python 2 `str`, Python 2/3 `bytearray`, etc
65100
if hasattr(s, 'decode'):
66101
return s.decode('utf-8')
67102

68-
# Always try to coerce the object into the `stringify` object we expect
103+
# Always try to coerce the object into the `six.text_type` object we expect
69104
# e.g. `to_unicode(1)`, `to_unicode(dict(key='value'))`
70-
return stringify(s)
105+
return six.text_type(s)
71106

72107

73108
def get_connection_response(conn):
@@ -86,45 +121,3 @@ def get_connection_response(conn):
86121
return conn.getresponse(buffering=True)
87122
else:
88123
return conn.getresponse()
89-
90-
91-
if PY2:
92-
string_type = basestring
93-
msgpack_type = basestring
94-
numeric_types = (int, long, float)
95-
else:
96-
string_type = str
97-
msgpack_type = bytes
98-
numeric_types = (int, float)
99-
100-
if PY2:
101-
# avoids Python 3 `SyntaxError`
102-
# this block will be replaced with the `six` library
103-
from .utils.reraise import _reraise as reraise
104-
else:
105-
def reraise(tp, value, tb=None):
106-
"""Python 3 re-raise function. This function is internal and
107-
will be replaced entirely with the `six` library.
108-
"""
109-
try:
110-
if value is None:
111-
value = tp()
112-
if value.__traceback__ is not tb:
113-
raise value.with_traceback(tb)
114-
raise value
115-
finally:
116-
value = None
117-
tb = None
118-
119-
120-
__all__ = [
121-
'httplib',
122-
'iteritems',
123-
'PY2',
124-
'Queue',
125-
'stringify',
126-
'StringIO',
127-
'urlencode',
128-
'parse',
129-
'reraise',
130-
]

ddtrace/compat_async.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

ddtrace/contrib/aiobotocore/patch.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,6 @@ def __aexit__(self, *args, **kwargs):
6666
return response
6767

6868

69-
def truncate_arg_value(value, max_len=1024):
70-
"""Truncate values which are bytes and greater than `max_len`.
71-
Useful for parameters like 'Body' in `put_object` operations.
72-
"""
73-
if isinstance(value, bytes) and len(value) > max_len:
74-
return b'...'
75-
76-
return value
77-
78-
7969
@asyncio.coroutine
8070
def _wrapped_api_call(original_func, instance, args, kwargs):
8171
pin = Pin.get_from(instance)
@@ -96,12 +86,7 @@ def _wrapped_api_call(original_func, instance, args, kwargs):
9686
operation = None
9787
span.resource = endpoint_name
9888

99-
# add args in TRACED_ARGS if exist to the span
100-
if not aws.is_blacklist(endpoint_name):
101-
for name, value in aws.unpacking_args(args, ARGS_NAME, TRACED_ARGS):
102-
if name == 'params':
103-
value = {k: truncate_arg_value(v) for k, v in value.items()}
104-
span.set_tag(name, (value))
89+
aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS)
10590

10691
region_name = deep_getattr(instance, 'meta.region_name')
10792

ddtrace/contrib/aiohttp/middlewares.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ def on_prepare(request, response):
8383
elif res_info.get('prefix'):
8484
resource = res_info.get('prefix')
8585

86+
# prefix the resource name by the http method
87+
resource = '{} {}'.format(request.method, resource)
88+
8689
request_span.resource = resource
8790
request_span.set_tag('http.method', request.method)
8891
request_span.set_tag('http.status_code', response.status)

ddtrace/contrib/aiopg/connection.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,14 @@ def callproc(self, proc, args):
6363
class AIOTracedConnection(wrapt.ObjectProxy):
6464
""" TracedConnection wraps a Connection with tracing code. """
6565

66-
def __init__(self, conn, pin=None):
66+
def __init__(self, conn, pin=None, cursor_cls=AIOTracedCursor):
6767
super(AIOTracedConnection, self).__init__(conn)
6868
name = dbapi._get_vendor(conn)
6969
db_pin = pin or Pin(service=name, app=name, app_type=AppTypes.db)
7070
db_pin.onto(self)
71+
# wrapt requires prefix of `_self` for attributes that are only in the
72+
# proxy (since some of our source objects will use `__slots__`)
73+
self._self_cursor_cls = cursor_cls
7174

7275
def cursor(self, *args, **kwargs):
7376
# unfortunately we also need to patch this method as otherwise "self"
@@ -81,4 +84,4 @@ def _cursor(self, *args, **kwargs):
8184
pin = Pin.get_from(self)
8285
if not pin:
8386
return cursor
84-
return AIOTracedCursor(cursor, pin)
87+
return self._self_cursor_cls(cursor, pin)

0 commit comments

Comments
 (0)