From caa281e4413ed579630a2236a8fdcd86a55800ef Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 11:27:51 +0100 Subject: [PATCH 01/11] fix(crons): Make @monitor work with async functions --- sentry_sdk/crons/decorator.py | 93 +++++++++++++++-------------------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index 34f4d0ac95..f7f38df762 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -1,70 +1,59 @@ -import sys +import inspect +from functools import wraps -from sentry_sdk._compat import contextmanager, reraise from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.crons import capture_checkin from sentry_sdk.crons.consts import MonitorStatus from sentry_sdk.utils import now -if TYPE_CHECKING: - from typing import Generator, Optional - -@contextmanager -def monitor(monitor_slug=None): - # type: (Optional[str]) -> Generator[None, None, None] - """ - Decorator/context manager to capture checkin events for a monitor. +if TYPE_CHECKING: + from typing import Callable, Optional, Type + from types import TracebackType - Usage (as decorator): - ``` - import sentry_sdk - app = Celery() +class monitor: + def __init__(self, monitor_slug=None): + # type: (str) -> None + self.monitor_slug = monitor_slug - @app.task - @sentry_sdk.monitor(monitor_slug='my-fancy-slug') - def test(arg): - print(arg) - ``` + def __enter__(self): + # type: () -> None + self.start_timestamp = now() + self.check_in_id = capture_checkin( + monitor_slug=self.monitor_slug, status=MonitorStatus.IN_PROGRESS + ) - This does not have to be used with Celery, but if you do use it with celery, - put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. + def __exit__(self, exc_type, exc_value, traceback): + # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + duration_s = now() - self.start_timestamp - Usage (as context manager): - ``` - import sentry_sdk + if exc_type is None and exc_value is None and traceback is None: + status = MonitorStatus.OK + else: + status = MonitorStatus.ERROR - def test(arg): - with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): - print(arg) - ``` + capture_checkin( + monitor_slug=self.monitor_slug, + check_in_id=self.check_in_id, + status=status, + duration=duration_s, + ) + def __call__(self, fn): + # type: (Callable) -> Callable + if inspect.iscoroutinefunction(fn): - """ + @wraps(fn) + async def inner(*args, **kwargs): + with self: + return await fn(*args, **kwargs) - start_timestamp = now() - check_in_id = capture_checkin( - monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS - ) + else: - try: - yield - except Exception: - duration_s = now() - start_timestamp - capture_checkin( - monitor_slug=monitor_slug, - check_in_id=check_in_id, - status=MonitorStatus.ERROR, - duration=duration_s, - ) - exc_info = sys.exc_info() - reraise(*exc_info) + @wraps(fn) + def inner(*args, **kwargs): + with self: + return fn(*args, **kwargs) - duration_s = now() - start_timestamp - capture_checkin( - monitor_slug=monitor_slug, - check_in_id=check_in_id, - status=MonitorStatus.OK, - duration=duration_s, - ) + return inner From af271cb8511226577d3814ae4de879fcf44e1b30 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 12:13:12 +0100 Subject: [PATCH 02/11] py2 --- sentry_sdk/crons/decorator.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index f7f38df762..103c330c0a 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -1,6 +1,10 @@ -import inspect from functools import wraps +try: + from inspect import iscoroutinefunction +except ImportError: + iscoroutinefunction = lambda f: False + from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.crons import capture_checkin from sentry_sdk.crons.consts import MonitorStatus @@ -42,12 +46,18 @@ def __exit__(self, exc_type, exc_value, traceback): def __call__(self, fn): # type: (Callable) -> Callable - if inspect.iscoroutinefunction(fn): + if iscoroutinefunction(fn): + # No async def in Python 2... + # XXX get rid of this in SDK 2.0 + exec( + """ @wraps(fn) async def inner(*args, **kwargs): with self: return await fn(*args, **kwargs) + """ + ) else: From f55c3ff696033d3034d80fadf80bd2647e753fa2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 12:28:14 +0100 Subject: [PATCH 03/11] typo, fix --- sentry_sdk/crons/decorator.py | 6 -- tests/crons/__init__.py | 0 tests/{ => crons}/test_crons.py | 48 +++++----- tests/crons/test_crons_async_py3.py | 136 ++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 30 deletions(-) create mode 100644 tests/crons/__init__.py rename tests/{ => crons}/test_crons.py (82%) create mode 100644 tests/crons/test_crons_async_py3.py diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index 103c330c0a..49c743b22b 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -48,16 +48,10 @@ def __call__(self, fn): # type: (Callable) -> Callable if iscoroutinefunction(fn): - # No async def in Python 2... - # XXX get rid of this in SDK 2.0 - exec( - """ @wraps(fn) async def inner(*args, **kwargs): with self: return await fn(*args, **kwargs) - """ - ) else: diff --git a/tests/crons/__init__.py b/tests/crons/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_crons.py b/tests/crons/test_crons.py similarity index 82% rename from tests/test_crons.py rename to tests/crons/test_crons.py index 39d02a5d47..a5ccd19bd5 100644 --- a/tests/test_crons.py +++ b/tests/crons/test_crons.py @@ -39,22 +39,22 @@ def test_decorator(sentry_init): with mock.patch( "sentry_sdk.crons.decorator.capture_checkin" - ) as fake_capture_checking: + ) as fake_capture_checkin: result = _hello_world("Grace") assert result == "Hello, Grace" # Check for initial checkin - fake_capture_checking.assert_has_calls( + fake_capture_checkin.assert_has_calls( [ mock.call(monitor_slug="abc123", status="in_progress"), ] ) # Check for final checkin - assert fake_capture_checking.call_args[1]["monitor_slug"] == "abc123" - assert fake_capture_checking.call_args[1]["status"] == "ok" - assert fake_capture_checking.call_args[1]["duration"] - assert fake_capture_checking.call_args[1]["check_in_id"] + assert fake_capture_checkin.call_args[1]["monitor_slug"] == "abc123" + assert fake_capture_checkin.call_args[1]["status"] == "ok" + assert fake_capture_checkin.call_args[1]["duration"] + assert fake_capture_checkin.call_args[1]["check_in_id"] def test_decorator_error(sentry_init): @@ -62,24 +62,24 @@ def test_decorator_error(sentry_init): with mock.patch( "sentry_sdk.crons.decorator.capture_checkin" - ) as fake_capture_checking: + ) as fake_capture_checkin: with pytest.raises(ZeroDivisionError): result = _break_world("Grace") assert "result" not in locals() # Check for initial checkin - fake_capture_checking.assert_has_calls( + fake_capture_checkin.assert_has_calls( [ mock.call(monitor_slug="def456", status="in_progress"), ] ) # Check for final checkin - assert fake_capture_checking.call_args[1]["monitor_slug"] == "def456" - assert fake_capture_checking.call_args[1]["status"] == "error" - assert fake_capture_checking.call_args[1]["duration"] - assert fake_capture_checking.call_args[1]["check_in_id"] + assert fake_capture_checkin.call_args[1]["monitor_slug"] == "def456" + assert fake_capture_checkin.call_args[1]["status"] == "error" + assert fake_capture_checkin.call_args[1]["duration"] + assert fake_capture_checkin.call_args[1]["check_in_id"] def test_contextmanager(sentry_init): @@ -87,22 +87,22 @@ def test_contextmanager(sentry_init): with mock.patch( "sentry_sdk.crons.decorator.capture_checkin" - ) as fake_capture_checking: + ) as fake_capture_checkin: result = _hello_world_contextmanager("Grace") assert result == "Hello, Grace" # Check for initial checkin - fake_capture_checking.assert_has_calls( + fake_capture_checkin.assert_has_calls( [ mock.call(monitor_slug="abc123", status="in_progress"), ] ) # Check for final checkin - assert fake_capture_checking.call_args[1]["monitor_slug"] == "abc123" - assert fake_capture_checking.call_args[1]["status"] == "ok" - assert fake_capture_checking.call_args[1]["duration"] - assert fake_capture_checking.call_args[1]["check_in_id"] + assert fake_capture_checkin.call_args[1]["monitor_slug"] == "abc123" + assert fake_capture_checkin.call_args[1]["status"] == "ok" + assert fake_capture_checkin.call_args[1]["duration"] + assert fake_capture_checkin.call_args[1]["check_in_id"] def test_contextmanager_error(sentry_init): @@ -110,24 +110,24 @@ def test_contextmanager_error(sentry_init): with mock.patch( "sentry_sdk.crons.decorator.capture_checkin" - ) as fake_capture_checking: + ) as fake_capture_checkin: with pytest.raises(ZeroDivisionError): result = _break_world_contextmanager("Grace") assert "result" not in locals() # Check for initial checkin - fake_capture_checking.assert_has_calls( + fake_capture_checkin.assert_has_calls( [ mock.call(monitor_slug="def456", status="in_progress"), ] ) # Check for final checkin - assert fake_capture_checking.call_args[1]["monitor_slug"] == "def456" - assert fake_capture_checking.call_args[1]["status"] == "error" - assert fake_capture_checking.call_args[1]["duration"] - assert fake_capture_checking.call_args[1]["check_in_id"] + assert fake_capture_checkin.call_args[1]["monitor_slug"] == "def456" + assert fake_capture_checkin.call_args[1]["status"] == "error" + assert fake_capture_checkin.call_args[1]["duration"] + assert fake_capture_checkin.call_args[1]["check_in_id"] def test_capture_checkin_simple(sentry_init): diff --git a/tests/crons/test_crons_async_py3.py b/tests/crons/test_crons_async_py3.py new file mode 100644 index 0000000000..6e00b594bd --- /dev/null +++ b/tests/crons/test_crons_async_py3.py @@ -0,0 +1,136 @@ +import pytest + +import sentry_sdk + +try: + from unittest import mock # python 3.3 and above +except ImportError: + import mock # python < 3.3 + + +@sentry_sdk.monitor(monitor_slug="abc123") +async def _hello_world(name): + return "Hello, {}".format(name) + + +@sentry_sdk.monitor(monitor_slug="def456") +async def _break_world(name): + 1 / 0 + return "Hello, {}".format(name) + + +async def my_coroutine(): + return + + +async def _hello_world_contextmanager(name): + with sentry_sdk.monitor(monitor_slug="abc123"): + await my_coroutine() + return "Hello, {}".format(name) + + +async def _break_world_contextmanager(name): + with sentry_sdk.monitor(monitor_slug="def456"): + await my_coroutine() + 1 / 0 + return "Hello, {}".format(name) + + +@pytest.mark.asyncio +async def test_decorator(sentry_init): + sentry_init() + + with mock.patch( + "sentry_sdk.crons.decorator.capture_checkin" + ) as fake_capture_checkin: + result = await _hello_world("Grace") + assert result == "Hello, Grace" + + # Check for initial checkin + fake_capture_checkin.assert_has_calls( + [ + mock.call(monitor_slug="abc123", status="in_progress"), + ] + ) + + # Check for final checkin + assert fake_capture_checkin.call_args[1]["monitor_slug"] == "abc123" + assert fake_capture_checkin.call_args[1]["status"] == "ok" + assert fake_capture_checkin.call_args[1]["duration"] + assert fake_capture_checkin.call_args[1]["check_in_id"] + + +@pytest.mark.asyncio +async def test_decorator_error(sentry_init): + sentry_init() + + with mock.patch( + "sentry_sdk.crons.decorator.capture_checkin" + ) as fake_capture_checkin: + with pytest.raises(ZeroDivisionError): + result = await _break_world("Grace") + + assert "result" not in locals() + + # Check for initial checkin + fake_capture_checkin.assert_has_calls( + [ + mock.call(monitor_slug="def456", status="in_progress"), + ] + ) + + # Check for final checkin + assert fake_capture_checkin.call_args[1]["monitor_slug"] == "def456" + assert fake_capture_checkin.call_args[1]["status"] == "error" + assert fake_capture_checkin.call_args[1]["duration"] + assert fake_capture_checkin.call_args[1]["check_in_id"] + + +@pytest.mark.asyncio +async def test_contextmanager(sentry_init): + sentry_init() + + with mock.patch( + "sentry_sdk.crons.decorator.capture_checkin" + ) as fake_capture_checkin: + result = await _hello_world_contextmanager("Grace") + assert result == "Hello, Grace" + + # Check for initial checkin + fake_capture_checkin.assert_has_calls( + [ + mock.call(monitor_slug="abc123", status="in_progress"), + ] + ) + + # Check for final checkin + assert fake_capture_checkin.call_args[1]["monitor_slug"] == "abc123" + assert fake_capture_checkin.call_args[1]["status"] == "ok" + assert fake_capture_checkin.call_args[1]["duration"] + assert fake_capture_checkin.call_args[1]["check_in_id"] + + +@pytest.mark.asyncio +async def test_contextmanager_error(sentry_init): + sentry_init() + + with mock.patch( + "sentry_sdk.crons.decorator.capture_checkin" + ) as fake_capture_checkin: + with pytest.raises(ZeroDivisionError): + result = await _break_world_contextmanager("Grace") + + assert "result" not in locals() + + # Check for initial checkin + fake_capture_checkin.assert_has_calls( + [ + mock.call(monitor_slug="def456", status="in_progress"), + ] + ) + + # Check for final checkin + assert fake_capture_checkin.call_args[1]["monitor_slug"] == "def456" + assert fake_capture_checkin.call_args[1]["status"] == "error" + assert fake_capture_checkin.call_args[1]["duration"] + assert fake_capture_checkin.call_args[1]["check_in_id"] From 7decde0c27bc679ac0c146393a7afd36ff965756 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 12:31:29 +0100 Subject: [PATCH 04/11] silence flake8 --- sentry_sdk/crons/decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index 49c743b22b..641f803b8b 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -16,7 +16,7 @@ from types import TracebackType -class monitor: +class monitor: # noqa: N801 def __init__(self, monitor_slug=None): # type: (str) -> None self.monitor_slug = monitor_slug From d63fb87b77a61b6b5f2b7d1e2d6fc2e347a6ba9b Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 12:43:21 +0100 Subject: [PATCH 05/11] py2.7 --- sentry_sdk/crons/_decorator.py | 90 ++++++++++++++++++++++++++++++ sentry_sdk/crons/_decorator_py2.py | 70 +++++++++++++++++++++++ sentry_sdk/crons/decorator.py | 68 +++------------------- 3 files changed, 168 insertions(+), 60 deletions(-) create mode 100644 sentry_sdk/crons/_decorator.py create mode 100644 sentry_sdk/crons/_decorator_py2.py diff --git a/sentry_sdk/crons/_decorator.py b/sentry_sdk/crons/_decorator.py new file mode 100644 index 0000000000..52bb24ba59 --- /dev/null +++ b/sentry_sdk/crons/_decorator.py @@ -0,0 +1,90 @@ +from functools import wraps + +try: + from inspect import iscoroutinefunction +except ImportError: + iscoroutinefunction = lambda f: False + +from sentry_sdk._types import TYPE_CHECKING +from sentry_sdk.crons import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.utils import now + +if TYPE_CHECKING: + from typing import Callable, Optional, Type + from types import TracebackType + + +class monitor: # noqa: N801 + """ + Decorator/context manager to capture checkin events for a monitor. + + Usage (as decorator): + ``` + import sentry_sdk + + app = Celery() + + @app.task + @sentry_sdk.monitor(monitor_slug='my-fancy-slug') + def test(arg): + print(arg) + ``` + + This does not have to be used with Celery, but if you do use it with celery, + put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. + + Usage (as context manager): + ``` + import sentry_sdk + + def test(arg): + with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): + print(arg) + ``` + """ + + def __init__(self, monitor_slug=None): + # type: (str) -> None + self.monitor_slug = monitor_slug + + def __enter__(self): + # type: () -> None + self.start_timestamp = now() + self.check_in_id = capture_checkin( + monitor_slug=self.monitor_slug, status=MonitorStatus.IN_PROGRESS + ) + + def __exit__(self, exc_type, exc_value, traceback): + # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + duration_s = now() - self.start_timestamp + + if exc_type is None and exc_value is None and traceback is None: + status = MonitorStatus.OK + else: + status = MonitorStatus.ERROR + + capture_checkin( + monitor_slug=self.monitor_slug, + check_in_id=self.check_in_id, + status=status, + duration=duration_s, + ) + + def __call__(self, fn): + # type: (Callable) -> Callable + if iscoroutinefunction(fn): + + @wraps(fn) + async def inner(*args, **kwargs): + with self: + return await fn(*args, **kwargs) + + else: + + @wraps(fn) + def inner(*args, **kwargs): + with self: + return fn(*args, **kwargs) + + return inner diff --git a/sentry_sdk/crons/_decorator_py2.py b/sentry_sdk/crons/_decorator_py2.py new file mode 100644 index 0000000000..34f4d0ac95 --- /dev/null +++ b/sentry_sdk/crons/_decorator_py2.py @@ -0,0 +1,70 @@ +import sys + +from sentry_sdk._compat import contextmanager, reraise +from sentry_sdk._types import TYPE_CHECKING +from sentry_sdk.crons import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.utils import now + +if TYPE_CHECKING: + from typing import Generator, Optional + + +@contextmanager +def monitor(monitor_slug=None): + # type: (Optional[str]) -> Generator[None, None, None] + """ + Decorator/context manager to capture checkin events for a monitor. + + Usage (as decorator): + ``` + import sentry_sdk + + app = Celery() + + @app.task + @sentry_sdk.monitor(monitor_slug='my-fancy-slug') + def test(arg): + print(arg) + ``` + + This does not have to be used with Celery, but if you do use it with celery, + put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. + + Usage (as context manager): + ``` + import sentry_sdk + + def test(arg): + with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): + print(arg) + ``` + + + """ + + start_timestamp = now() + check_in_id = capture_checkin( + monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS + ) + + try: + yield + except Exception: + duration_s = now() - start_timestamp + capture_checkin( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=MonitorStatus.ERROR, + duration=duration_s, + ) + exc_info = sys.exc_info() + reraise(*exc_info) + + duration_s = now() - start_timestamp + capture_checkin( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=MonitorStatus.OK, + duration=duration_s, + ) diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index 641f803b8b..51a18ca688 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -1,63 +1,11 @@ -from functools import wraps +from sentry_sdk._compat import PY2 -try: - from inspect import iscoroutinefunction -except ImportError: - iscoroutinefunction = lambda f: False +if PY2: + from sentry_sdk.crons._decorator_py2 import monitor +else: + from sentry_sdk.crons._decorator import monitor -from sentry_sdk._types import TYPE_CHECKING -from sentry_sdk.crons import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.utils import now - -if TYPE_CHECKING: - from typing import Callable, Optional, Type - from types import TracebackType - - -class monitor: # noqa: N801 - def __init__(self, monitor_slug=None): - # type: (str) -> None - self.monitor_slug = monitor_slug - - def __enter__(self): - # type: () -> None - self.start_timestamp = now() - self.check_in_id = capture_checkin( - monitor_slug=self.monitor_slug, status=MonitorStatus.IN_PROGRESS - ) - - def __exit__(self, exc_type, exc_value, traceback): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None - duration_s = now() - self.start_timestamp - - if exc_type is None and exc_value is None and traceback is None: - status = MonitorStatus.OK - else: - status = MonitorStatus.ERROR - - capture_checkin( - monitor_slug=self.monitor_slug, - check_in_id=self.check_in_id, - status=status, - duration=duration_s, - ) - - def __call__(self, fn): - # type: (Callable) -> Callable - if iscoroutinefunction(fn): - - @wraps(fn) - async def inner(*args, **kwargs): - with self: - return await fn(*args, **kwargs) - - else: - - @wraps(fn) - def inner(*args, **kwargs): - with self: - return fn(*args, **kwargs) - - return inner +__all__ = [ + monitor, +] From 6b1ec4c5c87a3b1464ac97cca5c077afc0e4ee0d Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 13:05:20 +0100 Subject: [PATCH 06/11] doing it differently --- sentry_sdk/crons/_decorator.py | 71 ++-------------------------- sentry_sdk/crons/_decorator_py2.py | 73 ++++------------------------- sentry_sdk/crons/decorator.py | 74 ++++++++++++++++++++++++++++-- tests/crons/test_crons.py | 3 +- 4 files changed, 84 insertions(+), 137 deletions(-) diff --git a/sentry_sdk/crons/_decorator.py b/sentry_sdk/crons/_decorator.py index 52bb24ba59..183cd15f3e 100644 --- a/sentry_sdk/crons/_decorator.py +++ b/sentry_sdk/crons/_decorator.py @@ -1,78 +1,15 @@ from functools import wraps - -try: - from inspect import iscoroutinefunction -except ImportError: - iscoroutinefunction = lambda f: False +from inspect import iscoroutinefunction from sentry_sdk._types import TYPE_CHECKING -from sentry_sdk.crons import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.utils import now if TYPE_CHECKING: - from typing import Callable, Optional, Type - from types import TracebackType - - -class monitor: # noqa: N801 - """ - Decorator/context manager to capture checkin events for a monitor. - - Usage (as decorator): - ``` - import sentry_sdk - - app = Celery() - - @app.task - @sentry_sdk.monitor(monitor_slug='my-fancy-slug') - def test(arg): - print(arg) - ``` - - This does not have to be used with Celery, but if you do use it with celery, - put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. - - Usage (as context manager): - ``` - import sentry_sdk - - def test(arg): - with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): - print(arg) - ``` - """ - - def __init__(self, monitor_slug=None): - # type: (str) -> None - self.monitor_slug = monitor_slug - - def __enter__(self): - # type: () -> None - self.start_timestamp = now() - self.check_in_id = capture_checkin( - monitor_slug=self.monitor_slug, status=MonitorStatus.IN_PROGRESS - ) - - def __exit__(self, exc_type, exc_value, traceback): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None - duration_s = now() - self.start_timestamp - - if exc_type is None and exc_value is None and traceback is None: - status = MonitorStatus.OK - else: - status = MonitorStatus.ERROR + from typing import Any, Callable - capture_checkin( - monitor_slug=self.monitor_slug, - check_in_id=self.check_in_id, - status=status, - duration=duration_s, - ) +class _MonitorMixin: def __call__(self, fn): - # type: (Callable) -> Callable + # type: (Callable[..., Any]) -> Callable[..., Any] if iscoroutinefunction(fn): @wraps(fn) diff --git a/sentry_sdk/crons/_decorator_py2.py b/sentry_sdk/crons/_decorator_py2.py index 34f4d0ac95..ab391e29a9 100644 --- a/sentry_sdk/crons/_decorator_py2.py +++ b/sentry_sdk/crons/_decorator_py2.py @@ -1,70 +1,17 @@ -import sys +from functools import wraps -from sentry_sdk._compat import contextmanager, reraise from sentry_sdk._types import TYPE_CHECKING -from sentry_sdk.crons import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.utils import now if TYPE_CHECKING: - from typing import Generator, Optional + from typing import Any, Callable -@contextmanager -def monitor(monitor_slug=None): - # type: (Optional[str]) -> Generator[None, None, None] - """ - Decorator/context manager to capture checkin events for a monitor. +class _MonitorMixin: + def __call__(self, fn): + # type: (Callable[..., Any]) -> Callable[..., Any] + @wraps(fn) + def inner(*args, **kwargs): + with self: + return fn(*args, **kwargs) - Usage (as decorator): - ``` - import sentry_sdk - - app = Celery() - - @app.task - @sentry_sdk.monitor(monitor_slug='my-fancy-slug') - def test(arg): - print(arg) - ``` - - This does not have to be used with Celery, but if you do use it with celery, - put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. - - Usage (as context manager): - ``` - import sentry_sdk - - def test(arg): - with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): - print(arg) - ``` - - - """ - - start_timestamp = now() - check_in_id = capture_checkin( - monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS - ) - - try: - yield - except Exception: - duration_s = now() - start_timestamp - capture_checkin( - monitor_slug=monitor_slug, - check_in_id=check_in_id, - status=MonitorStatus.ERROR, - duration=duration_s, - ) - exc_info = sys.exc_info() - reraise(*exc_info) - - duration_s = now() - start_timestamp - capture_checkin( - monitor_slug=monitor_slug, - check_in_id=check_in_id, - status=MonitorStatus.OK, - duration=duration_s, - ) + return inner diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index 51a18ca688..f66c29d0a9 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -1,11 +1,75 @@ from sentry_sdk._compat import PY2 +from sentry_sdk._types import TYPE_CHECKING +from sentry_sdk.crons import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.utils import now + +if TYPE_CHECKING: + from typing import Optional, Type + from types import TracebackType if PY2: - from sentry_sdk.crons._decorator_py2 import monitor + from sentry_sdk.crons._decorator_py2 import _MonitorMixin else: - from sentry_sdk.crons._decorator import monitor + # This is in its own module so that we don't trigger + # `async def` SyntaxErrors on Python 2. + # Once we drop Python 2, remove the mixin and merge it + # into the main monitor class. + from sentry_sdk.crons._decorator import _MonitorMixin + + +class monitor(_MonitorMixin): + """ + Decorator/context manager to capture checkin events for a monitor. + + Usage (as decorator): + ``` + import sentry_sdk + + app = Celery() + + @app.task + @sentry_sdk.monitor(monitor_slug='my-fancy-slug') + def test(arg): + print(arg) + ``` + + This does not have to be used with Celery, but if you do use it with celery, + put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. + + Usage (as context manager): + ``` + import sentry_sdk + + def test(arg): + with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): + print(arg) + ``` + """ + + def __init__(self, monitor_slug=None): + # type: (Optional[str]) -> None + self.monitor_slug = monitor_slug + + def __enter__(self): + # type: () -> None + self.start_timestamp = now() + self.check_in_id = capture_checkin( + monitor_slug=self.monitor_slug, status=MonitorStatus.IN_PROGRESS + ) + + def __exit__(self, exc_type, exc_value, traceback): + # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + duration_s = now() - self.start_timestamp + if exc_type is None and exc_value is None and traceback is None: + status = MonitorStatus.OK + else: + status = MonitorStatus.ERROR -__all__ = [ - monitor, -] + capture_checkin( + monitor_slug=self.monitor_slug, + check_in_id=self.check_in_id, + status=status, + duration=duration_s, + ) diff --git a/tests/crons/test_crons.py b/tests/crons/test_crons.py index a5ccd19bd5..0b31494acf 100644 --- a/tests/crons/test_crons.py +++ b/tests/crons/test_crons.py @@ -2,9 +2,8 @@ import uuid import sentry_sdk -from sentry_sdk.crons import capture_checkin - from sentry_sdk import Hub, configure_scope, set_level +from sentry_sdk.crons import capture_checkin try: from unittest import mock # python 3.3 and above From 89e6ad3d3e944ae7575e72c1165e11c07f460a58 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 13:10:00 +0100 Subject: [PATCH 07/11] note --- sentry_sdk/crons/decorator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index f66c29d0a9..b184bd98c7 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -11,14 +11,14 @@ if PY2: from sentry_sdk.crons._decorator_py2 import _MonitorMixin else: - # This is in its own module so that we don't trigger - # `async def` SyntaxErrors on Python 2. + # This is in its own module so that we don't make Python 2 + # angery over `async def` SyntaxErrors. # Once we drop Python 2, remove the mixin and merge it # into the main monitor class. from sentry_sdk.crons._decorator import _MonitorMixin -class monitor(_MonitorMixin): +class monitor(_MonitorMixin): # noqa: N801 """ Decorator/context manager to capture checkin events for a monitor. From 0259700a58a6f449d42178ef3d2d7b191abfce49 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 13:19:04 +0100 Subject: [PATCH 08/11] mypy --- sentry_sdk/crons/_decorator.py | 6 ++++-- sentry_sdk/crons/_decorator_py2.py | 3 ++- sentry_sdk/crons/decorator.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/crons/_decorator.py b/sentry_sdk/crons/_decorator.py index 183cd15f3e..a9a9fd2574 100644 --- a/sentry_sdk/crons/_decorator.py +++ b/sentry_sdk/crons/_decorator.py @@ -14,14 +14,16 @@ def __call__(self, fn): @wraps(fn) async def inner(*args, **kwargs): - with self: + # type: (Any, Any) -> Any + with self: # type: ignore[attr-defined] return await fn(*args, **kwargs) else: @wraps(fn) def inner(*args, **kwargs): - with self: + # type: (Any, Any) -> Any + with self: # type: ignore[attr-defined] return fn(*args, **kwargs) return inner diff --git a/sentry_sdk/crons/_decorator_py2.py b/sentry_sdk/crons/_decorator_py2.py index ab391e29a9..6a9e1b8001 100644 --- a/sentry_sdk/crons/_decorator_py2.py +++ b/sentry_sdk/crons/_decorator_py2.py @@ -11,7 +11,8 @@ def __call__(self, fn): # type: (Callable[..., Any]) -> Callable[..., Any] @wraps(fn) def inner(*args, **kwargs): - with self: + # type: (Any, Any) -> Any + with self: # type: ignore[attr-defined] return fn(*args, **kwargs) return inner diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index b184bd98c7..46ddc428ae 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -12,7 +12,7 @@ from sentry_sdk.crons._decorator_py2 import _MonitorMixin else: # This is in its own module so that we don't make Python 2 - # angery over `async def` SyntaxErrors. + # angery over `async def`s. # Once we drop Python 2, remove the mixin and merge it # into the main monitor class. from sentry_sdk.crons._decorator import _MonitorMixin From b510a119d47e45cdf405b17c14c24d37dc57d487 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Mar 2024 17:04:39 +0100 Subject: [PATCH 09/11] better naming & typing --- sentry_sdk/crons/_decorator.py | 9 ++++++--- sentry_sdk/crons/_decorator_py2.py | 9 ++++++--- sentry_sdk/crons/decorator.py | 6 +++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/crons/_decorator.py b/sentry_sdk/crons/_decorator.py index a9a9fd2574..08ae8a4b5f 100644 --- a/sentry_sdk/crons/_decorator.py +++ b/sentry_sdk/crons/_decorator.py @@ -4,12 +4,15 @@ from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Callable + from typing import Any, Callable, ParamSpec, TypeVar + P = ParamSpec("P") + R = TypeVar("R") -class _MonitorMixin: + +class MonitorMixin: def __call__(self, fn): - # type: (Callable[..., Any]) -> Callable[..., Any] + # type: (Callable[P, R]) -> Callable[P, R] if iscoroutinefunction(fn): @wraps(fn) diff --git a/sentry_sdk/crons/_decorator_py2.py b/sentry_sdk/crons/_decorator_py2.py index 6a9e1b8001..9e1da797e2 100644 --- a/sentry_sdk/crons/_decorator_py2.py +++ b/sentry_sdk/crons/_decorator_py2.py @@ -3,12 +3,15 @@ from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Callable + from typing import Any, Callable, ParamSpec, TypeVar + P = ParamSpec("P") + R = TypeVar("R") -class _MonitorMixin: + +class MonitorMixin: def __call__(self, fn): - # type: (Callable[..., Any]) -> Callable[..., Any] + # type: (Callable[P, R]) -> Callable[P, R] @wraps(fn) def inner(*args, **kwargs): # type: (Any, Any) -> Any diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index 46ddc428ae..38653ca161 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -9,16 +9,16 @@ from types import TracebackType if PY2: - from sentry_sdk.crons._decorator_py2 import _MonitorMixin + from sentry_sdk.crons._decorator_py2 import MonitorMixin else: # This is in its own module so that we don't make Python 2 # angery over `async def`s. # Once we drop Python 2, remove the mixin and merge it # into the main monitor class. - from sentry_sdk.crons._decorator import _MonitorMixin + from sentry_sdk.crons._decorator import MonitorMixin -class monitor(_MonitorMixin): # noqa: N801 +class monitor(MonitorMixin): # noqa: N801 """ Decorator/context manager to capture checkin events for a monitor. From 3dc5bc2379a126dc187de8a6be76163f8a981e54 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Thu, 28 Mar 2024 11:49:52 +0100 Subject: [PATCH 10/11] fix mypy --- sentry_sdk/crons/_decorator.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/crons/_decorator.py b/sentry_sdk/crons/_decorator.py index 08ae8a4b5f..4c337a5b26 100644 --- a/sentry_sdk/crons/_decorator.py +++ b/sentry_sdk/crons/_decorator.py @@ -4,7 +4,13 @@ from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Callable, ParamSpec, TypeVar + from typing import ( + Awaitable, + Callable, + ParamSpec, + TypeVar, + Union, + ) P = ParamSpec("P") R = TypeVar("R") @@ -12,20 +18,18 @@ class MonitorMixin: def __call__(self, fn): - # type: (Callable[P, R]) -> Callable[P, R] + # type: (Callable[P, R]) -> Callable[P, Union[R, Awaitable[R]]] if iscoroutinefunction(fn): @wraps(fn) - async def inner(*args, **kwargs): - # type: (Any, Any) -> Any + async def inner(*args: "P.args", **kwargs: "P.kwargs") -> R: with self: # type: ignore[attr-defined] return await fn(*args, **kwargs) else: @wraps(fn) - def inner(*args, **kwargs): - # type: (Any, Any) -> Any + def inner(*args: "P.args", **kwargs: "P.kwargs") -> R: with self: # type: ignore[attr-defined] return fn(*args, **kwargs) From d923c51554508dd4a03fb145e1cc2addccf23531 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Thu, 28 Mar 2024 11:52:23 +0100 Subject: [PATCH 11/11] fix mypy --- sentry_sdk/crons/_decorator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/crons/_decorator.py b/sentry_sdk/crons/_decorator.py index 4c337a5b26..5a15000a48 100644 --- a/sentry_sdk/crons/_decorator.py +++ b/sentry_sdk/crons/_decorator.py @@ -22,14 +22,16 @@ def __call__(self, fn): if iscoroutinefunction(fn): @wraps(fn) - async def inner(*args: "P.args", **kwargs: "P.kwargs") -> R: + async def inner(*args: "P.args", **kwargs: "P.kwargs"): + # type: (...) -> R with self: # type: ignore[attr-defined] return await fn(*args, **kwargs) else: @wraps(fn) - def inner(*args: "P.args", **kwargs: "P.kwargs") -> R: + def inner(*args: "P.args", **kwargs: "P.kwargs"): + # type: (...) -> R with self: # type: ignore[attr-defined] return fn(*args, **kwargs)