Skip to content

Commit

Permalink
Merge pull request #772 from DataDog/0.18-dev
Browse files Browse the repository at this point in the history
v0.18.0
  • Loading branch information
majorgreys authored Dec 12, 2018
2 parents 47ecf1d + 9e49744 commit a0bdd48
Show file tree
Hide file tree
Showing 144 changed files with 2,865 additions and 1,181 deletions.
20 changes: 19 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ jobs:
- checkout
- *restore_cache_step
- run: tox -e 'boto_contrib-{py27,py34}-boto' --result-json /tmp/boto.1.results
- run: tox -e 'botocore_contrib-{py27,py34}-botocore' --result-json /tmp/boto.2.results
- run: tox -e 'botocore_contrib-{py27,py34,py35,py36}-botocore' --result-json /tmp/boto.2.results
- persist_to_workspace:
root: /tmp
paths:
Expand Down Expand Up @@ -420,6 +420,20 @@ jobs:
- grpc.results
- *save_cache_step

molten:
docker:
- *test_runner
resource_class: *resource_class
steps:
- checkout
- *restore_cache_step
- run: tox -e 'molten_contrib-{py36}-molten{070,072}' --result-json /tmp/molten.results
- persist_to_workspace:
root: /tmp
paths:
- molten.results
- *save_cache_step

