MEASUREMENT
bench/ctxmgr-async/exceptiongroup_suppress.py
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/python/context-managers
- How to run it
Времени здесь не измеряется вообще: все четыре скрипта смотрят на порядок вызовов, тип вышедшего наружу объекта и текст сообщений. Такие наблюдения честны между любыми сборками — в отличие от наносекунд, которые в этом репозитории между версиями не сравниваются. ## Главный результат: `suppress` и `ExceptionGroup` **Утверждение подтвердилось.** `contextlib.suppress` научился подавлять исключение *внутри* `BaseExceptionGroup` в **3.12** — и ни строчкой раньше. Граница видна и в исходнике, и в поведении. | | 3.11.15 | 3.12.3 | 3.13.7 | 3.14.7 | | --- | --- | --- | --- | --- | | ветка `BaseExceptionGroup` в `suppress.__exit__` | нет | **есть** | есть | есть | | `suppress(ValueError)` над `ExceptionGroup([ValueError])` | группа выходит наружу | **подавлено** | подавлено | подавлено | | над `ExceptionGroup([ValueError, TypeError])` | наружу оба | **наружу только TypeError** | только TypeError | только TypeError | | наружу вышел тот же объект, что бросали | **True** | False | False | False | | `ValueError` во вложенной группе | остаётся | **вырезан** | вырезан | вырезан | | `BaseExceptionGroup.split` существует | да | да | да | да | Последняя строка важна отдельно: `split` был на месте с 3.11. Изменился не он, а `suppress`, который начал его звать. Дословный вывод, 3.11 (полностью) — и он же на 3.12+ с отличиями по таблице:
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры: контекстные менеджеры в эпоху ExceptionGroup и async
Четыре сюжета, которых нет в bench/context-managers/: подавление внутри
группы исключений, ExitStack/AsyncExitStack, асинхронный протокол
__aenter__/__aexit__ и разбор того, что именно 3.12 сделала с contextlib.
| скрипт | что показывает |
|---|---|
exceptiongroup_suppress.py |
__exit__ -> True глушит группу целиком; suppress(ValueError) вырезает ветку из группы — но только с 3.12 |
exitstack.py |
обратный порядок выхода, частичный отказ в __enter__, подмена исключения внутренним __exit__, pop_all |
async_protocol.py |
минимальный __aenter__/__aexit__, подавление в __aexit__, перекрёстные TypeError, AsyncExitStack |
what_changed_312.py |
из чего собирается строка 3.12 в истории версий: suppress + переписанный @contextmanager |
Запуск:
for v in 3.11 3.12 3.13 3.14; do
echo "== $v"; python$v bench/ctxmgr-async/exceptiongroup_suppress.py
done
Времени здесь не измеряется вообще: все четыре скрипта смотрят на порядок вызовов, тип вышедшего наружу объекта и текст сообщений. Такие наблюдения честны между любыми сборками — в отличие от наносекунд, которые в этом репозитории между версиями не сравниваются.
Главный результат: suppress и ExceptionGroup
Утверждение подтвердилось. contextlib.suppress научился подавлять
исключение внутри BaseExceptionGroup в 3.12 — и ни строчкой раньше.
Граница видна и в исходнике, и в поведении.
| 3.11.15 | 3.12.3 | 3.13.7 | 3.14.7 | |
|---|---|---|---|---|
ветка BaseExceptionGroup в suppress.__exit__ |
нет | есть | есть | есть |
suppress(ValueError) над ExceptionGroup([ValueError]) |
группа выходит наружу | подавлено | подавлено | подавлено |
над ExceptionGroup([ValueError, TypeError]) |
наружу оба | наружу только TypeError | только TypeError | только TypeError |
| наружу вышел тот же объект, что бросали | True | False | False | False |
ValueError во вложенной группе |
остаётся | вырезан | вырезан | вырезан |
BaseExceptionGroup.split существует |
да | да | да | да |
Последняя строка важна отдельно: split был на месте с 3.11. Изменился не он,
а suppress, который начал его звать.
Дословный вывод, 3.11 (полностью) — и он же на 3.12+ с отличиями по таблице:
$ python3.11 exceptiongroup_suppress.py
PY 3.11.15
1) __exit__ -> True над ExceptionGroup
__exit__ увидел тип: ExceptionGroup
__exit__ увидел вложенные: ['ValueError', 'TypeError']
выполнение продолжилось: группа проглочена целиком, обе ветки
2) with suppress(ValueError): raise ExceptionGroup('eg', [ValueError])
РЕЗУЛЬТАТ: наружу вышло ExceptionGroup 'eg (1 sub-exception)' вложенные: ['ValueError']
3) with suppress(ValueError): raise ExceptionGroup('mix', [ValueError, TypeError])
РЕЗУЛЬТАТ: наружу вышло ExceptionGroup 'mix (2 sub-exceptions)'
вложенные: ['ValueError', 'TypeError']
это тот же объект, что бросали: True
4) ValueError лежит во ВЛОЖЕННОЙ группе
РЕЗУЛЬТАТ: наружу вышло ExceptionGroup('outer')[ExceptionGroup('inner')[ValueError, KeyError]]
5) контроль: обычный ValueError без группы
подавлено
6) BaseExceptionGroup.split(ValueError) доступен и на 3.11:
match: ['ValueError']
rest : ['TypeError']
rest is g: False | сообщение сохранено: True
7) есть ли ветка BaseExceptionGroup в Lib/contextlib.py: False
файл: /usr/lib/python3.11/contextlib.py
строка класса suppress: 429
8) except* ValueError над той же группой
поймано except*: ExceptionGroup ['ValueError']
осталось непойманным: ExceptionGroup ['TypeError']
Те же пункты на 3.14.7:
$ python3.14 exceptiongroup_suppress.py
PY 3.14.7
1) __exit__ -> True над ExceptionGroup
__exit__ увидел тип: ExceptionGroup
__exit__ увидел вложенные: ['ValueError', 'TypeError']
выполнение продолжилось: группа проглочена целиком, обе ветки
2) with suppress(ValueError): raise ExceptionGroup('eg', [ValueError])
РЕЗУЛЬТАТ: подавлено, выполнение продолжилось
3) with suppress(ValueError): raise ExceptionGroup('mix', [ValueError, TypeError])
РЕЗУЛЬТАТ: наружу вышло ExceptionGroup 'mix (1 sub-exception)'
вложенные: ['TypeError']
это тот же объект, что бросали: False
4) ValueError лежит во ВЛОЖЕННОЙ группе
РЕЗУЛЬТАТ: наружу вышло ExceptionGroup('outer')[ExceptionGroup('inner')[KeyError]]
5) контроль: обычный ValueError без группы
подавлено
6) BaseExceptionGroup.split(ValueError) доступен и на 3.11:
match: ['ValueError']
rest : ['TypeError']
rest is g: False | сообщение сохранено: True
7) есть ли ветка BaseExceptionGroup в Lib/contextlib.py: True
файл: .../cpython-3.14.7-linux-x86_64-gnu/lib/python3.14/contextlib.py
строка класса suppress: 433
8) except* ValueError над той же группой
поймано except*: ExceptionGroup ['ValueError']
осталось непойманным: ExceptionGroup ['TypeError']
(3.12.3 и 3.13.7 дают ровно те же строки; различаются только PY, путь к
contextlib.py и номер строки класса — 429 на 3.12, 433 на 3.13 и 3.14.)
Что здесь стоит прочитать вместе. Пункт 1 и пункт 3 — это две разные операции,
которые в разговорах называют одним словом «подавить». Возврат истины из
__exit__ убирает группу, включая ветки, о которых менеджер не знает.
suppress с 3.12 убирает ветку, а остаток пересобирает в новую группу —
поэтому rest is g даёт False, а сообщение группы сохраняется.
Первоисточники
Раздела «contextlib» в «Что нового в 3.12» нет вовсе — проверено поиском по
Doc/whatsnew/3.12.rst (тег v3.14.5): ноль вхождений слова contextlib.
Изменение задокументировано в двух других местах:
Doc/library/contextlib.rst, строки 279–325 (тегv3.14.5), уsuppress:If the code within the
withblock raises aBaseExceptionGroup, suppressed exceptions are removed from the group. Any exceptions of the group which are not suppressed are re-raised in a new group which is created using the original group'sderivemethod... versionchanged:: 3.12—suppressnow supports suppressing exceptions raised as part of aBaseExceptionGroup.Перевод: «Если код внутри блока
withвозбуждаетBaseExceptionGroup, подавляемые исключения удаляются из группы. Те исключения группы, которые не подавлены, возбуждаются заново в новой группе, созданной методомderiveисходной группы.» / «Изменено в версии 3.12:suppressтеперь поддерживает подавление исключений, возбуждённых в составеBaseExceptionGroup.»Misc/NEWS.d/3.12.0b1.rst, записьgh-issue: 103791,date: 2023-04-24:contextlib.suppressnow supports suppressing exceptions raised as part of anExceptionGroup. If other exceptions exist on the group, they are re-raised in a group that does not contain the suppressed exceptions.Перевод: «
contextlib.suppressтеперь поддерживает подавление исключений, возбуждённых в составеExceptionGroup. Если в группе есть другие исключения, они возбуждаются заново в группе, не содержащей подавленных.»
Реализация — Lib/contextlib.py, класс suppress, метод __exit__
(строка 429 на 3.11 и 3.12, строка 433 на 3.13.7 и 3.14.7). Разница между
3.11 и 3.12 в одном методе целиком:
# 3.11
return exctype is not None and issubclass(exctype, self._exceptions)
# 3.12+
if exctype is None:
return
if issubclass(exctype, self._exceptions):
return True
if issubclass(exctype, BaseExceptionGroup):
match, rest = excinst.split(self._exceptions)
if rest is None:
return True
raise rest
return False
Правило except* — Doc/reference/compound_stmts.rst, строки 333–395
(тег v3.14.5):
When an exception group is raised in the try block, each
except*clause splits (seeBaseExceptionGroup.split) it into the subgroups of matching and non-matching exceptions.
Перевод: «Когда в блоке try возбуждается группа исключений, каждое предложение
except* разделяет её (см. BaseExceptionGroup.split) на подгруппы
подходящих и неподходящих исключений.» — то есть suppress с 3.12 делает
ровно то же самое, что except*, только без синтаксиса.
ExitStack / AsyncExitStack
Вывод exitstack.py одинаков на всех четырёх версиях (различается только
строка PY):
$ python3.13 exitstack.py
PY 3.13.7
1) порядок при обычном выходе:
enter a
enter b
enter c
exit c (видит None)
exit b (видит None)
exit a (видит None)
2) третий менеджер упал в __enter__: c не открылся
enter a
enter b
enter c -> ОШИБКА
exit b (видит RuntimeError)
exit a (видит RuntimeError)
d не открывался и не закрывался; a и b закрыты в обратном порядке
3) наружу вышло ValueError: из тела
enter a
enter b
exit b (видит ValueError)
exit a (видит ValueError)
4) inner вернул True — блок завершился без исключения:
enter outer
enter inner
exit inner (видит ValueError)
exit outer (видит None)
outer увидел None, потому что inner уже подавил
5) наружу вышло: KeyError 'подмена'
__context__: ValueError из тела
enter outer
enter inner
exit inner (видит ValueError)
exit outer (видит KeyError)
outer увидел уже KeyError, а не исходный ValueError
6) после выхода из with, но до handle.close(): []
после handle.close(): ['callback выполнен']
pop_all переносит зарегистрированную уборку в другой объект
7) enter_context(object()) -> TypeError: 'builtins.object' object does not support the context manager protocol
Пункты 4 и 5 — это и есть та фраза документации, ради которой стоит писать про
ExitStack отдельно (Doc/library/contextlib.rst, строки 541–546, тег
v3.14.5):
Since registered callbacks are invoked in the reverse order of registration, this ends up behaving as if multiple nested
withstatements had been used with the registered set of callbacks. This even extends to exception handling - if an inner callback suppresses or replaces an exception, then outer callbacks will be passed arguments based on that updated state.
Перевод: «Поскольку зарегистрированные обработчики вызываются в порядке,
обратном регистрации, это ведёт себя так, будто использовалось несколько
вложенных инструкций with с зарегистрированным набором обработчиков. Это
распространяется даже на обработку исключений: если внутренний обработчик
подавляет или заменяет исключение, внешним обработчикам будут переданы
аргументы, отражающие это обновлённое состояние.»
Про AsyncExitStack — там же, строки 623–631:
An asynchronous context manager, similar to
ExitStack, that supports combining both synchronous and asynchronous context managers, as well as having coroutines for cleanup logic. Theclosemethod is not implemented;aclosemust be used instead.
Перевод: «Асинхронный контекстный менеджер, похожий на ExitStack, который
поддерживает объединение синхронных и асинхронных контекстных менеджеров, а
также корутины в качестве логики уборки. Метод close не реализован; вместо
него нужно использовать aclose.» Это проверено пунктом 7 в async_protocol.py.
Асинхронный протокол
Вывод async_protocol.py одинаков на 3.11, 3.12 и 3.13; на 3.14 меняются два
сообщения об ошибке.
$ python3.13 async_protocol.py
PY 3.13.7
1) минимальный асинхронный менеджер:
aenter a
тело, as = 'ресурс-a'
aexit a (видит None)
as получает возвращённое значение __aenter__, как и в with
2) __aexit__ вернул True:
aenter b
aexit b (видит ValueError)
выполнение продолжилось — истина подавляет, как и в __exit__
3a) async with над синхронным менеджером -> TypeError: 'SyncOnly' object does not support the asynchronous context manager protocol
3b) with над асинхронным менеджером -> TypeError: 'AsyncOnly' object does not support the context manager protocol
4) чередование с фоновой задачей: ['фон-0', 'тело начало', 'фон-1', 'тело кончило', 'фон-2']
между входом и выходом цикл событий отдавал управление другой задаче
5) @asynccontextmanager: ['до yield d', 'тело: ресурс-d', 'после yield d']
6) AsyncExitStack держит и синхронные, и асинхронные:
aenter A1
enter S1
aenter A2
aexit A2 (видит None)
exit S1
aexit A1 (видит None)
выход в обратном порядке регистрации, независимо от вида менеджера
7) у AsyncExitStack есть aclose: True | унаследованный close: False
На 3.14.7 пункт 3 звучит иначе — и это продолжение той же перестановки
LOAD_SPECIAL, о которой уже написано в bench/context-managers/README.md:
3a) async with над синхронным менеджером -> TypeError: 'SyncOnly' object does not support the asynchronous context manager protocol (missed __aexit__ method) but it supports the context manager protocol. Did you mean to use 'with'?
3b) with над асинхронным менеджером -> TypeError: 'AsyncOnly' object does not support the context manager protocol (missed __exit__ method) but it supports the asynchronous context manager protocol. Did you mean to use 'async with'?
сообщение TypeError |
3.11 – 3.13 | 3.14.7 |
|---|---|---|
async with над синхронным менеджером |
без подсказки | + «but it supports the context manager protocol. Did you mean to use 'with'?» |
with над асинхронным менеджером |
без подсказки | + «but it supports the asynchronous context manager protocol. Did you mean to use 'async with'?» |
Первоисточник протокола — Doc/reference/datamodel.rst, строки 3947–3979
(тег v3.14.5):
object.__aenter__(self)— Semantically similar to__enter__(), the only difference being that it must return an awaitable.object.__aexit__(self, exc_type, exc_value, traceback)— Semantically similar to__exit__(), the only difference being that it must return an awaitable.
Перевод: «object.__aenter__(self) — семантически похож на __enter__(), с
единственным отличием: должен вернуть ожидаемый объект.
object.__aexit__(self, exc_type, exc_value, traceback) — семантически похож
на __exit__(), с единственным отличием: должен вернуть ожидаемый объект.»
И перевод инструкции — Doc/reference/compound_stmts.rst, строки 1592–1629:
manager = (EXPRESSION)
aenter = manager.__aenter__
aexit = manager.__aexit__
value = await aenter()
hit_except = False
try:
TARGET = value
SUITE
except:
hit_except = True
if not await aexit(*sys.exc_info()):
raise
finally:
if not hit_except:
await aexit(None, None, None)
Строка if not await aexit(...): raise — тот же механизм подавления, что и в
синхронном варианте из PEP 343, с точностью до await. Пункт 2 замера это и
показывает.
Что 3.12 сделала с contextlib
what_changed_312.py собирает строку 3.12 для истории версий из двух фактов,
потому что раздела contextlib в whatsnew 3.12 не существует.
| 3.11.15 | 3.12.3 | 3.13.7 | 3.14.7 | |
|---|---|---|---|---|
ветка BaseExceptionGroup в suppress.__exit__ |
нет | есть | есть | есть |
@contextmanager зовёт gen.throw(typ, value, tb) |
да (3 аргумента) | нет | нет | нет |
@contextmanager зовёт gen.throw(value) |
нет | да (1 аргумент) | да | да |
прямой gen.throw(typ, value, tb) даёт DeprecationWarning |
нет | да | да | да |
| сколько аргументов contextlib передал шпиону | 3 | 1 | 1 | 1 |
у AbstractContextManager есть __slots__ |
нет | нет | да | да |
$ python3.11 what_changed_312.py
PY 3.11.15
1) suppress.__exit__ знает про BaseExceptionGroup: False
(подробности поведения — в exceptiongroup_suppress.py)
2) _GeneratorContextManager.__exit__ вызывает gen.throw:
3-аргументную форму throw(typ, value, tb): True
1-аргументную форму throw(value): False
файл: /usr/lib/python3.11/contextlib.py
3) прямой вызов gen.throw(typ, value, tb): принят
предупреждений: []
4) прямой вызов gen.throw(value): предупреждений 0
5) сколько аргументов contextlib передал в gen.throw: [3]
3 на 3.11, 1 начиная с 3.12 — это и есть переписанный @contextmanager
6) у contextlib.AbstractContextManager есть __slots__: False
(это уже 3.13, не 3.12 — приведено, чтобы 3.12 не приписали чужое)
$ python3.12 what_changed_312.py
PY 3.12.3
1) suppress.__exit__ знает про BaseExceptionGroup: True
(подробности поведения — в exceptiongroup_suppress.py)
2) _GeneratorContextManager.__exit__ вызывает gen.throw:
3-аргументную форму throw(typ, value, tb): False
1-аргументную форму throw(value): True
файл: /usr/lib/python3.12/contextlib.py
3) прямой вызов gen.throw(typ, value, tb): принят
предупреждений: [('DeprecationWarning', 'the (type, exc, tb) signature of throw() is deprecated, use the single-arg signature instead.')]
4) прямой вызов gen.throw(value): предупреждений 0
5) сколько аргументов contextlib передал в gen.throw: [1]
3 на 3.11, 1 начиная с 3.12 — это и есть переписанный @contextmanager
6) у contextlib.AbstractContextManager есть __slots__: False
(это уже 3.13, не 3.12 — приведено, чтобы 3.12 не приписали чужое)
3.13.7 и 3.14.7 повторяют вывод 3.12 с одной поправкой: пункт 6 даёт True.
Второй факт задокументирован в Doc/whatsnew/3.12.rst, раздел Deprecated,
строки 1328–1330 (тег v3.14.5):
The 3-arg signatures (type, value, traceback) of
coroutine throw(),generator throw()andasync generator throw()are deprecated and may be removed in a future version of Python. Use the single-arg versions of these functions instead. (Contributed by Ofey Chan in gh:89874.)
Перевод: «3-аргументные сигнатуры (тип, значение, трассировка) методов
throw() у корутины, генератора и асинхронного генератора объявлены
устаревшими и могут быть удалены в будущей версии Python. Вместо них следует
использовать одноаргументные версии этих функций.»
Пункт 5 — практическое следствие: @contextmanager перестал сам вызывать
устаревшую форму. Обёртка вокруг генератора, перехватывающая throw, увидит
разное число аргументов на 3.11 и на 3.12+.
Оговорка про версию
3.14 здесь — это 3.14.7. Все факты, взятые с неё, поведенческие:
текст сообщения об ошибке, наличие ветки в исходнике, число аргументов в
вызове. От кандидата к финалу такие вещи меняться не должны — но «не должны»
это не «проверено», поэтому там, где строки попадают в урок, версия
называется полностью.
Тег исходников CPython, по которому цитируются Doc/ и Misc/, — v3.14.5.
Файлы Lib/contextlib.py читаются прямо у установленных интерпретаторов
(3.11.15, 3.12.3, 3.13.7, 3.14.7), поэтому номера строк в выводе — из них,
а не из тега.
Script
116 lines"""Подавление исключений в эпоху ExceptionGroup: что умеет __exit__ и что умеет suppress.
Один сюжет: граница между «менеджер подавляет ИСКЛЮЧЕНИЕ» и «менеджер подавляет
ЧАСТЬ ГРУППЫ». Первое было всегда, второе появилось в 3.12 и только у suppress.
Запускать: python3.11 / 3.12 / 3.13 / 3.14 — вывод различается.
"""
import contextlib
import sys
print("PY", sys.version.split()[0])
print()
# --- 1. __exit__, вернувший истину, глушит ГРУППУ ЦЕЛИКОМ ------------------
# Протокол не изменился: __exit__ получает (тип, значение, traceback) одного
# объекта. Если этот объект — ExceptionGroup, то истина убирает всю группу,
# включая те исключения, о которых менеджер ничего не знает.
class SwallowAll:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
print(" __exit__ увидел тип:", exc_type.__name__ if exc_type else None)
if exc is not None:
print(" __exit__ увидел вложенные:", [type(e).__name__ for e in exc.exceptions])
return True
print("1) __exit__ -> True над ExceptionGroup")
with SwallowAll():
raise ExceptionGroup("eg", [ValueError("v"), TypeError("t")])
print(" выполнение продолжилось: группа проглочена целиком, обе ветки")
print()
# --- 2. suppress(ValueError) и группа --------------------------------------
# Здесь и проходит граница версий.
print("2) with suppress(ValueError): raise ExceptionGroup('eg', [ValueError])")
try:
with contextlib.suppress(ValueError):
raise ExceptionGroup("eg", [ValueError("v")])
print(" РЕЗУЛЬТАТ: подавлено, выполнение продолжилось")
except BaseException as e: # noqa: BLE001
print(" РЕЗУЛЬТАТ: наружу вышло", type(e).__name__, repr(str(e)),
"вложенные:", [type(x).__name__ for x in getattr(e, "exceptions", ())])
print()
# --- 3. Смешанная группа: что остаётся снаружи ------------------------------
print("3) with suppress(ValueError): raise ExceptionGroup('mix', [ValueError, TypeError])")
original = ExceptionGroup("mix", [ValueError("v"), TypeError("t")])
try:
with contextlib.suppress(ValueError):
raise original
print(" РЕЗУЛЬТАТ: подавлено целиком")
except BaseException as e: # noqa: BLE001
print(" РЕЗУЛЬТАТ: наружу вышло", type(e).__name__, repr(str(e)))
print(" вложенные:", [type(x).__name__ for x in getattr(e, "exceptions", ())])
print(" это тот же объект, что бросали:", e is original)
print()
# --- 4. Вложенная группа ----------------------------------------------------
print("4) ValueError лежит во ВЛОЖЕННОЙ группе")
try:
with contextlib.suppress(ValueError):
raise ExceptionGroup("outer", [ExceptionGroup("inner", [ValueError("deep"), KeyError("k")])])
print(" РЕЗУЛЬТАТ: подавлено целиком")
except BaseException as e: # noqa: BLE001
def shape(x, depth=0):
if isinstance(x, BaseExceptionGroup):
return f"{type(x).__name__}({x.args[0]!r})[" + ", ".join(shape(i, depth + 1) for i in x.exceptions) + "]"
return type(x).__name__
print(" РЕЗУЛЬТАТ: наружу вышло", shape(e))
print()
# --- 5. Обычное исключение suppress подавляет на всех версиях одинаково -----
print("5) контроль: обычный ValueError без группы")
with contextlib.suppress(ValueError):
raise ValueError("plain")
print(" подавлено")
print()
# --- 6. Что именно делает 3.12: split ---------------------------------------
# Реализация в Lib/contextlib.py опирается на BaseExceptionGroup.split.
# Сам split есть на всех версиях 3.11+ — изменился не он, а suppress.
g = ExceptionGroup("mix", [ValueError("v"), TypeError("t")])
match, rest = g.split(ValueError)
print("6) BaseExceptionGroup.split(ValueError) доступен и на 3.11:")
print(" match:", [type(x).__name__ for x in match.exceptions])
print(" rest :", [type(x).__name__ for x in rest.exceptions])
print(" rest is g:", rest is g, "| сообщение сохранено:", rest.args[0] == g.args[0])
print()
# --- 7. Ветка BaseExceptionGroup в исходнике suppress ------------------------
import inspect # noqa: E402
body = inspect.getsource(contextlib.suppress.__exit__)
print("7) есть ли ветка BaseExceptionGroup в Lib/contextlib.py:",
"BaseExceptionGroup" in body)
print(" файл:", inspect.getsourcefile(contextlib.suppress))
print(" строка класса suppress:", inspect.getsourcelines(contextlib.suppress)[1])
print()
# --- 8. except* рядом: он делает то же самое, но синтаксисом ----------------
print("8) except* ValueError над той же группой")
try:
try:
raise ExceptionGroup("mix", [ValueError("v"), TypeError("t")])
except* ValueError as eg:
print(" поймано except*:", type(eg).__name__,
[type(x).__name__ for x in eg.exceptions])
except BaseException as e: # noqa: BLE001
print(" осталось непойманным:", type(e).__name__,
[type(x).__name__ for x in getattr(e, "exceptions", ())])