Repository: getsentry/pytest-sentry (language: python)

Issue:
Revert "feat: SDK 2.0 Support (#31)"
This reverts commit 2ea3687c665c784eee2b00c59e6ca9651f033a4e, which should have been released as a pre-release version.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0953073..90dcc3e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -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
diff --git a/README.rst b/README.rst
index a011f57..5d2b00d 100644
--- a/README.rst
+++ b/README.rst
@@ -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)
@@ -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})
 
 
@@ -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'])
 
 
diff --git a/setup.cfg b/setup.cfg
index 4b3d7f0..742bd02 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,7 +16,7 @@ classifiers =
 py_modules = pytest_sentry
 install_requires =
     pytest
-    sentry-sdk
+    sentry-sdk>=2.0.0a1,<3
     wrapt
 
 [options.entry_points]


Test patch:
diff --git a/pytest_sentry.py b/pytest_sentry.py
index d587991..57db670 100644
--- a/pytest_sentry.py
+++ b/pytest_sentry.py
@@ -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(
     [
@@ -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:
@@ -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
         )
@@ -94,38 +97,38 @@ 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
 
@@ -133,13 +136,13 @@ def inner(f):
 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)
@@ -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
@@ -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
 
@@ -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]
 
@@ -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):
diff --git a/tests/test_envvars.py b/tests/test_envvars.py
index 997f34a..0d48f43 100644
--- a/tests/test_envvars.py
+++ b/tests/test_envvars.py
@@ -1,5 +1,6 @@
 import pytest
 import sentry_sdk
+from sentry_sdk.scope import use_scope
 import pytest_sentry
 
 events = []
@@ -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"
diff --git a/tests/test_fixture.py b/tests/test_fixture.py
index 458177f..219fc23 100644
--- a/tests/test_fixture.py
+++ b/tests/test_fixture.py
@@ -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()
diff --git a/tests/test_hub.py b/tests/test_hub.py
deleted file mode 100644
index 1b91b16..0000000
--- a/tests/test_hub.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from __future__ import absolute_import
-
-import pytest
-import unittest
-import sentry_sdk
-import pytest_sentry
-
-pytestmark = pytest.mark.sentry_client(pytest_sentry.Client())
-
-_DEFAULT_HUB = sentry_sdk.Hub.current
-
-
-def _assert_right_hubs():
-    for hub in sentry_sdk.Hub.current, sentry_sdk.Hub.main:
-        assert hub.client is None
-        assert hub is _DEFAULT_HUB
-
-
-def test_basic():
-    _assert_right_hubs()
-
-
-class TestSimpleClass(object):
-    def setup_method(self):
-        _assert_right_hubs()
-
-    def test_basic(self):
-        _assert_right_hubs()
-
-    def teardown_method(self):
-        _assert_right_hubs()
-
-
-class TestUnittestClass(unittest.TestCase):
-    def setUp(self):
-        _assert_right_hubs()
-
-    def test_basic(self):
-        _assert_right_hubs()
-
-    def tearDown(self):
-        _assert_right_hubs()
diff --git a/tests/test_scope.py b/tests/test_scope.py
new file mode 100644
index 0000000..065c507
--- /dev/null
+++ b/tests/test_scope.py
@@ -0,0 +1,53 @@
+from __future__ import absolute_import
+
+import pytest
+import unittest
+import sentry_sdk
+import pytest_sentry
+
+pytestmark = pytest.mark.sentry_client(pytest_sentry.Client())
+
+_DEFAULT_GLOBAL_SCOPE = sentry_sdk.Scope.get_global_scope()
+_DEFAULT_ISOLATION_SCOPE = sentry_sdk.Scope.get_isolation_scope()
+
+
+def _assert_right_scopes():
+    global_scope = sentry_sdk.Scope.get_global_scope()
+    isolation_scope = sentry_sdk.Scope.get_isolation_scope()
+
+    assert not global_scope.get_client().is_active()
+    assert global_scope is _DEFAULT_GLOBAL_SCOPE
+
+    assert not isolation_scope.get_client().is_active()
+    assert isolation_scope is _DEFAULT_ISOLATION_SCOPE
+
+
+def test_basic():
+    _assert_right_scopes()
+
+
+def test_sentry_test_scope(sentry_test_scope):
+    # Ensure that we are within a transaction (started by the fixture)
+    assert sentry_test_scope.span is not None
+
+
+class TestSimpleClass(object):
+    def setup_method(self):
+        _assert_right_scopes()
+
+    def test_basic(self):
+        _assert_right_scopes()
+
+    def teardown_method(self):
+        _assert_right_scopes()
+
+
+class TestUnittestClass(unittest.TestCase):
+    def setUp(self):
+        _assert_right_scopes()
+
+    def test_basic(self):
+        _assert_right_scopes()
+
+    def tearDown(self):
+        _assert_right_scopes()