mysqlconnector:
docker:
- *test_runner
Expand Down Expand Up @@ -985,6 +999,9 @@ workflows:
- kombu:
requires:
- flake8
- molten:
requires:
- flake8
- mongoengine:
requires:
- flake8
Expand Down Expand Up @@ -1082,6 +1099,7 @@ workflows:
- integration
- jinja2
- kombu
- molten
- mongoengine
- msgpack
- mysqlconnector
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.ddtox/
.coverage
.coverage.*
.cache
Expand Down
55 changes: 55 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
This file configures a local pytest plugin, which allows us to configure plugin hooks to control the
execution of our tests. Either by loading in fixtures, configuring directories to ignore, etc
Local plugins: https://docs.pytest.org/en/3.10.1/writing_plugins.html#local-conftest-plugins
Hook reference: https://docs.pytest.org/en/3.10.1/reference.html#hook-reference
"""
import os
import re
import sys

import pytest

PY_DIR_PATTERN = re.compile(r'^py[23][0-9]$')


# Determine if the folder should be ignored
# https://docs.pytest.org/en/3.10.1/reference.html#_pytest.hookspec.pytest_ignore_collect
# DEV: We can only ignore folders/modules, we cannot ignore individual files
# DEV: We must wrap with `@pytest.mark.hookwrapper` to inherit from default (e.g. honor `--ignore`)
# https://github.com/pytest-dev/pytest/issues/846#issuecomment-122129189
@pytest.mark.hookwrapper
def pytest_ignore_collect(path, config):
"""
Skip directories defining a required minimum Python version
Example::
File: tests/contrib/vertica/py35/test.py
Python 2.7: Skip
Python 3.4: Skip
Python 3.5: Collect
Python 3.6: Collect
"""
# Execute original behavior first
# DEV: We need to set `outcome.force_result(True)` if we need to override
# these results and skip this directory
outcome = yield

# Was not ignored by default behavior
if not outcome.get_result():
# DEV: `path` is a `LocalPath`
path = str(path)
if not os.path.isdir(path):
path = os.path.dirname(path)
dirname = os.path.basename(path)

# Directory name match `py[23][0-9]`
if PY_DIR_PATTERN.match(dirname):
# Split out version numbers into a tuple: `py35` -> `(3, 5)`
min_required = tuple((int(v) for v in dirname.strip('py')))

# If the current Python version does not meet the minimum required, skip this directory
if sys.version_info[0:2] < min_required:
outcome.force_result(True)
2 changes: 1 addition & 1 deletion ddtrace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .tracer import Tracer
from .settings import config

__version__ = '0.17.1'
__version__ = '0.18.0'

# a global tracer instance with integration settings
tracer = Tracer()
Expand Down
143 changes: 68 additions & 75 deletions ddtrace/compat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import platform
import sys
import textwrap

import six

__all__ = [
'httplib',
'iteritems',
'PY2',
'Queue',
'stringify',
'StringIO',
'urlencode',
'parse',
'reraise',
]

PYTHON_VERSION_INFO = sys.version_info
PY2 = sys.version_info[0] == 2
Expand All @@ -8,32 +23,58 @@
PYTHON_VERSION = platform.python_version()
PYTHON_INTERPRETER = platform.python_implementation()

stringify = str

if PY2:
from urllib import urlencode
import httplib
stringify = unicode
from Queue import Queue
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
else:
from queue import Queue
from urllib.parse import urlencode
import http.client as httplib
from io import StringIO

try:
import urlparse as parse
StringIO = six.moves.cStringIO
except ImportError:
from urllib import parse
StringIO = six.StringIO

try:
httplib = six.moves.http_client
urlencode = six.moves.urllib.parse.urlencode
parse = six.moves.urllib.parse
Queue = six.moves.queue.Queue
iteritems = six.iteritems
reraise = six.reraise

stringify = six.text_type
string_type = six.string_types[0]
msgpack_type = six.binary_type
# DEV: `six` doesn't have `float` in `integer_types`
numeric_types = six.integer_types + (float, )


if PYTHON_VERSION_INFO[0:2] >= (3, 4):
from asyncio import iscoroutinefunction
from .compat_async import _make_async_decorator as make_async_decorator
except ImportError:

# Execute from a string to get around syntax errors from `yield from`
# DEV: The idea to do this was stolen from `six`
# https://github.com/benjaminp/six/blob/15e31431af97e5e64b80af0a3f598d382bcdd49a/six.py#L719-L737
six.exec_(textwrap.dedent("""
import functools
import asyncio
def make_async_decorator(tracer, coro, *params, **kw_params):
\"\"\"
Decorator factory that creates an asynchronous wrapper that yields
a coroutine result. This factory is required to handle Python 2
compatibilities.
:param object tracer: the tracer instance that is used
:param function f: the coroutine that must be executed
:param tuple params: arguments given to the Tracer.trace()
:param dict kw_params: keyword arguments given to the Tracer.trace()
\"\"\"
@functools.wraps(coro)
@asyncio.coroutine
def func_wrapper(*args, **kwargs):
with tracer.trace(*params, **kw_params):
result = yield from coro(*args, **kwargs) # noqa: E999
return result
return func_wrapper
"""))

else:
# asyncio is missing so we can't have coroutines; these
# functions are used only to ensure code executions in case
# of an unexpected behavior
Expand All @@ -44,30 +85,24 @@ def make_async_decorator(tracer, fn, *params, **kw_params):
return fn


def iteritems(obj, **kwargs):
func = getattr(obj, "iteritems", None)
if not func:
func = obj.items
return func(**kwargs)


# DEV: There is `six.u()` which does something similar, but doesn't have the guard around `hasattr(s, 'decode')`
def to_unicode(s):
""" Return a unicode string for the given bytes or string instance. """
# No reason to decode if we already have the unicode compatible object we expect
# DEV: `stringify` will be a `str` for python 3 and `unicode` for python 2
# DEV: `six.text_type` will be a `str` for python 3 and `unicode` for python 2
# DEV: Double decoding a `unicode` can cause a `UnicodeEncodeError`
# e.g. `'\xc3\xbf'.decode('utf-8').decode('utf-8')`
if isinstance(s, stringify):
if isinstance(s, six.text_type):
return s

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

# Always try to coerce the object into the `stringify` object we expect
# Always try to coerce the object into the `six.text_type` object we expect
# e.g. `to_unicode(1)`, `to_unicode(dict(key='value'))`
return stringify(s)
return six.text_type(s)


def get_connection_response(conn):
Expand All @@ -86,45 +121,3 @@ def get_connection_response(conn):
return conn.getresponse(buffering=True)
else:
return conn.getresponse()


if PY2:
string_type = basestring
msgpack_type = basestring
numeric_types = (int, long, float)
else:
string_type = str
msgpack_type = bytes
numeric_types = (int, float)

if PY2:
# avoids Python 3 `SyntaxError`
# this block will be replaced with the `six` library
from .utils.reraise import _reraise as reraise
else:
def reraise(tp, value, tb=None):
"""Python 3 re-raise function. This function is internal and
will be replaced entirely with the `six` library.
"""
try:
if value is None:
value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
finally:
value = None
tb = None


__all__ = [
'httplib',
'iteritems',
'PY2',
'Queue',
'stringify',
'StringIO',
'urlencode',
'parse',
'reraise',
]
28 changes: 0 additions & 28 deletions ddtrace/compat_async.py

This file was deleted.

17 changes: 1 addition & 16 deletions ddtrace/contrib/aiobotocore/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,6 @@ def __aexit__(self, *args, **kwargs):
return response


def truncate_arg_value(value, max_len=1024):
"""Truncate values which are bytes and greater than `max_len`.
Useful for parameters like 'Body' in `put_object` operations.
"""
if isinstance(value, bytes) and len(value) > max_len:
return b'...'

return value


@asyncio.coroutine
def _wrapped_api_call(original_func, instance, args, kwargs):
pin = Pin.get_from(instance)
Expand All @@ -96,12 +86,7 @@ def _wrapped_api_call(original_func, instance, args, kwargs):
operation = None
span.resource = endpoint_name

# add args in TRACED_ARGS if exist to the span
if not aws.is_blacklist(endpoint_name):
for name, value in aws.unpacking_args(args, ARGS_NAME, TRACED_ARGS):
if name == 'params':
value = {k: truncate_arg_value(v) for k, v in value.items()}
span.set_tag(name, (value))
aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS)

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

Expand Down
3 changes: 3 additions & 0 deletions ddtrace/contrib/aiohttp/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ def on_prepare(request, response):
elif res_info.get('prefix'):
resource = res_info.get('prefix')

# prefix the resource name by the http method
resource = '{} {}'.format(request.method, resource)

request_span.resource = resource
request_span.set_tag('http.method', request.method)
request_span.set_tag('http.status_code', response.status)
Expand Down
7 changes: 5 additions & 2 deletions ddtrace/contrib/aiopg/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@ def callproc(self, proc, args):
class AIOTracedConnection(wrapt.ObjectProxy):
""" TracedConnection wraps a Connection with tracing code. """

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

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

0 comments on commit a0bdd48

Please sign in to comment.