Skip to content

Commit

Permalink
Reapply "feat: SDK 2.0 Support (#31)" (#33)
Browse files Browse the repository at this point in the history
This reverts commit 687802f and changes the install requires version specifier. To be merged into main only after a patch release containing #32 is released.
  • Loading branch information
szokeasaurusrex authored Apr 5, 2024
1 parent 6544be9 commit f85f3a2
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 93 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.7', '3.8', '3.9', '3.10', '3.11']
python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
Expand Down
14 changes: 7 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ Advanced Options
================

``pytest-sentry`` supports marking your tests to use a different DSN, client or
hub per-test. You can use this to provide custom options to the ``Client``
scope per-test. You can use this to provide custom options to the ``Client``
object from the `Sentry SDK for Python
<https://github.com/getsentry/sentry-python>`_::

import random
import pytest

from sentry_sdk import Hub
from sentry_sdk import Scope
from pytest_sentry import Client

@pytest.mark.sentry_client(None)
Expand All @@ -108,7 +108,7 @@ object from the `Sentry SDK for Python

@pytest.mark.sentry_client(Client("CUSTOM DSN"))
@pytest.mark.sentry_client(lambda: Client("CUSTOM DSN"))
@pytest.mark.sentry_client(Hub(Client("CUSTOM DSN")))
@pytest.mark.sentry_client(Scope(Client("CUSTOM DSN")))
@pytest.mark.sentry_client({"dsn": ..., "debug": True})


Expand All @@ -125,12 +125,12 @@ you configured this plugin with. That's because ``pytest-sentry`` goes to
extreme lenghts to keep its own SDK setup separate from the SDK setup of the
tested code.

``pytest-sentry`` exposes the ``sentry_test_hub`` fixture whose return value is
the ``Hub`` being used to send events to Sentry. Use ``with sentry_test_hub:``
``pytest-sentry`` exposes the ``sentry_test_scope`` fixture whose return value is
the ``Scope`` being used to send events to Sentry. Use ``with use_scope(entry_test_scope):``
to temporarily switch context. You can use this to set custom tags like so::

def test_foo(sentry_test_hub):
with sentry_test_hub:
def test_foo(sentry_test_scope):
with use_scope(sentry_test_scope):
sentry_sdk.set_tag("pull_request", os.environ['EXAMPLE_CI_PULL_REQUEST'])


Expand Down
75 changes: 40 additions & 35 deletions pytest_sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import sentry_sdk
from sentry_sdk.integrations import Integration

from sentry_sdk import Hub, capture_exception
from sentry_sdk import Scope, capture_exception
from sentry_sdk.tracing import Transaction
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.scope import add_global_event_processor, use_scope

_ENVVARS_AS_TAGS = frozenset(
[
Expand Down Expand Up @@ -59,7 +59,7 @@ def __init__(self, always_report=None):
def setup_once():
@add_global_event_processor
def procesor(event, hint):
if Hub.current.get_integration(PytestIntegration) is None:
if Scope.get_client().get_integration(PytestIntegration) is None:
return event

for key in _ENVVARS_AS_TAGS:
Expand All @@ -82,7 +82,10 @@ def procesor(event, hint):
class Client(sentry_sdk.Client):
def __init__(self, *args, **kwargs):
kwargs.setdefault("dsn", os.environ.get("PYTEST_SENTRY_DSN", None))
kwargs.setdefault("traces_sample_rate", float(os.environ.get("PYTEST_SENTRY_TRACES_SAMPLE_RATE", 1.0)))
kwargs.setdefault(
"traces_sample_rate",
float(os.environ.get("PYTEST_SENTRY_TRACES_SAMPLE_RATE", 1.0)),
)
kwargs.setdefault("_experiments", {}).setdefault(
"auto_enabling_integrations", True
)
Expand All @@ -94,52 +97,52 @@ def __init__(self, *args, **kwargs):

def hookwrapper(itemgetter, **kwargs):
"""
A version of pytest.hookimpl that sets the current hub to the correct one
A version of pytest.hookimpl that sets the current scope to the correct one
and skips the hook if the integration is disabled.
Assumes the function is a hookwrapper, ie yields once
"""

@wrapt.decorator
def _with_hub(wrapped, instance, args, kwargs):
def _with_scope(wrapped, instance, args, kwargs):
item = itemgetter(*args, **kwargs)
hub = _resolve_hub_marker_value(item.get_closest_marker("sentry_client"))
scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client"))

if hub.get_integration(PytestIntegration) is None:
if scope.client.get_integration(PytestIntegration) is None:
yield
else:
with hub:
with use_scope(scope):
gen = wrapped(*args, **kwargs)

while True:
try:
with hub:
with use_scope(scope):
chunk = next(gen)

y = yield chunk

with hub:
with use_scope(scope):
gen.send(y)

except StopIteration:
break

def inner(f):
return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_hub(f))
return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_scope(f))

return inner


def pytest_load_initial_conftests(early_config, parser, args):
early_config.addinivalue_line(
"markers",
"sentry_client(client=None): Use this client instance for reporting tests. You can also pass a DSN string directly, or a `Hub` if you need it.",
"sentry_client(client=None): Use this client instance for reporting tests. You can also pass a DSN string directly, or a `Scope` if you need it.",
)


def _start_transaction(**kwargs):
transaction = Transaction.continue_from_headers(
dict(Hub.current.iter_trace_propagation_headers()), **kwargs
dict(Scope.get_current_scope().iter_trace_propagation_headers()), **kwargs
)
transaction.same_process_as_parent = True
return sentry_sdk.start_transaction(transaction)
Expand All @@ -154,7 +157,7 @@ def pytest_runtest_protocol(item):
# We use the full name including parameters because then we can identify
# how often a single test has run as part of the same GITHUB_RUN_ID.

with _start_transaction(op=op, name=u"{} {}".format(op, name)) as tx:
with _start_transaction(op=op, name="{} {}".format(op, name)) as tx:
yield

# Purposefully drop transaction to spare quota. We only created it to
Expand All @@ -171,14 +174,16 @@ def pytest_runtest_call(item):
# We use the full name including parameters because then we can identify
# how often a single test has run as part of the same GITHUB_RUN_ID.

with _start_transaction(op=op, name=u"{} {}".format(op, name)):
with _start_transaction(op=op, name="{} {}".format(op, name)):
yield


@hookwrapper(itemgetter=lambda fixturedef, request: request._pyfuncitem)
def pytest_fixture_setup(fixturedef, request):
op = "pytest.fixture.setup"
with _start_transaction(op=op, name=u"{} {}".format(op, fixturedef.argname)) as transaction:
with _start_transaction(
op=op, name="{} {}".format(op, fixturedef.argname)
) as transaction:
transaction.set_tag("pytest.fixture.scope", fixturedef.scope)
yield

Expand All @@ -198,31 +203,31 @@ def pytest_runtest_makereport(item, call):
call.excinfo
]

integration = Hub.current.get_integration(PytestIntegration)
integration = Scope.get_client().get_integration(PytestIntegration)

if (cur_exc_chain and call.excinfo is None) or integration.always_report:
for exc_info in cur_exc_chain:
capture_exception((exc_info.type, exc_info.value, exc_info.tb))


DEFAULT_HUB = Hub(Client())
DEFAULT_SCOPE = Scope(client=Client())

_hub_cache = {}
_scope_cache = {}


def _resolve_hub_marker_value(marker_value):
if id(marker_value) not in _hub_cache:
_hub_cache[id(marker_value)] = rv = _resolve_hub_marker_value_uncached(
def _resolve_scope_marker_value(marker_value):
if id(marker_value) not in _scope_cache:
_scope_cache[id(marker_value)] = rv = _resolve_scope_marker_value_uncached(
marker_value
)
return rv

return _hub_cache[id(marker_value)]
return _scope_cache[id(marker_value)]


def _resolve_hub_marker_value_uncached(marker_value):
def _resolve_scope_marker_value_uncached(marker_value):
if marker_value is None:
marker_value = DEFAULT_HUB
marker_value = DEFAULT_SCOPE
else:
marker_value = marker_value.args[0]

Expand All @@ -231,35 +236,35 @@ def _resolve_hub_marker_value_uncached(marker_value):

if marker_value is None:
# user explicitly disabled reporting
return Hub()
return Scope()

if isinstance(marker_value, str):
return Hub(Client(marker_value))
return Scope(client=Client(marker_value))

if isinstance(marker_value, dict):
return Hub(Client(**marker_value))
return Scope(client=Client(**marker_value))

if isinstance(marker_value, Client):
return Hub(marker_value)
return Scope(client=marker_value)

if isinstance(marker_value, Hub):
if isinstance(marker_value, Scope):
return marker_value

raise RuntimeError(
"The `sentry_client` value must be a client, hub or string, not {}".format(
"The `sentry_client` value must be a client, scope or string, not {}".format(
repr(type(marker_value))
)
)


@pytest.fixture
def sentry_test_hub(request):
def sentry_test_scope(request):
"""
Gives back the current hub.
Gives back the current scope.
"""

item = request.node
return _resolve_hub_marker_value(item.get_closest_marker("sentry_client"))
return _resolve_scope_marker_value(item.get_closest_marker("sentry_client"))


def _process_stacktrace(stacktrace):
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ classifiers =
py_modules = pytest_sentry
install_requires =
pytest
sentry-sdk
sentry-sdk>=2.0.0a1,<3
wrapt

[options.entry_points]
Expand Down
7 changes: 4 additions & 3 deletions tests/test_envvars.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import sentry_sdk
from sentry_sdk.scope import use_scope
import pytest_sentry

events = []
Expand All @@ -26,9 +27,9 @@ def clear_events(monkeypatch):
pytestmark = pytest.mark.sentry_client(pytest_sentry.Client(transport=MyTransport()))


def test_basic(sentry_test_hub):
with sentry_test_hub:
sentry_test_hub.capture_message("hi")
def test_basic(sentry_test_scope):
with use_scope(sentry_test_scope):
sentry_test_scope.capture_message("hi")

(event,) = events
assert event["tags"]["pytest_environ.GITHUB_RUN_ID"] == "123abc"
Expand Down
8 changes: 4 additions & 4 deletions tests/test_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
pytestmark = pytest.mark.sentry_client(GLOBAL_CLIENT)


def test_basic(sentry_test_hub):
assert sentry_test_hub.client is GLOBAL_CLIENT
def test_basic(sentry_test_scope):
assert sentry_test_scope.client is GLOBAL_CLIENT


@pytest.mark.sentry_client(None)
def test_func(sentry_test_hub):
assert sentry_test_hub.client is None
def test_func(sentry_test_scope):
assert not sentry_test_scope.client.is_active()
42 changes: 0 additions & 42 deletions tests/test_hub.py

This file was deleted.

Loading

0 comments on commit f85f3a2

Please sign in to comment.