From a75d51729ba39bbd4d86982f3f0941c27727d5aa Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Fri, 13 Jun 2025 15:24:05 +0200 Subject: [PATCH 1/2] Correct semantics to use isolation scope and not current scope Previously this library was based on hubs which maps to the isolation scope now. --- README.md | 4 ++-- pytest_sentry/fixtures.py | 2 +- pytest_sentry/helpers.py | 24 ++++++++++++++---------- pytest_sentry/hooks.py | 33 ++++++++++++++------------------- tests/test_envvars.py | 2 +- tests/test_scope.py | 12 ++++++------ 6 files changed, 38 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index ae1ba53..b2b4c83 100644 --- a/README.md +++ b/README.md @@ -153,12 +153,12 @@ extreme lenghts to keep its own SDK setup separate from the SDK setup of the tested code. `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):` +the isolation `Scope` being used to send events to Sentry. Use `with use_isolation_scope(sentry_test_scope)`: to temporarily switch context. You can use this to set custom tags like so:: ```python def test_foo(sentry_test_scope): - with use_scope(sentry_test_scope): + with use_isolation_scope(sentry_test_scope): sentry_sdk.set_tag("pull_request", os.environ['EXAMPLE_CI_PULL_REQUEST']) ``` diff --git a/pytest_sentry/fixtures.py b/pytest_sentry/fixtures.py index ff72030..30b3536 100644 --- a/pytest_sentry/fixtures.py +++ b/pytest_sentry/fixtures.py @@ -6,7 +6,7 @@ @pytest.fixture def sentry_test_scope(request): """ - Gives back the current scope. + Gives back the isolation Scope. """ item = request.node diff --git a/pytest_sentry/helpers.py b/pytest_sentry/helpers.py index ce95a16..a902b15 100644 --- a/pytest_sentry/helpers.py +++ b/pytest_sentry/helpers.py @@ -1,21 +1,25 @@ import sentry_sdk +from sentry_sdk.scope import ScopeType +from sentry_sdk.opentelemetry.scope import setup_scope_context_management from pytest_sentry.client import Client -DEFAULT_SCOPE = sentry_sdk.Scope(client=Client()) +setup_scope_context_management() +DEFAULT_ISOLATION_SCOPE = sentry_sdk.Scope(ty=ScopeType.ISOLATION) +DEFAULT_ISOLATION_SCOPE.set_client(Client()) -_scope_cache = {} +_isolation_scope_cache = {} 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( + if id(marker_value) not in _isolation_scope_cache: + _isolation_scope_cache[id(marker_value)] = rv = _resolve_scope_marker_value_uncached( marker_value ) return rv - return _scope_cache[id(marker_value)] + return _isolation_scope_cache[id(marker_value)] def _resolve_scope_marker_value_uncached(marker_value): @@ -23,7 +27,7 @@ def _resolve_scope_marker_value_uncached(marker_value): # If no special configuration is provided # (like pytestmark or @pytest.mark.sentry_client() decorator) # use the default scope - marker_value = DEFAULT_SCOPE + marker_value = DEFAULT_ISOLATION_SCOPE else: marker_value = marker_value.args[0] @@ -37,25 +41,25 @@ def _resolve_scope_marker_value_uncached(marker_value): if isinstance(marker_value, str): # If a DSN string is provided, create a new client and use that - scope = sentry_sdk.get_current_scope() + scope = sentry_sdk.get_isolation_scope() scope.set_client(Client(marker_value)) return scope if isinstance(marker_value, dict): # If a dict is provided, create a new client using the dict as Client options - scope = sentry_sdk.get_current_scope() + scope = sentry_sdk.get_isolation_scope() scope.set_client(Client(**marker_value)) return scope if isinstance(marker_value, Client): # If a Client instance is provided, use that - scope = sentry_sdk.get_current_scope() + scope = sentry_sdk.get_isolation_scope() scope.set_client(marker_value) return scope if isinstance(marker_value, sentry_sdk.Scope): # If a Scope instance is provided, use the client from it - scope = sentry_sdk.get_current_scope() + scope = sentry_sdk.get_isolation_scope() scope.set_client(marker_value.client) return marker_value diff --git a/pytest_sentry/hooks.py b/pytest_sentry/hooks.py index a3acce8..c2e798a 100644 --- a/pytest_sentry/hooks.py +++ b/pytest_sentry/hooks.py @@ -27,31 +27,26 @@ def hookwrapper(itemgetter, **kwargs): """ @wrapt.decorator - def _with_scope(wrapped, instance, args, kwargs): + def _with_isolation_scope(wrapped, instance, args, kwargs): item = itemgetter(*args, **kwargs) - scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client")) + isolation_scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client")) - if scope.client.get_integration(PytestIntegration) is None: + if isolation_scope.client.get_integration(PytestIntegration) is None: yield else: - with sentry_sdk.use_scope(scope): + with sentry_sdk.use_isolation_scope(isolation_scope): gen = wrapped(*args, **kwargs) - while True: - try: - with sentry_sdk.use_scope(scope): + while True: + try: chunk = next(gen) - - y = yield chunk - - with sentry_sdk.use_scope(scope): + y = yield chunk gen.send(y) - - except StopIteration: - break + except StopIteration: + break def inner(f): - return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_scope(f)) + return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_isolation_scope(f)) return inner @@ -89,7 +84,7 @@ 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 sentry_sdk.continue_trace(dict(sentry_sdk.get_current_scope().iter_trace_propagation_headers())): + with sentry_sdk.continue_trace(dict(sentry_sdk.get_isolation_scope().iter_trace_propagation_headers())): with sentry_sdk.start_span(op=op, name=name) as span: span.set_attribute("pytest-sentry.rerun", is_rerun) if is_rerun: @@ -107,7 +102,7 @@ def pytest_fixture_setup(fixturedef, request): op = "pytest.fixture.setup" name = "{} {}".format(op, fixturedef.argname) - with sentry_sdk.continue_trace(dict(sentry_sdk.get_current_scope().iter_trace_propagation_headers())): + with sentry_sdk.continue_trace(dict(sentry_sdk.get_isolation_scope().iter_trace_propagation_headers())): with sentry_sdk.start_span(op=op, name=name) as root_span: root_span.set_tag("pytest.fixture.scope", fixturedef.scope) yield @@ -135,8 +130,8 @@ def pytest_runtest_makereport(item, call): call.excinfo ] - scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client")) - integration = scope.client.get_integration(PytestIntegration) + isolation_scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client")) + integration = isolation_scope.client.get_integration(PytestIntegration) if (cur_exc_chain and call.excinfo is None) or (integration is not None and integration.always_report): for exc_info in cur_exc_chain: diff --git a/tests/test_envvars.py b/tests/test_envvars.py index d8861a9..d014b75 100644 --- a/tests/test_envvars.py +++ b/tests/test_envvars.py @@ -32,7 +32,7 @@ def capture_envelope(self, envelope): def test_basic(sentry_test_scope): - with sentry_sdk.use_scope(sentry_test_scope): + with sentry_sdk.use_isolation_scope(sentry_test_scope): sentry_test_scope.capture_message("hi") (event,) = events diff --git a/tests/test_scope.py b/tests/test_scope.py index 3ba4c99..e0dfbda 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -9,15 +9,15 @@ pytestmark = pytest.mark.sentry_client(GLOBAL_CLIENT) -_DEFAULT_GLOBAL_SCOPE = sentry_sdk.Scope.get_global_scope() -_DEFAULT_ISOLATION_SCOPE = sentry_sdk.Scope.get_isolation_scope() +_DEFAULT_GLOBAL_SCOPE = sentry_sdk.get_global_scope() +_DEFAULT_ISOLATION_SCOPE = sentry_sdk.get_isolation_scope() def _assert_right_scopes(): - global_scope = sentry_sdk.Scope.get_global_scope() + global_scope = sentry_sdk.get_global_scope() assert global_scope is _DEFAULT_GLOBAL_SCOPE - isolation_scope = sentry_sdk.Scope.get_isolation_scope() + isolation_scope = sentry_sdk.get_isolation_scope() assert isolation_scope is _DEFAULT_ISOLATION_SCOPE @@ -25,9 +25,9 @@ def test_basic(): _assert_right_scopes() -def test_sentry_test_scope(sentry_test_scope): +def test_correct_span(): # Ensure that we are within a root span (started by the pytest_runtest_call hook) - assert sentry_test_scope.span is not None + assert sentry_sdk.get_current_scope().span is not None class TestSimpleClass(object): From 4abb772ba160de3f235db306fd033c503cff9f6f Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Mon, 16 Jun 2025 13:48:53 +0200 Subject: [PATCH 2/2] Simplify loop --- pytest_sentry/hooks.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pytest_sentry/hooks.py b/pytest_sentry/hooks.py index c2e798a..80926c2 100644 --- a/pytest_sentry/hooks.py +++ b/pytest_sentry/hooks.py @@ -36,14 +36,7 @@ def _with_isolation_scope(wrapped, instance, args, kwargs): else: with sentry_sdk.use_isolation_scope(isolation_scope): gen = wrapped(*args, **kwargs) - - while True: - try: - chunk = next(gen) - y = yield chunk - gen.send(y) - except StopIteration: - break + yield from gen def inner(f): return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_isolation_scope(f))