From 2561ac3812e06a63526a90c40771192d2fbe2e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Mar 2024 17:01:49 +0200 Subject: [PATCH 01/13] Adapted the code to Asphalt 5 --- .github/workflows/publish.yml | 6 +- .github/workflows/test.yml | 4 +- .pre-commit-config.yaml | 7 +- docs/api.rst | 18 ++- docs/conf.py | 6 +- pyproject.toml | 22 ++-- src/asphalt/exceptions/__init__.py | 94 ++-------------- src/asphalt/exceptions/{api.py => _api.py} | 32 +++--- .../{component.py => _component.py} | 68 ++++++------ src/asphalt/exceptions/_utils.py | 83 ++++++++++++++ src/asphalt/exceptions/reporters/__init__.py | 0 src/asphalt/exceptions/reporters/raygun.py | 13 +-- src/asphalt/exceptions/reporters/sentry.py | 44 ++++---- tests/conftest.py | 6 + tests/reporters/test_raygun.py | 9 +- tests/reporters/test_sentry.py | 29 +++-- tests/test_component.py | 104 ++++++++++-------- tests/test_report_exception.py | 57 ++++++---- 18 files changed, 320 insertions(+), 282 deletions(-) rename src/asphalt/exceptions/{api.py => _api.py} (63%) rename src/asphalt/exceptions/{component.py => _component.py} (56%) create mode 100644 src/asphalt/exceptions/_utils.py create mode 100644 src/asphalt/exceptions/reporters/__init__.py create mode 100644 tests/conftest.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f68b255..92bbd0d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x - name: Install dependencies @@ -23,7 +23,7 @@ jobs: - name: Create packages run: python -m build - name: Archive packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist path: dist @@ -36,7 +36,7 @@ jobs: id-token: write steps: - name: Retrieve packages - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - name: Upload packages uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04d2ef6..99ae5d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,12 +11,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.7", "3.10", "3.11", "3.12", "pypy-3.9"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.10"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a794fc..81b8b60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.3.1 hooks: - id: ruff args: [--fix, --show-fixes] @@ -26,4 +26,7 @@ repos: rev: v1.8.0 hooks: - id: mypy - additional_dependencies: ["pytest"] + additional_dependencies: + - asphalt@git+https://github.com/asphalt-framework/asphalt@5.0 + - pytest + - sentry-sdk diff --git a/docs/api.rst b/docs/api.rst index 82455e2..3075522 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,28 +1,26 @@ API reference ============= +.. py:currentmodule:: asphalt.exceptions + Component --------- -.. autoclass:: asphalt.exceptions.component.ExceptionReporterComponent - :members: +.. autoclass:: ExceptionReporterComponent Functions --------- -.. autofunction:: asphalt.exceptions.report_exception +.. autofunction:: report_exception -Abstract classes ----------------- +Interfaces +---------- -.. automodule:: asphalt.exceptions.api - :members: +.. autoclass:: ExceptionReporter +.. autoclass:: ExtrasProvider Exception reporters ------------------- .. autoclass:: asphalt.exceptions.reporters.sentry.SentryExceptionReporter - :members: - .. autoclass:: asphalt.exceptions.reporters.raygun.RaygunExceptionReporter - :members: diff --git a/docs/conf.py b/docs/conf.py index 9ebf2f8..ed870f0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from importlib.metadata import version +import importlib.metadata from packaging.version import parse @@ -17,7 +17,7 @@ author = "Alex Grönholm" copyright = "2017, " + author -v = parse(version(project)) +v = parse(importlib.metadata.version(project)) version = v.base_version release = v.public @@ -25,11 +25,11 @@ exclude_patterns = ["_build"] pygments_style = "sphinx" +autodoc_default_options = {"members": True, "show-inheritance": True} highlight_language = "python3" todo_include_todos = False html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] htmlhelp_basename = project.replace("-", "") + "doc" extlinks = { diff --git a/pyproject.toml b/pyproject.toml index 9fa0e64..e98acb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,14 +19,13 @@ classifiers = [ "Typing :: Typed", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [ "asphalt ~= 4.6", "typing_extensions >= 4.6.0; python_version < '3.10'" @@ -40,7 +39,6 @@ Homepage = "https://github.com/asphalt-framework/asphalt-exceptions" test = [ "asphalt-exceptions[sentry,raygun]", "pytest", - "pytest-asyncio", "pytest-cov", "pytest-mock", ] @@ -64,10 +62,6 @@ raygun = "asphalt.exceptions.reporters.raygun:RaygunExceptionReporter" version_scheme = "post-release" local_scheme = "dirty-tag" -[tool.ruff] -line-length = 99 -target-version = "py37" - [tool.ruff.lint] select = [ "ASYNC", # flake8-async @@ -80,17 +74,18 @@ select = [ "UP", # pyupgrade ] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["asphalt.exceptions"] [tool.pytest.ini_options] addopts = "-rsx --tb=short" -asyncio_mode = "strict" testpaths = ["tests"] [tool.mypy] -python_version = "3.7" -ignore_missing_imports = true +python_version = "3.8" +strict = true +explicit_package_bases = true +mypy_path = ["src", "examples"] [tool.coverage.run] source = ["asphalt.exceptions"] @@ -103,7 +98,7 @@ show_missing = true [tool.tox] legacy_tox_ini = """ [tox] -envlist = py37, py38, py39, py310, py311, py312, pypy3 +envlist = py38, py39, py310, py311, py312, pypy3 skip_missing_interpreters = true minversion = 4.4.3 @@ -114,6 +109,5 @@ package = editable [testenv:docs] extras = doc -commands = sphinx-build docs build/sphinx -package = editable +commands = sphinx-build -W -n docs build/sphinx {posargs} """ diff --git a/src/asphalt/exceptions/__init__.py b/src/asphalt/exceptions/__init__.py index 73fae70..960a014 100644 --- a/src/asphalt/exceptions/__init__.py +++ b/src/asphalt/exceptions/__init__.py @@ -1,85 +1,13 @@ -from __future__ import annotations - -import inspect -import logging -import sys from typing import Any -from asphalt.core import Context, merge_config, qualified_name - -from asphalt.exceptions.api import ExceptionReporter, ExtrasProvider - -__all__ = ("report_exception",) - -module_logger = logging.getLogger(__name__) - - -def report_exception( - ctx: Context, - message: str, - exception: BaseException | None = None, - *, - logger: logging.Logger | str | bool = True, -) -> None: - """ - Report an exception to all exception reporters in the given context (and optionally log it too) - - :param ctx: context object to use for adding tags and other contextual information to the - report, as well as for retrieving the sentry client object - :param message: a free-form message to pass to the exception reporters (often used to describe - what was happening when the exception occurred) - :param exception: the exception to report; retrieved from :func:`sys.exc_info` if omitted - :param logger: logger instance or logger name to log the exception in, instead of the name of - the module where the exception was raised (or ``False`` to skip logging the exception) - - """ - if not exception: - exception = sys.exc_info()[1] - if not exception: - raise ValueError( - 'missing "exception" parameter and no current exception present in ' - "sys.exc_info()" - ) - - actual_logger: logging.Logger | None - if isinstance(logger, bool): - tb = exception.__traceback__ - if logger and tb: - frame = tb.tb_frame - module = inspect.getmodule(frame) - if module and module.__spec__: - actual_logger = logging.getLogger(module.__spec__.name) - else: # pragma: no cover - actual_logger = logging.getLogger(frame.f_globals["__name__"]) - else: - actual_logger = None - elif isinstance(logger, str): - actual_logger = logging.getLogger(logger) - else: - actual_logger = logger - - if actual_logger: - actual_logger.error(message, exc_info=exception) - - extras_providers = ctx.get_resources(ExtrasProvider) - for reporter in ctx.get_resources(ExceptionReporter): - extra: dict[str, Any] = {} - for provider in extras_providers: - try: - new_extra = provider.get_extras(ctx, reporter) - except Exception: - module_logger.exception( - "error retrieving exception extras for %s from %s", - qualified_name(reporter), - qualified_name(provider), - ) - else: - if isinstance(new_extra, dict): - extra = merge_config(extra, new_extra) - - try: - reporter.report_exception(ctx, exception, message, extra) - except Exception: - module_logger.exception( - "error calling exception reporter (%s)", qualified_name(reporter) - ) +from ._api import ExceptionReporter as ExceptionReporter +from ._api import ExtrasProvider as ExtrasProvider +from ._component import ExceptionReporterComponent as ExceptionReporterComponent +from ._utils import report_exception as report_exception + +# Re-export imports, so they look like they live directly in this package +key: str +value: Any +for key, value in list(locals().items()): + if getattr(value, "__module__", "").startswith(f"{__name__}."): + value.__module__ = __name__ diff --git a/src/asphalt/exceptions/api.py b/src/asphalt/exceptions/_api.py similarity index 63% rename from src/asphalt/exceptions/api.py rename to src/asphalt/exceptions/_api.py index 33aba4b..6c6a490 100644 --- a/src/asphalt/exceptions/api.py +++ b/src/asphalt/exceptions/_api.py @@ -3,21 +3,18 @@ from abc import ABCMeta, abstractmethod from typing import Any -from asphalt.core import Context - class ExceptionReporter(metaclass=ABCMeta): """ Interface for services that log exceptions on external systems. - Exception reporter instances must be made available as resources in the context for them to - take effect. + Exception reporter instances must be made available as resources in the context for + them to take effect. """ @abstractmethod def report_exception( self, - ctx: Context, exception: BaseException, message: str, extra: dict[str, Any], @@ -25,13 +22,13 @@ def report_exception( """ Report the given exception to an external service. - Implementors should typically queue an event or something instead of using a synchronous - operations to send the exception. + Implementors should typically queue an event or something instead of using a + synchronous operations to send the exception. - :param ctx: the context in which the exception occurred :param exception: an exception :param message: an accompanying message - :param extra: backend specific extra contextual information gathered from extras providers + :param extra: backend specific extra contextual information gathered from extras + providers """ @@ -39,21 +36,20 @@ class ExtrasProvider(metaclass=ABCMeta): """ Interface for a provider of extra data for exception reporters. - Implementors must check the type of the reporter and provide extra data specific to each - backend. See the documentation of each reporter class to find out the acceptable data - structures. + Implementors must check the type of the reporter and provide extra data specific to + each backend. See the documentation of each reporter class to find out the + acceptable data structures. - .. note:: Extras are gathered from providers in an unspecified order. The dicts are then - merged, so any conflicting keys might be lost. + .. note:: Extras are gathered from providers in an unspecified order. The dicts are + then merged, so any conflicting keys might be lost. """ @abstractmethod - def get_extras(self, ctx: Context, reporter: ExceptionReporter) -> dict[str, Any] | None: + def get_extras(self, reporter: ExceptionReporter) -> dict[str, Any] | None: """ Return context specific extras for the given exception reporter backend. - :param ctx: the context in which the exception was raised :param reporter: the exception reporter for which to provide extras - :return: a dict containing backend specific extra data, or ``None`` if no appropriate - extra data can be provided + :return: a dict containing backend specific extra data, or ``None`` if no + appropriate extra data can be provided """ diff --git a/src/asphalt/exceptions/component.py b/src/asphalt/exceptions/_component.py similarity index 56% rename from src/asphalt/exceptions/component.py rename to src/asphalt/exceptions/_component.py index 4accaff..f74e7a0 100644 --- a/src/asphalt/exceptions/component.py +++ b/src/asphalt/exceptions/_component.py @@ -2,85 +2,84 @@ import logging from asyncio import AbstractEventLoop, get_running_loop -from functools import partial -from typing import Any, AsyncIterator +from collections.abc import AsyncGenerator, Mapping +from typing import Any from asphalt.core import ( Component, - Context, PluginContainer, + add_resource, context_teardown, merge_config, qualified_name, ) -from asphalt.exceptions import report_exception -from asphalt.exceptions.api import ExceptionReporter +from ._api import ExceptionReporter +from ._utils import report_exception reporter_backends = PluginContainer("asphalt.exceptions.reporters", ExceptionReporter) logger = logging.getLogger(__name__) -def default_exception_handler( - loop: AbstractEventLoop, context: dict[str, Any], *, ctx: Context -) -> None: +def default_exception_handler(loop: AbstractEventLoop, context: dict[str, Any]) -> None: if "exception" in context: - report_exception(ctx, context["message"], context["exception"]) + report_exception(context["message"], context["exception"]) class ExceptionReporterComponent(Component): """ Creates one or more :class:`~asphalt.exceptions.api.ExceptionReporter` resources. - These resources are used by :func:`~asphalt.exceptions.report_exception` which calls each one - of them to report the exception. + These resources are used by :func:`~asphalt.exceptions.report_exception` which calls + each one of them to report the exception. - Optionally (and by default), a default exception handler is also installed for the event loop - which calls :func:`~asphalt.exceptions.report_exception` for all exceptions that occur in the - event loop machinery or in tasks which are garbage collected without ever having been awaited - on. + Optionally (and by default), a default exception handler is also installed for the + event loop which calls :func:`~asphalt.exceptions.report_exception` for all + exceptions that occur in the event loop machinery or in tasks which are garbage + collected without ever having been awaited on. Exception reporters can be configured in two ways: - #. a single reporter, with configuration supplied directly as keyword arguments to this - component's constructor - #. multiple reporters, by providing the ``reporters`` option where each key is the resource - name and each value is a dictionary containing that reporter's configuration + #. a single reporter, with configuration supplied directly as keyword arguments to + this component's constructor + #. multiple reporters, by providing the ``reporters`` option where each key is the + resource name and each value is a dictionary containing that reporter's + configuration - Each exception reporter configuration has one special option that is not passed to the - constructor of the backend class: + Each exception reporter configuration has one special option that is not passed to + the constructor of the backend class: * backend: entry point name of the reporter backend class (required) - :param reporters: a dictionary of resource name ⭢ constructor arguments for the chosen - reporter class - :param install_default_handler: ``True`` to install a new default exception handler for the - event loop when the component starts + :param reporters: a dictionary of resource name ⭢ constructor arguments for the + chosen reporter class + :param install_default_handler: ``True`` to install a new default exception handler + for the event loop when the component starts :param default_args: default values for constructor keyword arguments """ def __init__( self, - reporters: dict[str, dict[str, Any] | None] | None = None, + reporters: Mapping[str, dict[str, Any] | None] | None = None, install_default_handler: bool = True, - **default_args, + **default_args: Any, ) -> None: self.install_default_handler = install_default_handler if not reporters: reporters = {"default": default_args} - self.reporters: list[tuple] = [] + self.reporters: list[tuple[str, ExceptionReporter]] = [] for resource_name, config in reporters.items(): merged_config = merge_config(default_args, config or {}) type_ = merged_config.pop("backend", resource_name) - serializer = reporter_backends.create_object(type_, **merged_config) - self.reporters.append((resource_name, serializer)) + reporter = reporter_backends.create_object(type_, **merged_config) + self.reporters.append((resource_name, reporter)) @context_teardown - async def start(self, ctx: Context) -> AsyncIterator[None]: + async def start(self) -> AsyncGenerator[None, Exception | None]: for resource_name, reporter in self.reporters: - types = [ExceptionReporter, type(reporter)] - ctx.add_resource(reporter, resource_name, types=types) + types: list[type] = [ExceptionReporter, type(reporter)] + add_resource(reporter, resource_name, types=types) logger.info( "Configured exception reporter (%s; class=%s)", resource_name, @@ -88,8 +87,7 @@ async def start(self, ctx: Context) -> AsyncIterator[None]: ) if self.install_default_handler: - handler = partial(default_exception_handler, ctx=ctx) - get_running_loop().set_exception_handler(handler) + get_running_loop().set_exception_handler(default_exception_handler) logger.info("Installed default event loop exception handler") yield diff --git a/src/asphalt/exceptions/_utils.py b/src/asphalt/exceptions/_utils.py new file mode 100644 index 0000000..dedc545 --- /dev/null +++ b/src/asphalt/exceptions/_utils.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import inspect +import logging +import sys +from typing import Any + +from asphalt.core import get_resources, merge_config, qualified_name + +from ._api import ExceptionReporter, ExtrasProvider + +module_logger = logging.getLogger(__name__) + + +def report_exception( + message: str, + exception: BaseException | None = None, + *, + logger: logging.Logger | str | bool = True, +) -> None: + """ + Report an exception to all exception reporters in the given context (and optionally + log it too) + + :param message: a free-form message to pass to the exception reporters (often used + to describe what was happening when the exception occurred) + :param exception: the exception to report; retrieved from :func:`sys.exc_info` if + omitted + :param logger: logger instance or logger name to log the exception in, instead of + the name ofthe module where the exception was raised (or ``False`` to skip + logging the exception) + + """ + if not exception: + exception = sys.exc_info()[1] + if not exception: + raise ValueError( + 'missing "exception" parameter and no current exception present in ' + "sys.exc_info()" + ) + + actual_logger: logging.Logger | None + if isinstance(logger, bool): + tb = exception.__traceback__ + if logger and tb: + frame = tb.tb_frame + module = inspect.getmodule(frame) + if module and module.__spec__: + actual_logger = logging.getLogger(module.__spec__.name) + else: # pragma: no cover + actual_logger = logging.getLogger(frame.f_globals["__name__"]) + else: + actual_logger = None + elif isinstance(logger, str): + actual_logger = logging.getLogger(logger) + else: + actual_logger = logger + + if actual_logger: + actual_logger.error(message, exc_info=exception) + + extras_providers = get_resources(ExtrasProvider) # type: ignore[type-abstract] + for reporter in get_resources(ExceptionReporter): # type: ignore[type-abstract] + extra: dict[str, Any] = {} + for provider in extras_providers: + try: + new_extra = provider.get_extras(reporter) + except Exception: + module_logger.exception( + "error retrieving exception extras for %s from %s", + qualified_name(reporter), + qualified_name(provider), + ) + else: + if isinstance(new_extra, dict): + extra = merge_config(extra, new_extra) + + try: + reporter.report_exception(exception, message, extra) + except Exception: + module_logger.exception( + "error calling exception reporter (%s)", qualified_name(reporter) + ) diff --git a/src/asphalt/exceptions/reporters/__init__.py b/src/asphalt/exceptions/reporters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/asphalt/exceptions/reporters/raygun.py b/src/asphalt/exceptions/reporters/raygun.py index f557554..198d8a8 100644 --- a/src/asphalt/exceptions/reporters/raygun.py +++ b/src/asphalt/exceptions/reporters/raygun.py @@ -2,10 +2,9 @@ from typing import Any -from asphalt.core import Context from raygun4py.raygunprovider import RaygunSender -from asphalt.exceptions.api import ExceptionReporter +from asphalt.exceptions._api import ExceptionReporter class RaygunExceptionReporter(ExceptionReporter): @@ -14,23 +13,23 @@ class RaygunExceptionReporter(ExceptionReporter): To use this backend, install asphalt-exceptions with the ``raygun`` extra. - All keyword arguments are directly passed to :class:`raygun4py.raygunprovider.RaygunSender`. + All keyword arguments are directly passed to + :class:`raygun4py.raygunprovider.RaygunSender`. The extras passed to this backend are passed to :meth:`raygun4py.raygunprovider.RaygunSender.send_exception` as keyword arguments. - .. warning:: The current implementation of this backend sends exceptions synchronously, - potentially blocking the event loop. + .. warning:: The current implementation of this backend sends exceptions + synchronously, potentially blocking the event loop. .. _Raygun: https://raygun.com/ """ - def __init__(self, api_key: str, **config) -> None: + def __init__(self, api_key: str, **config: Any) -> None: self.client = RaygunSender(api_key, config) def report_exception( self, - ctx: Context, exception: BaseException, message: str, extra: dict[str, Any], diff --git a/src/asphalt/exceptions/reporters/sentry.py b/src/asphalt/exceptions/reporters/sentry.py index a8ed9dd..d069308 100644 --- a/src/asphalt/exceptions/reporters/sentry.py +++ b/src/asphalt/exceptions/reporters/sentry.py @@ -6,10 +6,10 @@ from typing import Any, Sequence import sentry_sdk -from asphalt.core import Context, resolve_reference +from asphalt.core import resolve_reference from sentry_sdk.integrations import Integration -from asphalt.exceptions.api import ExceptionReporter +from asphalt.exceptions._api import ExceptionReporter if sys.version_info >= (3, 10): from typing import TypeAlias @@ -23,7 +23,9 @@ Breadcrumb: TypeAlias = "dict[str, Any]" BreadcrumbHint: TypeAlias = "dict[str, Any]" EventProcessor: TypeAlias = "Callable[[Event, Hint], Event | None]" -BreadcrumbProcessor: TypeAlias = "Callable[[Breadcrumb, BreadcrumbHint], Breadcrumb | None]" +BreadcrumbProcessor: TypeAlias = ( + "Callable[[Breadcrumb, BreadcrumbHint], Breadcrumb | None]" +) class SentryExceptionReporter(ExceptionReporter): @@ -37,16 +39,17 @@ class SentryExceptionReporter(ExceptionReporter): * environment: "development" or "production", depending on the ``__debug__`` flag - Integrations can be added via the ``integrations`` option which is a list where each item is - either an object that implements the :class:`sentry_sdk.integrations.Integration` interface, - or a dictionary where the ``type`` key is a module:varname reference to a class implementing - the aforementioned interface. The ``args`` key, when present, should be a sequence that is - passed to the integration as positional arguments, while the ``kwargs`` key, when present, - should be a mapping of keyword arguments to their values. + Integrations can be added via the ``integrations`` option which is a list where each + item is either an object that implements the + :class:`sentry_sdk.integrations.Integration` interface, or a dictionary where the + ``type`` key is a module:varname reference to a class implementing + the aforementioned interface. The ``args`` key, when present, should be a sequence + that is passed to the integration as positional arguments, while the ``kwargs`` key, + when present, should be a mapping of keyword arguments to their values. - The extras passed to this backend are passed to :func:`sentry_sdk.capture_exception` as keyword - arguments. Two such options have been special cased and can be looked up as a - ``module:varname`` reference: + The extras passed to this backend are passed to :func:`sentry_sdk.capture_exception` + as keyword arguments. Two such options have been special cased and can be looked up + as a ``module:varname`` reference: - ``before_send`` - ``before_breadcrumb`` @@ -62,7 +65,7 @@ def __init__( integrations: Sequence[Integration | dict[str, Any]] = (), before_send: EventProcessor | str | None = None, before_breadcrumb: BreadcrumbProcessor | str | None = None, - **options, + **options: Any, ) -> None: if isinstance(before_send, str): _before_send: EventProcessor | None = resolve_reference(before_send) @@ -70,7 +73,9 @@ def __init__( _before_send = before_send if isinstance(before_breadcrumb, str): - _before_breadcrumb: BreadcrumbProcessor | None = resolve_reference(before_breadcrumb) + _before_breadcrumb: BreadcrumbProcessor | None = resolve_reference( + before_breadcrumb + ) else: _before_breadcrumb = before_breadcrumb @@ -80,11 +85,13 @@ def __init__( for integration in integrations: if isinstance(integration, dict): integration_class = resolve_reference(integration["type"]) - integration = integration_class( - *integration.get("args", ()), **integration.get("kwargs", {}) + integrations_.append( + integration_class( + *integration.get("args", ()), **integration.get("kwargs", {}) + ) ) - - integrations_.append(integration) + else: + integrations_.append(integration) sentry_sdk.init( integrations=integrations_, @@ -95,7 +102,6 @@ def __init__( def report_exception( self, - ctx: Context, exception: BaseException, message: str, extra: dict[str, Any], diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5c53fe0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" diff --git a/tests/reporters/test_raygun.py b/tests/reporters/test_raygun.py index 2b0ac91..3524597 100644 --- a/tests/reporters/test_raygun.py +++ b/tests/reporters/test_raygun.py @@ -2,13 +2,13 @@ from unittest.mock import patch import pytest -from asphalt.core import Context from asphalt.exceptions.reporters.raygun import RaygunExceptionReporter +pytestmark = pytest.mark.anyio -@pytest.mark.asyncio -async def test_raygun(): + +async def test_raygun() -> None: exception = None try: 1 / 0 @@ -17,7 +17,8 @@ async def test_raygun(): reporter = RaygunExceptionReporter("abvcgrdg234") with patch.object(reporter.client, "_post") as post: - reporter.report_exception(Context(), exception, "test exception", {}) + assert exception is not None + reporter.report_exception(exception, "test exception", {}) await asyncio.sleep(0.1) assert post.call_count == 1 diff --git a/tests/reporters/test_sentry.py b/tests/reporters/test_sentry.py index 08aa27e..46ed4f0 100644 --- a/tests/reporters/test_sentry.py +++ b/tests/reporters/test_sentry.py @@ -1,34 +1,37 @@ +from __future__ import annotations + +from typing import Any from unittest.mock import MagicMock import pytest -from asphalt.core import Context from pytest_mock import MockerFixture from sentry_sdk import Transport from sentry_sdk.integrations import Integration from asphalt.exceptions.reporters.sentry import SentryExceptionReporter +pytestmark = pytest.mark.anyio + class DummyIntegration(Integration): - def __init__(self, arg_a, arg_b): + def __init__(self, arg_a: Any, arg_b: Any): self.arg_a = arg_a self.arg_b = arg_b @staticmethod - def setup_once(): + def setup_once() -> None: pass -def before_send(event, hint): +def before_send(event: dict[str, Any], hint: dict[str, Any]) -> None: pass -def before_breadcrumb(breadcrumb, hint): +def before_breadcrumb(breadcrumb: dict[str, Any], hint: dict[str, Any]) -> None: pass -@pytest.mark.asyncio -async def test_sentry(): +async def test_sentry() -> None: exception = None try: 1 / 0 @@ -39,13 +42,14 @@ async def test_sentry(): reporter = SentryExceptionReporter( dsn="http://username:password@127.0.0.1/000000", transport=transport ) - reporter.report_exception(Context(), exception, "test exception", {}) + assert exception is not None + reporter.report_exception(exception, "test exception", {}) transport.capture_event.assert_called_once() -def test_integrations(): - integrations = [ +def test_integrations() -> None: + integrations: list[Integration | dict[str, Any]] = [ DummyIntegration(1, 2), {"type": f"{__name__}:DummyIntegration", "args": [3], "kwargs": {"arg_b": 4}}, ] @@ -54,10 +58,11 @@ def test_integrations(): ) -def test_hook_lookup(mocker: MockerFixture): +def test_hook_lookup(mocker: MockerFixture) -> None: init_func = mocker.patch("sentry_sdk.init") SentryExceptionReporter( - before_send=f"{__name__}:before_send", before_breadcrumb=f"{__name__}:before_breadcrumb" + before_send=f"{__name__}:before_send", + before_breadcrumb=f"{__name__}:before_breadcrumb", ) init_func.assert_called_once_with( integrations=[], diff --git a/tests/test_component.py b/tests/test_component.py index 11f5e1c..5724e70 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -1,98 +1,106 @@ +from __future__ import annotations + import gc import logging -from asyncio import sleep -from collections import OrderedDict +from asyncio import get_running_loop, sleep +from typing import Any import pytest -from asphalt.core.context import Context +from asphalt.core import Context, require_resource +from pytest import LogCaptureFixture + +from asphalt.exceptions import ExceptionReporter, ExceptionReporterComponent -from asphalt.exceptions.api import ExceptionReporter -from asphalt.exceptions.component import ExceptionReporterComponent +pytestmark = pytest.mark.anyio class DummyExceptionReporter(ExceptionReporter): + reported_exception: BaseException + def report_exception( - self, ctx: Context, exception: BaseException, message: str, extra=None + self, + exception: BaseException, + message: str, + extra: dict[str, Any], ) -> None: self.reported_exception = exception -@pytest.mark.parametrize("install_default_handler", [True, False], ids=["default", "nodefault"]) -@pytest.mark.asyncio -async def test_start(caplog, install_default_handler): +@pytest.mark.parametrize( + "install_default_handler", + [pytest.param(True, id="default"), pytest.param(False, id="nodefault")], +) +async def test_start(caplog: LogCaptureFixture, install_default_handler: bool) -> None: class DummyReporter(ExceptionReporter): def report_exception( - self, ctx: Context, exception: BaseException, message: str, extra=None + self, + exception: BaseException, + message: str, + xtra: dict[str, Any] | None = None, ) -> None: pass class DummyReporter2(ExceptionReporter): def report_exception( - self, ctx: Context, exception: BaseException, message: str, extra=None + self, + exception: BaseException, + message: str, + xtra: dict[str, Any] | None = None, ) -> None: pass - caplog.set_level(logging.INFO) - reporters = OrderedDict( - [ - ("dummy1", {"backend": DummyReporter}), - ("dummy2", {"backend": DummyReporter2}), - ] - ) + caplog.set_level(logging.INFO, "asphalt.exceptions") + reporters: dict[str, dict[str, Any]] = { + "dummy1": {"backend": DummyReporter}, + "dummy2": {"backend": DummyReporter2}, + } cmp = ExceptionReporterComponent( reporters=reporters, install_default_handler=install_default_handler ) - async with Context() as ctx: - await cmp.start(ctx) - - messages = [ - record.message - for record in caplog.records - if record.name == "asphalt.exceptions.component" - ] + async with Context(): + await cmp.start() + if install_default_handler: - assert len(messages) == 4 - assert messages[-2] == "Installed default event loop exception handler" - assert messages[-1] == "Uninstalled default event loop exception handler" + assert len(caplog.messages) == 4 + assert caplog.messages[-2] == "Installed default event loop exception handler" + assert caplog.messages[-1] == "Uninstalled default event loop exception handler" else: - assert len(messages) == 2 + assert len(caplog.messages) == 2 - assert messages[0] == ( + assert caplog.messages[0] == ( "Configured exception reporter (dummy1; " "class=tests.test_component.test_start..DummyReporter)" ) - assert messages[1] == ( + assert caplog.messages[1] == ( "Configured exception reporter (dummy2; " "class=tests.test_component.test_start..DummyReporter2)" ) -@pytest.mark.asyncio -async def test_default_exception_handler(event_loop): +async def test_default_exception_handler() -> None: """ - Test that an unawaited Task being garbage collected ends up being processed by the default - exception handler. - + Test that an unawaited Task being garbage collected ends up being processed by the + default exception handler. """ - async def fail_task(): + async def fail_task() -> float: return 1 / 0 - async with Context() as ctx: + async with Context(): component = ExceptionReporterComponent(backend=DummyExceptionReporter) - await component.start(ctx) - reporter = ctx.get_resource(ExceptionReporter) - event_loop.create_task(fail_task()) + await component.start() + reporter = require_resource(ExceptionReporter) # type: ignore[type-abstract] + task = get_running_loop().create_task(fail_task()) await sleep(0.1) + del task gc.collect() + assert isinstance(reporter, DummyExceptionReporter) assert isinstance(reporter.reported_exception, ZeroDivisionError) -@pytest.mark.asyncio -async def test_default_exception_handler_no_exception(event_loop): - async with Context() as ctx: +async def test_default_exception_handler_no_exception() -> None: + async with Context(): component = ExceptionReporterComponent(backend=DummyExceptionReporter) - await component.start(ctx) - handler = event_loop.get_exception_handler() - handler(event_loop, {"message": "dummy"}) + await component.start() + get_running_loop().call_exception_handler({"message": "dummy"}) diff --git a/tests/test_report_exception.py b/tests/test_report_exception.py index 472a04a..a30392f 100644 --- a/tests/test_report_exception.py +++ b/tests/test_report_exception.py @@ -1,10 +1,15 @@ +from __future__ import annotations + import logging +from typing import Any import pytest -from asphalt.core import Context +from asphalt.core import Context, add_resource +from pytest import LogCaptureFixture + +from asphalt.exceptions import ExceptionReporter, ExtrasProvider, report_exception -from asphalt.exceptions import ExtrasProvider, report_exception -from asphalt.exceptions.api import ExceptionReporter +pytestmark = pytest.mark.anyio @pytest.mark.parametrize( @@ -12,15 +17,23 @@ [True, False, "asphalt.exceptions", logging.getLogger("asphalt.exceptions")], ids=["autologger", "nologger", "strlogger", "loggerinstance"], ) -@pytest.mark.parametrize("faulty_extras_provider", [False, True], ids=["goodextras", "badextras"]) -@pytest.mark.parametrize("faulty_reporter", [False, True], ids=["goodreporter", "badreporter"]) -@pytest.mark.asyncio -async def test_report_exception(logger, faulty_extras_provider, faulty_reporter, caplog): +@pytest.mark.parametrize( + "faulty_extras_provider", [False, True], ids=["goodextras", "badextras"] +) +@pytest.mark.parametrize( + "faulty_reporter", [False, True], ids=["goodreporter", "badreporter"] +) +async def test_report_exception( + logger: logging.Logger | str, + faulty_extras_provider: bool, + faulty_reporter: bool, + caplog: LogCaptureFixture, +) -> None: reported_exception = reported_message = None class DummyReporter(ExceptionReporter): def report_exception( - self, ctx: Context, exception: BaseException, message: str, extra + self, exception: BaseException, message: str, extra: dict[str, Any] ) -> None: nonlocal reported_exception, reported_message assert extra == ({} if faulty_extras_provider else {"foo": "bar"}) @@ -30,37 +43,37 @@ def report_exception( raise Exception("bar") class DummyProvider(ExtrasProvider): - def get_extras(self, ctx: Context, reporter: ExceptionReporter): + def get_extras(self, reporter: ExceptionReporter) -> dict[str, Any]: if faulty_extras_provider: raise Exception("foo") else: return {"foo": "bar"} class EmptyProvider(ExtrasProvider): - def get_extras(self, ctx: Context, reporter: ExceptionReporter): + def get_extras(self, reporter: ExceptionReporter) -> None: return None - async with Context() as ctx: - ctx.add_resource(DummyReporter(), types=[ExceptionReporter]) - ctx.add_resource(DummyProvider(), "dummy", types=[ExtrasProvider]) - ctx.add_resource(EmptyProvider(), "empty", types=[ExtrasProvider]) + async with Context(): + add_resource(DummyReporter(), types=[ExceptionReporter]) + add_resource(DummyProvider(), "dummy", types=[ExtrasProvider]) + add_resource(EmptyProvider(), "empty", types=[ExtrasProvider]) try: 1 / 0 except ZeroDivisionError as e: - report_exception(ctx, "Got a boo-boo", logger=logger) + report_exception("Got a boo-boo", logger=logger) assert reported_exception is e assert reported_message == "Got a boo-boo" if faulty_reporter and logger: - messages = [ - record.message for record in caplog.records if record.name == "asphalt.exceptions" - ] - assert messages[-1].startswith( + assert caplog.messages[-1].startswith( "error calling exception reporter " "(tests.test_report_exception.test_report_exception..DummyReporter)" ) -def test_no_exception(): - exc = pytest.raises(Exception, report_exception, Context(), "test") - exc.match('missing "exception" parameter and no current exception present in sys.exc_info()') +def test_no_exception() -> None: + exc = pytest.raises(Exception, report_exception, "test") + exc.match( + 'missing "exception" parameter and no current exception present in ' + "sys.exc_info()" + ) From ab1e1ed935c3d9aba7131ad879ebccaf8561ac2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Tue, 26 Mar 2024 17:43:37 +0200 Subject: [PATCH 02/13] Updated code and tests to match the new resource API --- src/asphalt/exceptions/_component.py | 2 +- src/asphalt/exceptions/_utils.py | 4 ++-- tests/test_component.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/asphalt/exceptions/_component.py b/src/asphalt/exceptions/_component.py index f74e7a0..69bcf31 100644 --- a/src/asphalt/exceptions/_component.py +++ b/src/asphalt/exceptions/_component.py @@ -76,7 +76,7 @@ def __init__( self.reporters.append((resource_name, reporter)) @context_teardown - async def start(self) -> AsyncGenerator[None, Exception | None]: + async def start(self) -> AsyncGenerator[None, BaseException | None]: for resource_name, reporter in self.reporters: types: list[type] = [ExceptionReporter, type(reporter)] add_resource(reporter, resource_name, types=types) diff --git a/src/asphalt/exceptions/_utils.py b/src/asphalt/exceptions/_utils.py index dedc545..a24a7d1 100644 --- a/src/asphalt/exceptions/_utils.py +++ b/src/asphalt/exceptions/_utils.py @@ -59,8 +59,8 @@ def report_exception( if actual_logger: actual_logger.error(message, exc_info=exception) - extras_providers = get_resources(ExtrasProvider) # type: ignore[type-abstract] - for reporter in get_resources(ExceptionReporter): # type: ignore[type-abstract] + extras_providers = get_resources(ExtrasProvider).values() # type: ignore[type-abstract] + for reporter in get_resources(ExceptionReporter).values(): # type: ignore[type-abstract] extra: dict[str, Any] = {} for provider in extras_providers: try: diff --git a/tests/test_component.py b/tests/test_component.py index 5724e70..9685008 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -6,7 +6,7 @@ from typing import Any import pytest -from asphalt.core import Context, require_resource +from asphalt.core import Context, get_resource_nowait from pytest import LogCaptureFixture from asphalt.exceptions import ExceptionReporter, ExceptionReporterComponent @@ -89,7 +89,7 @@ async def fail_task() -> float: async with Context(): component = ExceptionReporterComponent(backend=DummyExceptionReporter) await component.start() - reporter = require_resource(ExceptionReporter) # type: ignore[type-abstract] + reporter = get_resource_nowait(ExceptionReporter) # type: ignore[type-abstract] task = get_running_loop().create_task(fail_task()) await sleep(0.1) del task From d4daa6f89de88136f69425f76b64d8d9ec21fc65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 13 Jun 2024 13:54:11 +0300 Subject: [PATCH 03/13] Updated code for Asphalt 5 --- .pre-commit-config.yaml | 6 +++--- pyproject.toml | 4 ++-- src/asphalt/exceptions/reporters/raygun.py | 2 +- src/asphalt/exceptions/reporters/sentry.py | 4 ++-- tests/conftest.py | 12 ++++++------ tests/reporters/test_raygun.py | 4 ++-- tests/test_component.py | 10 +++++----- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81b8b60..60d00ed 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ # * Run "pre-commit install". repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-toml - id: check-yaml @@ -16,14 +16,14 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.1 + rev: v0.4.8 hooks: - id: ruff args: [--fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.10.0 hooks: - id: mypy additional_dependencies: diff --git a/pyproject.toml b/pyproject.toml index e98acb6..4d2b867 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,13 +65,13 @@ local_scheme = "dirty-tag" [tool.ruff.lint] select = [ "ASYNC", # flake8-async - "E", "F", "W", # default Flake8 "G", # flake8-logging-format "I", # isort "ISC", # flake8-implicit-str-concat "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "UP", # pyupgrade + "W", # pycodestyle warnings ] [tool.ruff.lint.isort] @@ -85,7 +85,7 @@ testpaths = ["tests"] python_version = "3.8" strict = true explicit_package_bases = true -mypy_path = ["src", "examples"] +mypy_path = ["src", "tests"] [tool.coverage.run] source = ["asphalt.exceptions"] diff --git a/src/asphalt/exceptions/reporters/raygun.py b/src/asphalt/exceptions/reporters/raygun.py index 198d8a8..ec4d5a9 100644 --- a/src/asphalt/exceptions/reporters/raygun.py +++ b/src/asphalt/exceptions/reporters/raygun.py @@ -4,7 +4,7 @@ from raygun4py.raygunprovider import RaygunSender -from asphalt.exceptions._api import ExceptionReporter +from .._api import ExceptionReporter class RaygunExceptionReporter(ExceptionReporter): diff --git a/src/asphalt/exceptions/reporters/sentry.py b/src/asphalt/exceptions/reporters/sentry.py index d069308..ba9f340 100644 --- a/src/asphalt/exceptions/reporters/sentry.py +++ b/src/asphalt/exceptions/reporters/sentry.py @@ -9,7 +9,7 @@ from asphalt.core import resolve_reference from sentry_sdk.integrations import Integration -from asphalt.exceptions._api import ExceptionReporter +from .._api import ExceptionReporter if sys.version_info >= (3, 10): from typing import TypeAlias @@ -95,7 +95,7 @@ def __init__( sentry_sdk.init( integrations=integrations_, - before_send=_before_send, + before_send=_before_send, # type: ignore[arg-type] before_breadcrumb=_before_breadcrumb, **options, ) diff --git a/tests/conftest.py b/tests/conftest.py index 5c53fe0..d200d98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ -import pytest - - -@pytest.fixture -def anyio_backend() -> str: - return "asyncio" +# import pytest +# +# +# @pytest.fixture +# def anyio_backend() -> str: +# return "asyncio" diff --git a/tests/reporters/test_raygun.py b/tests/reporters/test_raygun.py index 3524597..a3a6b13 100644 --- a/tests/reporters/test_raygun.py +++ b/tests/reporters/test_raygun.py @@ -1,7 +1,7 @@ -import asyncio from unittest.mock import patch import pytest +from anyio import sleep from asphalt.exceptions.reporters.raygun import RaygunExceptionReporter @@ -20,5 +20,5 @@ async def test_raygun() -> None: assert exception is not None reporter.report_exception(exception, "test exception", {}) - await asyncio.sleep(0.1) + await sleep(0.1) assert post.call_count == 1 diff --git a/tests/test_component.py b/tests/test_component.py index 9685008..3fb5838 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -6,6 +6,7 @@ from typing import Any import pytest +import sniffio from asphalt.core import Context, get_resource_nowait from pytest import LogCaptureFixture @@ -26,11 +27,7 @@ def report_exception( self.reported_exception = exception -@pytest.mark.parametrize( - "install_default_handler", - [pytest.param(True, id="default"), pytest.param(False, id="nodefault")], -) -async def test_start(caplog: LogCaptureFixture, install_default_handler: bool) -> None: +async def test_start(caplog: LogCaptureFixture) -> None: class DummyReporter(ExceptionReporter): def report_exception( self, @@ -54,6 +51,7 @@ def report_exception( "dummy1": {"backend": DummyReporter}, "dummy2": {"backend": DummyReporter2}, } + install_default_handler = sniffio.current_async_library() == "asyncio" cmp = ExceptionReporterComponent( reporters=reporters, install_default_handler=install_default_handler ) @@ -77,6 +75,7 @@ def report_exception( ) +@pytest.mark.parametrize("anyio_backend", ["asyncio"], indirect=True) async def test_default_exception_handler() -> None: """ Test that an unawaited Task being garbage collected ends up being processed by the @@ -99,6 +98,7 @@ async def fail_task() -> float: assert isinstance(reporter.reported_exception, ZeroDivisionError) +@pytest.mark.parametrize("anyio_backend", ["asyncio"], indirect=True) async def test_default_exception_handler_no_exception() -> None: async with Context(): component = ExceptionReporterComponent(backend=DummyExceptionReporter) From 101e9411a92bc8a42b6243170e09fc1dd05f26bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 14 Dec 2024 12:54:28 +0200 Subject: [PATCH 04/13] Asphalt 5 update --- .github/workflows/test.yml | 12 +++++++----- .pre-commit-config.yaml | 6 +++--- .readthedocs.yml | 2 +- pyproject.toml | 8 ++++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99ae5d6..354210a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.10"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -23,11 +23,13 @@ jobs: cache: pip cache-dependency-path: pyproject.toml - name: Install dependencies - run: pip install -e .[test] coverage - - name: Test with pytest run: | - coverage run -m pytest - coverage xml + pip install git+https://github.com/asphalt-framework/asphalt.git@5.0 + pip install -e .[test] + - name: Test with pytest + run: coverage run -m pytest -v + - name: Generate coverage report + run: coverage xml - name: Upload Coverage uses: coverallsapp/github-action@v2 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 60d00ed..3d18e11 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ # * Run "pre-commit install". repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-toml - id: check-yaml @@ -16,14 +16,14 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.8 + rev: v0.8.3 hooks: - id: ruff args: [--fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.13.0 hooks: - id: mypy additional_dependencies: diff --git a/.readthedocs.yml b/.readthedocs.yml index 026b967..ac1099a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.9" sphinx: configuration: docs/conf.py diff --git a/pyproject.toml b/pyproject.toml index 4d2b867..040ce59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,13 +19,13 @@ classifiers = [ "Typing :: Typed", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "asphalt ~= 4.6", "typing_extensions >= 4.6.0; python_version < '3.10'" @@ -39,7 +39,6 @@ Homepage = "https://github.com/asphalt-framework/asphalt-exceptions" test = [ "asphalt-exceptions[sentry,raygun]", "pytest", - "pytest-cov", "pytest-mock", ] doc = [ @@ -76,13 +75,14 @@ select = [ [tool.ruff.lint.isort] known-first-party = ["asphalt.exceptions"] +known-third-party = ["asphalt.core"] [tool.pytest.ini_options] addopts = "-rsx --tb=short" testpaths = ["tests"] [tool.mypy] -python_version = "3.8" +python_version = "3.9" strict = true explicit_package_bases = true mypy_path = ["src", "tests"] From b08d43286b817587e5a3701aba8bc021abd5381f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 15 Dec 2024 16:52:59 +0200 Subject: [PATCH 05/13] Migrated to native tox configuration --- pyproject.toml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 040ce59..f001a77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,18 +96,18 @@ branch = true show_missing = true [tool.tox] -legacy_tox_ini = """ -[tox] -envlist = py38, py39, py310, py311, py312, pypy3 +env_list = ["py39", "py310", "py311", "py312", "py313", "pypy3"] skip_missing_interpreters = true -minversion = 4.4.3 -[testenv] -extras = test -commands = python -m pytest {posargs} -package = editable +[tool.tox.env_run_base] +commands = [["python", "-m", "pytest", { replace = "posargs", extend = true }]] +package = "editable" +extras = ["test"] -[testenv:docs] -extras = doc -commands = sphinx-build -W -n docs build/sphinx {posargs} -""" +[tool.tox.env.pyright] +deps = ["pyright"] +commands = [["pyright", "--verifytypes", "asphalt.exceptions"]] + +[tool.tox.env.docs] +commands = [["sphinx-build", "-W", "-n", "docs", "build/sphinx", { replace = "posargs", extend = true }]] +extras = ["doc"] From 5a8a5e56b028807bf5c7df3bb16b77bbc8c5a59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 15 Dec 2024 17:25:24 +0200 Subject: [PATCH 06/13] Use sphinx_rtd_extension as an extension, as recommended by their docs --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index ed870f0..9d2fe6c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,6 +8,7 @@ "sphinx.ext.intersphinx", "sphinx.ext.extlinks", "sphinx_autodoc_typehints", + "sphinx_rtd_theme", ] templates_path = ["_templates"] From 9600733ef0f936ec1122d4a84944d8fde9b3b430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 16 Dec 2024 00:43:43 +0200 Subject: [PATCH 07/13] More asphalt5 updates --- docs/extending.rst | 6 +++--- docs/usage.rst | 11 ++++++----- pyproject.toml | 2 +- src/asphalt/exceptions/__init__.py | 12 +++++------- src/asphalt/exceptions/reporters/sentry.py | 4 ++-- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/extending.rst b/docs/extending.rst index dbb9883..def88c2 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -5,8 +5,8 @@ Writing new reporter backends ----------------------------- To support new exception reporting services, you can subclass the -:class:`~asphalt.exceptions.api.ExceptionReporter` class. You just need to implement the -:meth:`~asphalt.exceptions.api.ExceptionReporter.report_exception` method. +:class:`~asphalt.exceptions.ExceptionReporter` class. You just need to implement the +:meth:`~asphalt.exceptions.ExceptionReporter.report_exception` method. If you want your exception reporter to be available as a backend for :class:`~asphalt.exceptions.component.ExceptionReporterComponent`, you need to add the @@ -39,7 +39,7 @@ Writing extras providers ------------------------ If you want to provide backend specific extra data for exception reporting, you can do so by -subclassing :class:`~asphalt.exceptions.api.ExtrasProvider` and adding one or more instances of it +subclassing :class:`~asphalt.exceptions.ExtrasProvider` and adding one or more instances of it as resources to the context. For example, if you wanted to provide extra data for Sentry about your custom context diff --git a/docs/usage.rst b/docs/usage.rst index ce3cbf5..bf75a30 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -10,8 +10,8 @@ When an exception is caught, the typical course of action is to log it:: except Exception: logger.exception('Tried to do something but it failed :(') -To take advantage of the exception reporters configured with this component, all you have to do is -call :func:`~asphalt.exceptions.report_exception` instead:: +To take advantage of the exception reporters configured with this component, all you +have to do is call :func:`~asphalt.exceptions.report_exception` instead:: from asphalt.exceptions import report_exception @@ -26,6 +26,7 @@ call :func:`~asphalt.exceptions.report_exception` instead:: This will not only log the exception as usual, but also send it to any external services represented by the configured exception reporter backends. -The ``ctx`` argument is required in order for the function to find the configured exception -reporter resources. Additionally, it looks up plugins matching the fully qualified class name of -the context object to provide additional information to each exception reporter backend. +The ``ctx`` argument is required in order for the function to find the configured +exception reporter resources. Additionally, it looks up plugins matching the fully +qualified class name of the context object to provide additional information to each +exception reporter backend. diff --git a/pyproject.toml b/pyproject.toml index f001a77..11e857f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ select = [ "I", # isort "ISC", # flake8-implicit-str-concat "PGH", # pygrep-hooks - "RUF100", # unused noqa (yesqa) + "RUF", # Ruff-specific rules "UP", # pyupgrade "W", # pycodestyle warnings ] diff --git a/src/asphalt/exceptions/__init__.py b/src/asphalt/exceptions/__init__.py index 960a014..67acabd 100644 --- a/src/asphalt/exceptions/__init__.py +++ b/src/asphalt/exceptions/__init__.py @@ -1,13 +1,11 @@ -from typing import Any - from ._api import ExceptionReporter as ExceptionReporter from ._api import ExtrasProvider as ExtrasProvider from ._component import ExceptionReporterComponent as ExceptionReporterComponent from ._utils import report_exception as report_exception # Re-export imports, so they look like they live directly in this package -key: str -value: Any -for key, value in list(locals().items()): - if getattr(value, "__module__", "").startswith(f"{__name__}."): - value.__module__ = __name__ +for __value in list(locals().values()): + if getattr(__value, "__module__", "").startswith(f"{__name__}."): + __value.__module__ = __name__ + +del __value diff --git a/src/asphalt/exceptions/reporters/sentry.py b/src/asphalt/exceptions/reporters/sentry.py index ba9f340..e6faab3 100644 --- a/src/asphalt/exceptions/reporters/sentry.py +++ b/src/asphalt/exceptions/reporters/sentry.py @@ -2,8 +2,8 @@ import logging import sys -from collections.abc import Callable -from typing import Any, Sequence +from collections.abc import Callable, Sequence +from typing import Any import sentry_sdk from asphalt.core import resolve_reference From b0ce2431a4882d1043a1e82ae74a1a0c0c37580b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 30 Dec 2024 15:44:24 +0200 Subject: [PATCH 08/13] Updated code to work with the latest Asphalt 5 code --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 354210a..605785c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: cache-dependency-path: pyproject.toml - name: Install dependencies run: | - pip install git+https://github.com/asphalt-framework/asphalt.git@5.0 + pip install git+https://github.com/asphalt-framework/asphalt.git pip install -e .[test] - name: Test with pytest run: coverage run -m pytest -v diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d18e11..fa5ce9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,6 +27,6 @@ repos: hooks: - id: mypy additional_dependencies: - - asphalt@git+https://github.com/asphalt-framework/asphalt@5.0 + - asphalt@git+https://github.com/asphalt-framework/asphalt - pytest - sentry-sdk diff --git a/pyproject.toml b/pyproject.toml index 11e857f..bf77a80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ known-first-party = ["asphalt.exceptions"] known-third-party = ["asphalt.core"] [tool.pytest.ini_options] -addopts = "-rsx --tb=short" +addopts = ["-rsfE", "--tb=short"] testpaths = ["tests"] [tool.mypy] From 6ef71a9f8126233a39b595ce45b6b4632d0bf65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 30 Dec 2024 15:49:22 +0200 Subject: [PATCH 09/13] Re-added coverage to test dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bf77a80..8296b72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ Homepage = "https://github.com/asphalt-framework/asphalt-exceptions" [project.optional-dependencies] test = [ "asphalt-exceptions[sentry,raygun]", + "coverage >= 7", "pytest", "pytest-mock", ] From 4a7f9d5604c1dd664d135eefe2427aacb9d1c9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 30 Dec 2024 16:00:28 +0200 Subject: [PATCH 10/13] Fixed test failures --- pyproject.toml | 1 + tests/reporters/test_sentry.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8296b72..11be6ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ Homepage = "https://github.com/asphalt-framework/asphalt-exceptions" [project.optional-dependencies] test = [ + "anyio[trio] ~= 4.1", "asphalt-exceptions[sentry,raygun]", "coverage >= 7", "pytest", diff --git a/tests/reporters/test_sentry.py b/tests/reporters/test_sentry.py index 46ed4f0..598fad4 100644 --- a/tests/reporters/test_sentry.py +++ b/tests/reporters/test_sentry.py @@ -45,7 +45,7 @@ async def test_sentry() -> None: assert exception is not None reporter.report_exception(exception, "test exception", {}) - transport.capture_event.assert_called_once() + transport.capture_envelope.assert_called_once() def test_integrations() -> None: From 6556dcc975b45c14a490b6118a0f0a297de228f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 30 Dec 2024 17:41:03 +0200 Subject: [PATCH 11/13] Changed the dependency to asphalt master --- .github/workflows/test.yml | 4 +--- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 605785c..898794e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,9 +23,7 @@ jobs: cache: pip cache-dependency-path: pyproject.toml - name: Install dependencies - run: | - pip install git+https://github.com/asphalt-framework/asphalt.git - pip install -e .[test] + run: pip install -e .[test] - name: Test with pytest run: coverage run -m pytest -v - name: Generate coverage report diff --git a/pyproject.toml b/pyproject.toml index 11be6ab..94ff360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "asphalt ~= 4.6", + "asphalt @ git+https://github.com/asphalt-framework/asphalt", "typing_extensions >= 4.6.0; python_version < '3.10'" ] dynamic = ["version"] From 54fdf284dbb53521f8849ef5f1d15543817df823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 30 Dec 2024 17:49:24 +0200 Subject: [PATCH 12/13] Don't inherit docstrings --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 9d2fe6c..dc3799d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,6 +27,7 @@ exclude_patterns = ["_build"] pygments_style = "sphinx" autodoc_default_options = {"members": True, "show-inheritance": True} +autodoc_inherit_docstrings = False highlight_language = "python3" todo_include_todos = False From 6874cbe91edb9539f44f1a42bcfc9b90af5a95d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 1 Jan 2025 04:03:37 +0200 Subject: [PATCH 13/13] Removed commented out fixture --- tests/conftest.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index d200d98..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -# import pytest -# -# -# @pytest.fixture -# def anyio_backend() -> str: -# return "asyncio"