Skip to content

Commit cc6aa20

Browse files
authored
Release v4.4.0
2 parents d012371 + 5c60306 commit cc6aa20

File tree

11 files changed

+640
-16
lines changed

11 files changed

+640
-16
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
=========
33

4+
## v4.4.0 (2023-02-21)
5+
6+
### Enhancements
7+
8+
* Sub-exceptions will now be reported when an `ExceptionGroup` or `BaseExceptionGroup` is passed to `bugsnag.notify`. This includes support for the backports provided by the `exceptiongroup` package
9+
[#332](https://github.com/bugsnag/bugsnag-python/pull/332)
10+
[#338](https://github.com/bugsnag/bugsnag-python/pull/338)
11+
412
## v4.3.0 (2022-11-02)
513

614
### Enhancements

bugsnag/event.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717

1818
__all__ = ('Event',)
1919

20+
if sys.version_info < (3, 11):
21+
try:
22+
from exceptiongroup import BaseExceptionGroup
23+
except ImportError:
24+
# we're on Python < 3.11 and exceptiongroup isn't installed
25+
# an empty tuple can be passed to 'isinstance' safely and will
26+
# always return false, so we default to that
27+
BaseExceptionGroup = ()
28+
2029

2130
class Event:
2231
"""
@@ -254,6 +263,21 @@ def _generate_error_list(
254263
)
255264
)
256265

266+
# unwrap BaseExceptionGroups so that their contained exceptions are
267+
# also reported
268+
# we don't recurse into nested BaseExceptionGroups or cause/context
269+
# here because there's a big risk of that leading to a huge number of
270+
# exceptions, which is difficult to reason about
271+
if isinstance(self._original_error, BaseExceptionGroup):
272+
for sub_exception in self._original_error.exceptions: # type: ignore # noqa
273+
error_list.append(
274+
Error(
275+
class_name(sub_exception),
276+
str(sub_exception),
277+
self._generate_stacktrace(sub_exception.__traceback__)
278+
)
279+
)
280+
257281
return error_list
258282

259283
def _generate_stacktrace(

bugsnag/utils.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,13 +240,17 @@ def is_json_content_type(value: str) -> bool:
240240
return type == 'application' and (subtype == 'json' or suffix == 'json')
241241

242242

243+
_ignore_modules = ('__main__', 'builtins')
244+
245+
243246
def fully_qualified_class_name(obj):
244247
module = inspect.getmodule(obj)
245-
if module is not None and module.__name__ != "__main__":
246-
return module.__name__ + "." + obj.__class__.__name__
247-
else:
248+
249+
if module is None or module.__name__ in _ignore_modules:
248250
return obj.__class__.__name__
249251

252+
return module.__name__ + '.' + obj.__class__.__name__
253+
250254

251255
def package_version(package_name):
252256
try:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
setup(
1616
name='bugsnag',
17-
version='4.3.0',
17+
version='4.4.0',
1818
description='Automatic error monitoring for django, flask, etc.',
1919
long_description=__doc__,
2020
author='Simon Maynard',

tests/conftest.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import pytest
2-
from bugsnag import Breadcrumbs
2+
import bugsnag
3+
import bugsnag.legacy as global_setup
4+
from tests.utils import FakeBugsnagServer
35

46

57
# resize the breadcrumb list to 0 before each test to prevent tests from
68
# interfering with eachother
79
@pytest.fixture(autouse=True)
810
def reset_breadcrumbs():
9-
Breadcrumbs(0).resize(0)
11+
bugsnag.Breadcrumbs(0).resize(0)
12+
13+
14+
@pytest.fixture
15+
def bugsnag_server():
16+
server = FakeBugsnagServer(wait_for_duplicate_requests=False)
17+
bugsnag.configure(endpoint=server.url, api_key='3874876376238728937')
18+
19+
yield server
20+
21+
# Reset shared client config
22+
global_setup.configuration = bugsnag.Configuration()
23+
global_setup.default_client.configuration = global_setup.configuration
24+
global_setup.default_client.uninstall_sys_hook()
25+
26+
server.shutdown()

tests/fixtures/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# flake8: noqa
2+
import sys
3+
from ..utils import is_exception_group_supported
24

35
from .start_and_end_of_file import (
46
start_of_file,
@@ -13,3 +15,12 @@
1315
exception_with_no_cause,
1416
raise_exception_with_no_cause,
1517
)
18+
19+
if is_exception_group_supported:
20+
from .exception_groups import (
21+
exception_group_with_no_cause,
22+
base_exception_group_subclass,
23+
exception_group_with_nested_group,
24+
exception_group_with_implicit_cause,
25+
exception_group_with_explicit_cause,
26+
)

tests/fixtures/exception_groups.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# flake8: noqa
2+
try:
3+
from exceptiongroup import ExceptionGroup, BaseExceptionGroup # noqa
4+
except ImportError:
5+
# if we're here and 'exceptiongroup' isn't installed, it must mean we're on
6+
# Python 3.11+ and have support natively
7+
pass
8+
9+
from .caused_by import exception_with_explicit_cause
10+
11+
# this creates an exception with a very small __traceback__, so it's easier to
12+
# assert against in tests
13+
def generate_exception(exception_class, message):
14+
try:
15+
raise exception_class(message)
16+
except BaseException as exception:
17+
return exception
18+
19+
20+
def raise_exception_group_with_no_cause():
21+
raise ExceptionGroup('the message of the group', [generate_exception(Exception, 'exception #1'), generate_exception(ArithmeticError, 'exception #2'), generate_exception(NameError, 'exception #3'), generate_exception(AssertionError, 'exception #4')])
22+
23+
24+
try:
25+
raise_exception_group_with_no_cause()
26+
except BaseExceptionGroup as exception_group:
27+
exception_group_with_no_cause = exception_group
28+
29+
30+
class MyExceptionGroup(BaseExceptionGroup):
31+
pass
32+
33+
34+
def raise_base_exception_group_subclass_with_no_cause():
35+
raise MyExceptionGroup('my very easy method just speeds up (n)making exception groups', [generate_exception(GeneratorExit, 'exception #1'), generate_exception(ReferenceError, 'exception #2'), generate_exception(NotImplementedError, 'exception #3')])
36+
37+
38+
try:
39+
raise_base_exception_group_subclass_with_no_cause()
40+
except BaseExceptionGroup as exception_group:
41+
base_exception_group_subclass = exception_group
42+
43+
44+
def raise_exception_group_with_nested_group():
45+
raise ExceptionGroup('the message of the group', [generate_exception(Exception, 'exception #1'), exception_group_with_no_cause, generate_exception(ArithmeticError, 'exception #3')])
46+
47+
48+
try:
49+
raise_exception_group_with_nested_group()
50+
except BaseExceptionGroup as exception_group:
51+
exception_group_with_nested_group = exception_group
52+
53+
54+
def raise_exception_group_with_implicit_cause():
55+
try:
56+
raise_exception_group_with_nested_group()
57+
except BaseExceptionGroup as exception_group:
58+
raise ExceptionGroup('group with implicit cause', [exception_with_explicit_cause, generate_exception(NameError, 'exception #2')])
59+
60+
61+
try:
62+
raise_exception_group_with_implicit_cause()
63+
except BaseExceptionGroup as exception_group:
64+
exception_group_with_implicit_cause = exception_group
65+
66+
67+
def raise_exception_group_with_explicit_cause():
68+
try:
69+
raise_exception_group_with_implicit_cause()
70+
except BaseExceptionGroup as exception_group:
71+
raise ExceptionGroup('group with explicit cause', [generate_exception(NameError, 'exception #1'), exception_with_explicit_cause]) from exception_group
72+
73+
74+
try:
75+
raise_exception_group_with_explicit_cause()
76+
except BaseExceptionGroup as exception_group:
77+
exception_group_with_explicit_cause = exception_group

tests/integrations/conftest.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import pytest
2-
from tests.utils import FakeBugsnagServer
3-
4-
import bugsnag.legacy as global_setup
52
import bugsnag
3+
import bugsnag.legacy as global_setup
4+
from tests.utils import FakeBugsnagServer
65

76

87
@pytest.fixture
98
def bugsnag_server():
10-
server = FakeBugsnagServer()
9+
server = FakeBugsnagServer(wait_for_duplicate_requests=True)
1110
bugsnag.configure(endpoint=server.url, api_key='3874876376238728937')
1211

1312
yield server

0 commit comments

Comments
 (0)