@ajksunkang-aios/kgraph-linux-x64 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kgraph-launcher +15 -3
- package/lib/kgraph/scripts/build-bundle.sh +17 -4
- package/lib/site-packages/outcome/__init__.py +20 -0
- package/lib/site-packages/outcome/_impl.py +239 -0
- package/lib/site-packages/outcome/_util.py +33 -0
- package/lib/site-packages/outcome/_version.py +7 -0
- package/lib/site-packages/outcome/py.typed +0 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE +3 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.MIT +20 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/METADATA +63 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/RECORD +13 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/WHEEL +6 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/top_level.txt +1 -0
- package/lib/site-packages/sniffio/__init__.py +17 -0
- package/lib/site-packages/sniffio/_impl.py +95 -0
- package/lib/site-packages/sniffio/_tests/__init__.py +0 -0
- package/lib/site-packages/sniffio/_tests/test_sniffio.py +84 -0
- package/lib/site-packages/sniffio/_version.py +3 -0
- package/lib/site-packages/sniffio/py.typed +0 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/INSTALLER +1 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE +3 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.MIT +20 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/METADATA +104 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/RECORD +14 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/WHEEL +5 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/top_level.txt +1 -0
- package/lib/site-packages/sortedcontainers/__init__.py +74 -0
- package/lib/site-packages/sortedcontainers/sorteddict.py +812 -0
- package/lib/site-packages/sortedcontainers/sortedlist.py +2646 -0
- package/lib/site-packages/sortedcontainers/sortedset.py +733 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/LICENSE +13 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/METADATA +264 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/RECORD +10 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/WHEEL +6 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/top_level.txt +1 -0
- package/lib/site-packages/trio/__init__.py +133 -0
- package/lib/site-packages/trio/__main__.py +3 -0
- package/lib/site-packages/trio/_abc.py +714 -0
- package/lib/site-packages/trio/_channel.py +610 -0
- package/lib/site-packages/trio/_core/__init__.py +94 -0
- package/lib/site-packages/trio/_core/_asyncgens.py +243 -0
- package/lib/site-packages/trio/_core/_concat_tb.py +26 -0
- package/lib/site-packages/trio/_core/_entry_queue.py +223 -0
- package/lib/site-packages/trio/_core/_exceptions.py +169 -0
- package/lib/site-packages/trio/_core/_generated_instrumentation.py +50 -0
- package/lib/site-packages/trio/_core/_generated_io_epoll.py +98 -0
- package/lib/site-packages/trio/_core/_generated_io_kqueue.py +153 -0
- package/lib/site-packages/trio/_core/_generated_io_windows.py +204 -0
- package/lib/site-packages/trio/_core/_generated_run.py +269 -0
- package/lib/site-packages/trio/_core/_generated_windows_ffi.py +10 -0
- package/lib/site-packages/trio/_core/_instrumentation.py +117 -0
- package/lib/site-packages/trio/_core/_io_common.py +31 -0
- package/lib/site-packages/trio/_core/_io_epoll.py +385 -0
- package/lib/site-packages/trio/_core/_io_kqueue.py +292 -0
- package/lib/site-packages/trio/_core/_io_windows.py +1036 -0
- package/lib/site-packages/trio/_core/_ki.py +271 -0
- package/lib/site-packages/trio/_core/_local.py +104 -0
- package/lib/site-packages/trio/_core/_mock_clock.py +165 -0
- package/lib/site-packages/trio/_core/_parking_lot.py +317 -0
- package/lib/site-packages/trio/_core/_run.py +3148 -0
- package/lib/site-packages/trio/_core/_run_context.py +15 -0
- package/lib/site-packages/trio/_core/_tests/__init__.py +0 -0
- package/lib/site-packages/trio/_core/_tests/test_asyncgen.py +339 -0
- package/lib/site-packages/trio/_core/_tests/test_cancelled.py +222 -0
- package/lib/site-packages/trio/_core/_tests/test_exceptiongroup_gc.py +103 -0
- package/lib/site-packages/trio/_core/_tests/test_guest_mode.py +755 -0
- package/lib/site-packages/trio/_core/_tests/test_instrumentation.py +315 -0
- package/lib/site-packages/trio/_core/_tests/test_io.py +522 -0
- package/lib/site-packages/trio/_core/_tests/test_ki.py +703 -0
- package/lib/site-packages/trio/_core/_tests/test_local.py +118 -0
- package/lib/site-packages/trio/_core/_tests/test_mock_clock.py +193 -0
- package/lib/site-packages/trio/_core/_tests/test_parking_lot.py +389 -0
- package/lib/site-packages/trio/_core/_tests/test_run.py +3024 -0
- package/lib/site-packages/trio/_core/_tests/test_thread_cache.py +227 -0
- package/lib/site-packages/trio/_core/_tests/test_tutil.py +13 -0
- package/lib/site-packages/trio/_core/_tests/test_unbounded_queue.py +154 -0
- package/lib/site-packages/trio/_core/_tests/test_windows.py +305 -0
- package/lib/site-packages/trio/_core/_tests/tutil.py +117 -0
- package/lib/site-packages/trio/_core/_tests/type_tests/nursery_start.py +79 -0
- package/lib/site-packages/trio/_core/_tests/type_tests/run.py +51 -0
- package/lib/site-packages/trio/_core/_thread_cache.py +317 -0
- package/lib/site-packages/trio/_core/_traps.py +318 -0
- package/lib/site-packages/trio/_core/_unbounded_queue.py +163 -0
- package/lib/site-packages/trio/_core/_wakeup_socketpair.py +75 -0
- package/lib/site-packages/trio/_core/_windows_cffi.py +313 -0
- package/lib/site-packages/trio/_deprecate.py +171 -0
- package/lib/site-packages/trio/_dtls.py +1380 -0
- package/lib/site-packages/trio/_file_io.py +513 -0
- package/lib/site-packages/trio/_highlevel_generic.py +125 -0
- package/lib/site-packages/trio/_highlevel_open_tcp_listeners.py +251 -0
- package/lib/site-packages/trio/_highlevel_open_tcp_stream.py +397 -0
- package/lib/site-packages/trio/_highlevel_open_unix_stream.py +65 -0
- package/lib/site-packages/trio/_highlevel_serve_listeners.py +148 -0
- package/lib/site-packages/trio/_highlevel_socket.py +423 -0
- package/lib/site-packages/trio/_highlevel_ssl_helpers.py +180 -0
- package/lib/site-packages/trio/_path.py +289 -0
- package/lib/site-packages/trio/_repl.py +159 -0
- package/lib/site-packages/trio/_signals.py +185 -0
- package/lib/site-packages/trio/_socket.py +1326 -0
- package/lib/site-packages/trio/_ssl.py +964 -0
- package/lib/site-packages/trio/_subprocess.py +1178 -0
- package/lib/site-packages/trio/_subprocess_platform/__init__.py +123 -0
- package/lib/site-packages/trio/_subprocess_platform/kqueue.py +48 -0
- package/lib/site-packages/trio/_subprocess_platform/waitid.py +113 -0
- package/lib/site-packages/trio/_subprocess_platform/windows.py +11 -0
- package/lib/site-packages/trio/_sync.py +908 -0
- package/lib/site-packages/trio/_tests/__init__.py +0 -0
- package/lib/site-packages/trio/_tests/astrill-codesigning-cert.cer +0 -0
- package/lib/site-packages/trio/_tests/check_type_completeness.py +247 -0
- package/lib/site-packages/trio/_tests/module_with_deprecations.py +22 -0
- package/lib/site-packages/trio/_tests/pytest_plugin.py +54 -0
- package/lib/site-packages/trio/_tests/test_abc.py +72 -0
- package/lib/site-packages/trio/_tests/test_channel.py +750 -0
- package/lib/site-packages/trio/_tests/test_contextvars.py +56 -0
- package/lib/site-packages/trio/_tests/test_deprecate.py +277 -0
- package/lib/site-packages/trio/_tests/test_deprecate_strict_exception_groups_false.py +64 -0
- package/lib/site-packages/trio/_tests/test_dtls.py +950 -0
- package/lib/site-packages/trio/_tests/test_exports.py +626 -0
- package/lib/site-packages/trio/_tests/test_fakenet.py +317 -0
- package/lib/site-packages/trio/_tests/test_file_io.py +269 -0
- package/lib/site-packages/trio/_tests/test_highlevel_generic.py +98 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_listeners.py +419 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_stream.py +693 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_unix_stream.py +86 -0
- package/lib/site-packages/trio/_tests/test_highlevel_serve_listeners.py +186 -0
- package/lib/site-packages/trio/_tests/test_highlevel_socket.py +336 -0
- package/lib/site-packages/trio/_tests/test_highlevel_ssl_helpers.py +169 -0
- package/lib/site-packages/trio/_tests/test_path.py +279 -0
- package/lib/site-packages/trio/_tests/test_repl.py +428 -0
- package/lib/site-packages/trio/_tests/test_scheduler_determinism.py +47 -0
- package/lib/site-packages/trio/_tests/test_signals.py +186 -0
- package/lib/site-packages/trio/_tests/test_socket.py +1253 -0
- package/lib/site-packages/trio/_tests/test_ssl.py +1371 -0
- package/lib/site-packages/trio/_tests/test_subprocess.py +767 -0
- package/lib/site-packages/trio/_tests/test_sync.py +735 -0
- package/lib/site-packages/trio/_tests/test_testing.py +682 -0
- package/lib/site-packages/trio/_tests/test_testing_raisesgroup.py +1128 -0
- package/lib/site-packages/trio/_tests/test_threads.py +1173 -0
- package/lib/site-packages/trio/_tests/test_timeouts.py +281 -0
- package/lib/site-packages/trio/_tests/test_tracing.py +88 -0
- package/lib/site-packages/trio/_tests/test_trio.py +8 -0
- package/lib/site-packages/trio/_tests/test_unix_pipes.py +288 -0
- package/lib/site-packages/trio/_tests/test_util.py +349 -0
- package/lib/site-packages/trio/_tests/test_wait_for_object.py +225 -0
- package/lib/site-packages/trio/_tests/test_windows_pipes.py +112 -0
- package/lib/site-packages/trio/_tests/tools/__init__.py +0 -0
- package/lib/site-packages/trio/_tests/tools/test_gen_exports.py +179 -0
- package/lib/site-packages/trio/_tests/tools/test_mypy_annotate.py +140 -0
- package/lib/site-packages/trio/_tests/tools/test_sync_requirements.py +80 -0
- package/lib/site-packages/trio/_tests/type_tests/check_wraps.py +9 -0
- package/lib/site-packages/trio/_tests/type_tests/open_memory_channel.py +4 -0
- package/lib/site-packages/trio/_tests/type_tests/path.py +140 -0
- package/lib/site-packages/trio/_tests/type_tests/subprocesses.py +23 -0
- package/lib/site-packages/trio/_tests/type_tests/task_status.py +29 -0
- package/lib/site-packages/trio/_threads.py +610 -0
- package/lib/site-packages/trio/_timeouts.py +197 -0
- package/lib/site-packages/trio/_tools/__init__.py +0 -0
- package/lib/site-packages/trio/_tools/gen_exports.py +401 -0
- package/lib/site-packages/trio/_tools/mypy_annotate.py +126 -0
- package/lib/site-packages/trio/_tools/sync_requirements.py +98 -0
- package/lib/site-packages/trio/_tools/windows_ffi_build.py +220 -0
- package/lib/site-packages/trio/_unix_pipes.py +197 -0
- package/lib/site-packages/trio/_util.py +385 -0
- package/lib/site-packages/trio/_version.py +3 -0
- package/lib/site-packages/trio/_wait_for_object.py +67 -0
- package/lib/site-packages/trio/_windows_pipes.py +144 -0
- package/lib/site-packages/trio/abc.py +23 -0
- package/lib/site-packages/trio/from_thread.py +13 -0
- package/lib/site-packages/trio/lowlevel.py +95 -0
- package/lib/site-packages/trio/py.typed +0 -0
- package/lib/site-packages/trio/socket.py +602 -0
- package/lib/site-packages/trio/testing/__init__.py +58 -0
- package/lib/site-packages/trio/testing/_check_streams.py +570 -0
- package/lib/site-packages/trio/testing/_checkpoints.py +69 -0
- package/lib/site-packages/trio/testing/_fake_net.py +584 -0
- package/lib/site-packages/trio/testing/_memory_streams.py +633 -0
- package/lib/site-packages/trio/testing/_network.py +36 -0
- package/lib/site-packages/trio/testing/_raises_group.py +1015 -0
- package/lib/site-packages/trio/testing/_sequencer.py +87 -0
- package/lib/site-packages/trio/testing/_trio_test.py +50 -0
- package/lib/site-packages/trio/to_thread.py +4 -0
- package/lib/site-packages/trio-0.33.0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/trio-0.33.0.dist-info/METADATA +186 -0
- package/lib/site-packages/trio-0.33.0.dist-info/RECORD +156 -0
- package/lib/site-packages/trio-0.33.0.dist-info/REQUESTED +0 -0
- package/lib/site-packages/trio-0.33.0.dist-info/WHEEL +5 -0
- package/lib/site-packages/trio-0.33.0.dist-info/entry_points.txt +2 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE +3 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.MIT +22 -0
- package/lib/site-packages/trio-0.33.0.dist-info/top_level.txt +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from re import Pattern
|
|
7
|
+
from textwrap import indent
|
|
8
|
+
from typing import TYPE_CHECKING, Generic, Literal, TypeGuard, cast, overload
|
|
9
|
+
|
|
10
|
+
from trio._util import final
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import builtins
|
|
14
|
+
|
|
15
|
+
# sphinx will *only* work if we use types.TracebackType, and import
|
|
16
|
+
# *inside* TYPE_CHECKING. No other combination works.....
|
|
17
|
+
import types
|
|
18
|
+
from collections.abc import Callable, Sequence
|
|
19
|
+
|
|
20
|
+
from _pytest._code.code import ExceptionChainRepr, ReprExceptionInfo, Traceback
|
|
21
|
+
from typing_extensions import TypeVar
|
|
22
|
+
|
|
23
|
+
# this conditional definition is because we want to allow a TypeVar default
|
|
24
|
+
MatchE = TypeVar(
|
|
25
|
+
"MatchE",
|
|
26
|
+
bound=BaseException,
|
|
27
|
+
default=BaseException,
|
|
28
|
+
covariant=True,
|
|
29
|
+
)
|
|
30
|
+
else:
|
|
31
|
+
from typing import TypeVar
|
|
32
|
+
|
|
33
|
+
MatchE = TypeVar("MatchE", bound=BaseException, covariant=True)
|
|
34
|
+
|
|
35
|
+
# RaisesGroup doesn't work with a default.
|
|
36
|
+
BaseExcT_co = TypeVar("BaseExcT_co", bound=BaseException, covariant=True)
|
|
37
|
+
BaseExcT_1 = TypeVar("BaseExcT_1", bound=BaseException)
|
|
38
|
+
BaseExcT_2 = TypeVar("BaseExcT_2", bound=BaseException)
|
|
39
|
+
ExcT_1 = TypeVar("ExcT_1", bound=Exception)
|
|
40
|
+
ExcT_2 = TypeVar("ExcT_2", bound=Exception)
|
|
41
|
+
|
|
42
|
+
if sys.version_info < (3, 11):
|
|
43
|
+
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@final
|
|
47
|
+
class _ExceptionInfo(Generic[MatchE]):
|
|
48
|
+
"""Minimal re-implementation of pytest.ExceptionInfo, only used if pytest is not available. Supports a subset of its features necessary for functionality of :class:`trio.testing.RaisesGroup` and :class:`trio.testing.Matcher`."""
|
|
49
|
+
|
|
50
|
+
_excinfo: tuple[type[MatchE], MatchE, types.TracebackType] | None
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
excinfo: tuple[type[MatchE], MatchE, types.TracebackType] | None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._excinfo = excinfo
|
|
57
|
+
|
|
58
|
+
def fill_unfilled(
|
|
59
|
+
self,
|
|
60
|
+
exc_info: tuple[type[MatchE], MatchE, types.TracebackType],
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Fill an unfilled ExceptionInfo created with ``for_later()``."""
|
|
63
|
+
assert self._excinfo is None, "ExceptionInfo was already filled"
|
|
64
|
+
self._excinfo = exc_info
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def for_later(cls) -> _ExceptionInfo[MatchE]:
|
|
68
|
+
"""Return an unfilled ExceptionInfo."""
|
|
69
|
+
return cls(None)
|
|
70
|
+
|
|
71
|
+
# Note, special cased in sphinx config, since "type" conflicts.
|
|
72
|
+
@property
|
|
73
|
+
def type(self) -> type[MatchE]:
|
|
74
|
+
"""The exception class."""
|
|
75
|
+
assert (
|
|
76
|
+
self._excinfo is not None
|
|
77
|
+
), ".type can only be used after the context manager exits"
|
|
78
|
+
return self._excinfo[0]
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def value(self) -> MatchE:
|
|
82
|
+
"""The exception value."""
|
|
83
|
+
assert (
|
|
84
|
+
self._excinfo is not None
|
|
85
|
+
), ".value can only be used after the context manager exits"
|
|
86
|
+
return self._excinfo[1]
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def tb(self) -> types.TracebackType:
|
|
90
|
+
"""The exception raw traceback."""
|
|
91
|
+
assert (
|
|
92
|
+
self._excinfo is not None
|
|
93
|
+
), ".tb can only be used after the context manager exits"
|
|
94
|
+
return self._excinfo[2]
|
|
95
|
+
|
|
96
|
+
def exconly(self, tryshort: bool = False) -> str:
|
|
97
|
+
raise NotImplementedError(
|
|
98
|
+
"This is a helper method only available if you use RaisesGroup with the pytest package installed",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def errisinstance(
|
|
102
|
+
self,
|
|
103
|
+
exc: builtins.type[BaseException] | tuple[builtins.type[BaseException], ...],
|
|
104
|
+
) -> bool:
|
|
105
|
+
raise NotImplementedError(
|
|
106
|
+
"This is a helper method only available if you use RaisesGroup with the pytest package installed",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def getrepr(
|
|
110
|
+
self,
|
|
111
|
+
showlocals: bool = False,
|
|
112
|
+
style: str = "long",
|
|
113
|
+
abspath: bool = False,
|
|
114
|
+
tbfilter: bool | Callable[[_ExceptionInfo], Traceback] = True,
|
|
115
|
+
funcargs: bool = False,
|
|
116
|
+
truncate_locals: bool = True,
|
|
117
|
+
chain: bool = True,
|
|
118
|
+
) -> ReprExceptionInfo | ExceptionChainRepr:
|
|
119
|
+
raise NotImplementedError(
|
|
120
|
+
"This is a helper method only available if you use RaisesGroup with the pytest package installed",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Type checkers are not able to do conditional types depending on installed packages, so
|
|
125
|
+
# we've added signatures for all helpers to _ExceptionInfo, and then always use that.
|
|
126
|
+
# If this ends up leading to problems, we can resort to always using _ExceptionInfo and
|
|
127
|
+
# users that want to use getrepr/errisinstance/exconly can write helpers on their own, or
|
|
128
|
+
# we reimplement them ourselves...or get this merged in upstream pytest.
|
|
129
|
+
if TYPE_CHECKING:
|
|
130
|
+
ExceptionInfo = _ExceptionInfo
|
|
131
|
+
|
|
132
|
+
else:
|
|
133
|
+
try:
|
|
134
|
+
from pytest import ExceptionInfo # noqa: PT013
|
|
135
|
+
except ImportError: # pragma: no cover
|
|
136
|
+
ExceptionInfo = _ExceptionInfo
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# copied from pytest.ExceptionInfo
|
|
140
|
+
def _stringify_exception(exc: BaseException) -> str:
|
|
141
|
+
return "\n".join(
|
|
142
|
+
[
|
|
143
|
+
getattr(exc, "message", str(exc)),
|
|
144
|
+
*getattr(exc, "__notes__", []),
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# String patterns default to including the unicode flag.
|
|
150
|
+
_REGEX_NO_FLAGS = re.compile(r"").flags
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _match_pattern(match: Pattern[str]) -> str | Pattern[str]:
|
|
154
|
+
"""helper function to remove redundant `re.compile` calls when printing regex"""
|
|
155
|
+
return match.pattern if match.flags == _REGEX_NO_FLAGS else match
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def repr_callable(fun: Callable[[BaseExcT_1], bool]) -> str:
|
|
159
|
+
"""Get the repr of a ``check`` parameter.
|
|
160
|
+
|
|
161
|
+
Split out so it can be monkeypatched (e.g. by our hypothesis plugin)
|
|
162
|
+
"""
|
|
163
|
+
return repr(fun)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _exception_type_name(e: type[BaseException]) -> str:
|
|
167
|
+
return repr(e.__name__)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _check_raw_type(
|
|
171
|
+
expected_type: type[BaseException] | None,
|
|
172
|
+
exception: BaseException,
|
|
173
|
+
) -> str | None:
|
|
174
|
+
if expected_type is None:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
if not isinstance(
|
|
178
|
+
exception,
|
|
179
|
+
expected_type,
|
|
180
|
+
):
|
|
181
|
+
actual_type_str = _exception_type_name(type(exception))
|
|
182
|
+
expected_type_str = _exception_type_name(expected_type)
|
|
183
|
+
if isinstance(exception, BaseExceptionGroup) and not issubclass(
|
|
184
|
+
expected_type, BaseExceptionGroup
|
|
185
|
+
):
|
|
186
|
+
return f"Unexpected nested {actual_type_str}, expected {expected_type_str}"
|
|
187
|
+
return f"{actual_type_str} is not of type {expected_type_str}"
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class AbstractMatcher(ABC, Generic[BaseExcT_co]):
|
|
192
|
+
"""ABC with common functionality shared between Matcher and RaisesGroup"""
|
|
193
|
+
|
|
194
|
+
def __init__(
|
|
195
|
+
self,
|
|
196
|
+
match: str | Pattern[str] | None,
|
|
197
|
+
check: Callable[[BaseExcT_co], bool] | None,
|
|
198
|
+
) -> None:
|
|
199
|
+
if isinstance(match, str):
|
|
200
|
+
self.match: Pattern[str] | None = re.compile(match)
|
|
201
|
+
else:
|
|
202
|
+
self.match = match
|
|
203
|
+
self.check = check
|
|
204
|
+
self._fail_reason: str | None = None
|
|
205
|
+
|
|
206
|
+
# used to suppress repeated printing of `repr(self.check)`
|
|
207
|
+
self._nested: bool = False
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def fail_reason(self) -> str | None:
|
|
211
|
+
"""Set after a call to `matches` to give a human-readable
|
|
212
|
+
reason for why the match failed.
|
|
213
|
+
When used as a context manager the string will be given as the text of an
|
|
214
|
+
`AssertionError`"""
|
|
215
|
+
return self._fail_reason
|
|
216
|
+
|
|
217
|
+
def _check_check(
|
|
218
|
+
self: AbstractMatcher[BaseExcT_1],
|
|
219
|
+
exception: BaseExcT_1,
|
|
220
|
+
) -> bool:
|
|
221
|
+
if self.check is None:
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
if self.check(exception):
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
check_repr = "" if self._nested else " " + repr_callable(self.check)
|
|
228
|
+
self._fail_reason = f"check{check_repr} did not return True"
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
def _check_match(self, e: BaseException) -> bool:
|
|
232
|
+
if self.match is None or re.search(
|
|
233
|
+
self.match,
|
|
234
|
+
stringified_exception := _stringify_exception(e),
|
|
235
|
+
):
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
maybe_specify_type = (
|
|
239
|
+
f" of {_exception_type_name(type(e))}"
|
|
240
|
+
if isinstance(e, BaseExceptionGroup)
|
|
241
|
+
else ""
|
|
242
|
+
)
|
|
243
|
+
self._fail_reason = f"Regex pattern {_match_pattern(self.match)!r} did not match {stringified_exception!r}{maybe_specify_type}"
|
|
244
|
+
if _match_pattern(self.match) == stringified_exception:
|
|
245
|
+
self._fail_reason += "\n Did you mean to `re.escape()` the regex?"
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
# TODO: when transitioning to pytest, harmonize Matcher and RaisesGroup
|
|
249
|
+
# signatures. One names the parameter `exc_val` and the other `exception`
|
|
250
|
+
@abstractmethod
|
|
251
|
+
def matches(
|
|
252
|
+
self: AbstractMatcher[BaseExcT_1], exc_val: BaseException
|
|
253
|
+
) -> TypeGuard[BaseExcT_1]:
|
|
254
|
+
"""Check if an exception matches the requirements of this AbstractMatcher.
|
|
255
|
+
If it fails, `AbstractMatcher.fail_reason` should be set.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@final
|
|
260
|
+
class Matcher(AbstractMatcher[MatchE]):
|
|
261
|
+
"""Helper class to be used together with RaisesGroups when you want to specify requirements on sub-exceptions. Only specifying the type is redundant, and it's also unnecessary when the type is a nested `RaisesGroup` since it supports the same arguments.
|
|
262
|
+
The type is checked with `isinstance`, and does not need to be an exact match. If that is wanted you can use the ``check`` parameter.
|
|
263
|
+
:meth:`Matcher.matches` can also be used standalone to check individual exceptions.
|
|
264
|
+
|
|
265
|
+
Examples::
|
|
266
|
+
|
|
267
|
+
with RaisesGroups(Matcher(ValueError, match="string")):
|
|
268
|
+
...
|
|
269
|
+
with RaisesGroups(Matcher(check=lambda x: x.args == (3, "hello"))):
|
|
270
|
+
...
|
|
271
|
+
with RaisesGroups(Matcher(check=lambda x: type(x) is ValueError)):
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
Tip: if you install ``hypothesis`` and import it in ``conftest.py`` you will get
|
|
275
|
+
readable ``repr``s of ``check`` callables in the output.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
# At least one of the three parameters must be passed.
|
|
279
|
+
@overload
|
|
280
|
+
def __init__(
|
|
281
|
+
self,
|
|
282
|
+
exception_type: type[MatchE],
|
|
283
|
+
match: str | Pattern[str] = ...,
|
|
284
|
+
check: Callable[[MatchE], bool] = ...,
|
|
285
|
+
) -> None: ...
|
|
286
|
+
|
|
287
|
+
@overload
|
|
288
|
+
def __init__(
|
|
289
|
+
self: Matcher[BaseException], # Give E a value.
|
|
290
|
+
*,
|
|
291
|
+
match: str | Pattern[str],
|
|
292
|
+
# If exception_type is not provided, check() must do any typechecks itself.
|
|
293
|
+
check: Callable[[BaseException], bool] = ...,
|
|
294
|
+
) -> None: ...
|
|
295
|
+
|
|
296
|
+
@overload
|
|
297
|
+
def __init__(self, *, check: Callable[[BaseException], bool]) -> None: ...
|
|
298
|
+
|
|
299
|
+
def __init__(
|
|
300
|
+
self,
|
|
301
|
+
exception_type: type[MatchE] | None = None,
|
|
302
|
+
match: str | Pattern[str] | None = None,
|
|
303
|
+
check: Callable[[MatchE], bool] | None = None,
|
|
304
|
+
):
|
|
305
|
+
super().__init__(match, check)
|
|
306
|
+
if exception_type is None and match is None and check is None:
|
|
307
|
+
raise ValueError("You must specify at least one parameter to match on.")
|
|
308
|
+
if exception_type is not None and not issubclass(exception_type, BaseException):
|
|
309
|
+
raise ValueError(
|
|
310
|
+
f"exception_type {exception_type} must be a subclass of BaseException",
|
|
311
|
+
)
|
|
312
|
+
self.exception_type = exception_type
|
|
313
|
+
|
|
314
|
+
def matches(
|
|
315
|
+
self,
|
|
316
|
+
exception: BaseException,
|
|
317
|
+
) -> TypeGuard[MatchE]:
|
|
318
|
+
"""Check if an exception matches the requirements of this Matcher.
|
|
319
|
+
If it fails, `Matcher.fail_reason` will be set.
|
|
320
|
+
|
|
321
|
+
Examples::
|
|
322
|
+
|
|
323
|
+
assert Matcher(ValueError).matches(my_exception)
|
|
324
|
+
# is equivalent to
|
|
325
|
+
assert isinstance(my_exception, ValueError)
|
|
326
|
+
|
|
327
|
+
# this can be useful when checking e.g. the ``__cause__`` of an exception.
|
|
328
|
+
with pytest.raises(ValueError) as excinfo:
|
|
329
|
+
...
|
|
330
|
+
assert Matcher(SyntaxError, match="foo").matches(excinfo.value.__cause__)
|
|
331
|
+
# above line is equivalent to
|
|
332
|
+
assert isinstance(excinfo.value.__cause__, SyntaxError)
|
|
333
|
+
assert re.search("foo", str(excinfo.value.__cause__))
|
|
334
|
+
|
|
335
|
+
"""
|
|
336
|
+
if not self._check_type(exception):
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
if not self._check_match(exception):
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
return self._check_check(exception)
|
|
343
|
+
|
|
344
|
+
def __repr__(self) -> str:
|
|
345
|
+
parameters = []
|
|
346
|
+
if self.exception_type is not None:
|
|
347
|
+
parameters.append(self.exception_type.__name__)
|
|
348
|
+
if self.match is not None:
|
|
349
|
+
# If no flags were specified, discard the redundant re.compile() here.
|
|
350
|
+
parameters.append(
|
|
351
|
+
f"match={_match_pattern(self.match)!r}",
|
|
352
|
+
)
|
|
353
|
+
if self.check is not None:
|
|
354
|
+
parameters.append(f"check={repr_callable(self.check)}")
|
|
355
|
+
return f'Matcher({", ".join(parameters)})'
|
|
356
|
+
|
|
357
|
+
def _check_type(self, exception: BaseException) -> TypeGuard[MatchE]:
|
|
358
|
+
self._fail_reason = _check_raw_type(self.exception_type, exception)
|
|
359
|
+
return self._fail_reason is None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@final
|
|
363
|
+
class RaisesGroup(AbstractMatcher[BaseExceptionGroup[BaseExcT_co]]):
|
|
364
|
+
"""Contextmanager for checking for an expected `ExceptionGroup`.
|
|
365
|
+
This works similar to ``pytest.raises``, and a version of it will hopefully be added upstream, after which this can be deprecated and removed. See https://github.com/pytest-dev/pytest/issues/11538
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
The catching behaviour differs from :ref:`except* <except_star>` in multiple different ways, being much stricter by default. By using ``allow_unwrapped=True`` and ``flatten_subgroups=True`` you can match ``except*`` fully when expecting a single exception.
|
|
369
|
+
|
|
370
|
+
#. All specified exceptions must be present, *and no others*.
|
|
371
|
+
|
|
372
|
+
* If you expect a variable number of exceptions you need to use ``pytest.raises(ExceptionGroup)`` and manually check the contained exceptions. Consider making use of :func:`Matcher.matches`.
|
|
373
|
+
|
|
374
|
+
#. It will only catch exceptions wrapped in an exceptiongroup by default.
|
|
375
|
+
|
|
376
|
+
* With ``allow_unwrapped=True`` you can specify a single expected exception or `Matcher` and it will match the exception even if it is not inside an `ExceptionGroup`. If you expect one of several different exception types you need to use a `Matcher` object.
|
|
377
|
+
|
|
378
|
+
#. By default it cares about the full structure with nested `ExceptionGroup`'s. You can specify nested `ExceptionGroup`'s by passing `RaisesGroup` objects as expected exceptions.
|
|
379
|
+
|
|
380
|
+
* With ``flatten_subgroups=True`` it will "flatten" the raised `ExceptionGroup`, extracting all exceptions inside any nested :class:`ExceptionGroup`, before matching.
|
|
381
|
+
|
|
382
|
+
It does not care about the order of the exceptions, so ``RaisesGroups(ValueError, TypeError)`` is equivalent to ``RaisesGroups(TypeError, ValueError)``.
|
|
383
|
+
|
|
384
|
+
Examples::
|
|
385
|
+
|
|
386
|
+
with RaisesGroups(ValueError):
|
|
387
|
+
raise ExceptionGroup("", (ValueError(),))
|
|
388
|
+
with RaisesGroups(ValueError, ValueError, Matcher(TypeError, match="expected int")):
|
|
389
|
+
...
|
|
390
|
+
with RaisesGroups(KeyboardInterrupt, match="hello", check=lambda x: type(x) is BaseExceptionGroup):
|
|
391
|
+
...
|
|
392
|
+
with RaisesGroups(RaisesGroups(ValueError)):
|
|
393
|
+
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
|
|
394
|
+
|
|
395
|
+
# flatten_subgroups
|
|
396
|
+
with RaisesGroups(ValueError, flatten_subgroups=True):
|
|
397
|
+
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
|
|
398
|
+
|
|
399
|
+
# allow_unwrapped
|
|
400
|
+
with RaisesGroups(ValueError, allow_unwrapped=True):
|
|
401
|
+
raise ValueError
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
`RaisesGroup.matches` can also be used directly to check a standalone exception group.
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
The matching algorithm is greedy, which means cases such as this may fail::
|
|
408
|
+
|
|
409
|
+
with RaisesGroups(ValueError, Matcher(ValueError, match="hello")):
|
|
410
|
+
raise ExceptionGroup("", (ValueError("hello"), ValueError("goodbye")))
|
|
411
|
+
|
|
412
|
+
even though it generally does not care about the order of the exceptions in the group.
|
|
413
|
+
To avoid the above you should specify the first ValueError with a Matcher as well.
|
|
414
|
+
|
|
415
|
+
Tip: if you install ``hypothesis`` and import it in ``conftest.py`` you will get
|
|
416
|
+
readable ``repr``s of ``check`` callables in the output.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
# allow_unwrapped=True requires: singular exception, exception not being
|
|
420
|
+
# RaisesGroup instance, match is None, check is None
|
|
421
|
+
@overload
|
|
422
|
+
def __init__(
|
|
423
|
+
self,
|
|
424
|
+
exception: type[BaseExcT_co] | Matcher[BaseExcT_co],
|
|
425
|
+
*,
|
|
426
|
+
allow_unwrapped: Literal[True],
|
|
427
|
+
flatten_subgroups: bool = False,
|
|
428
|
+
) -> None: ...
|
|
429
|
+
|
|
430
|
+
# flatten_subgroups = True also requires no nested RaisesGroup
|
|
431
|
+
@overload
|
|
432
|
+
def __init__(
|
|
433
|
+
self,
|
|
434
|
+
exception: type[BaseExcT_co] | Matcher[BaseExcT_co],
|
|
435
|
+
*other_exceptions: type[BaseExcT_co] | Matcher[BaseExcT_co],
|
|
436
|
+
flatten_subgroups: Literal[True],
|
|
437
|
+
match: str | Pattern[str] | None = None,
|
|
438
|
+
check: Callable[[BaseExceptionGroup[BaseExcT_co]], bool] | None = None,
|
|
439
|
+
) -> None: ...
|
|
440
|
+
|
|
441
|
+
# simplify the typevars if possible (the following 3 are equivalent but go simpler->complicated)
|
|
442
|
+
# ... the first handles RaisesGroup[ValueError], the second RaisesGroup[ExceptionGroup[ValueError]],
|
|
443
|
+
# the third RaisesGroup[ValueError | ExceptionGroup[ValueError]].
|
|
444
|
+
# ... otherwise, we will get results like RaisesGroup[ValueError | ExceptionGroup[Never]] (I think)
|
|
445
|
+
# (technically correct but misleading)
|
|
446
|
+
@overload
|
|
447
|
+
def __init__(
|
|
448
|
+
self: RaisesGroup[ExcT_1],
|
|
449
|
+
exception: type[ExcT_1] | Matcher[ExcT_1],
|
|
450
|
+
*other_exceptions: type[ExcT_1] | Matcher[ExcT_1],
|
|
451
|
+
match: str | Pattern[str] | None = None,
|
|
452
|
+
check: Callable[[ExceptionGroup[ExcT_1]], bool] | None = None,
|
|
453
|
+
) -> None: ...
|
|
454
|
+
|
|
455
|
+
@overload
|
|
456
|
+
def __init__(
|
|
457
|
+
self: RaisesGroup[ExceptionGroup[ExcT_2]],
|
|
458
|
+
exception: RaisesGroup[ExcT_2],
|
|
459
|
+
*other_exceptions: RaisesGroup[ExcT_2],
|
|
460
|
+
match: str | Pattern[str] | None = None,
|
|
461
|
+
check: Callable[[ExceptionGroup[ExceptionGroup[ExcT_2]]], bool] | None = None,
|
|
462
|
+
) -> None: ...
|
|
463
|
+
|
|
464
|
+
@overload
|
|
465
|
+
def __init__(
|
|
466
|
+
self: RaisesGroup[ExcT_1 | ExceptionGroup[ExcT_2]],
|
|
467
|
+
exception: type[ExcT_1] | Matcher[ExcT_1] | RaisesGroup[ExcT_2],
|
|
468
|
+
*other_exceptions: type[ExcT_1] | Matcher[ExcT_1] | RaisesGroup[ExcT_2],
|
|
469
|
+
match: str | Pattern[str] | None = None,
|
|
470
|
+
check: (
|
|
471
|
+
Callable[[ExceptionGroup[ExcT_1 | ExceptionGroup[ExcT_2]]], bool] | None
|
|
472
|
+
) = None,
|
|
473
|
+
) -> None: ...
|
|
474
|
+
|
|
475
|
+
# same as the above 3 but handling BaseException
|
|
476
|
+
@overload
|
|
477
|
+
def __init__(
|
|
478
|
+
self: RaisesGroup[BaseExcT_1],
|
|
479
|
+
exception: type[BaseExcT_1] | Matcher[BaseExcT_1],
|
|
480
|
+
*other_exceptions: type[BaseExcT_1] | Matcher[BaseExcT_1],
|
|
481
|
+
match: str | Pattern[str] | None = None,
|
|
482
|
+
check: Callable[[BaseExceptionGroup[BaseExcT_1]], bool] | None = None,
|
|
483
|
+
) -> None: ...
|
|
484
|
+
|
|
485
|
+
@overload
|
|
486
|
+
def __init__(
|
|
487
|
+
self: RaisesGroup[BaseExceptionGroup[BaseExcT_2]],
|
|
488
|
+
exception: RaisesGroup[BaseExcT_2],
|
|
489
|
+
*other_exceptions: RaisesGroup[BaseExcT_2],
|
|
490
|
+
match: str | Pattern[str] | None = None,
|
|
491
|
+
check: (
|
|
492
|
+
Callable[[BaseExceptionGroup[BaseExceptionGroup[BaseExcT_2]]], bool] | None
|
|
493
|
+
) = None,
|
|
494
|
+
) -> None: ...
|
|
495
|
+
|
|
496
|
+
@overload
|
|
497
|
+
def __init__(
|
|
498
|
+
self: RaisesGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]],
|
|
499
|
+
exception: type[BaseExcT_1] | Matcher[BaseExcT_1] | RaisesGroup[BaseExcT_2],
|
|
500
|
+
*other_exceptions: type[BaseExcT_1]
|
|
501
|
+
| Matcher[BaseExcT_1]
|
|
502
|
+
| RaisesGroup[BaseExcT_2],
|
|
503
|
+
match: str | Pattern[str] | None = None,
|
|
504
|
+
check: (
|
|
505
|
+
Callable[
|
|
506
|
+
[BaseExceptionGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]]],
|
|
507
|
+
bool,
|
|
508
|
+
]
|
|
509
|
+
| None
|
|
510
|
+
) = None,
|
|
511
|
+
) -> None: ...
|
|
512
|
+
|
|
513
|
+
def __init__(
|
|
514
|
+
self: RaisesGroup[ExcT_1 | BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]],
|
|
515
|
+
exception: type[BaseExcT_1] | Matcher[BaseExcT_1] | RaisesGroup[BaseExcT_2],
|
|
516
|
+
*other_exceptions: type[BaseExcT_1]
|
|
517
|
+
| Matcher[BaseExcT_1]
|
|
518
|
+
| RaisesGroup[BaseExcT_2],
|
|
519
|
+
allow_unwrapped: bool = False,
|
|
520
|
+
flatten_subgroups: bool = False,
|
|
521
|
+
match: str | Pattern[str] | None = None,
|
|
522
|
+
check: (
|
|
523
|
+
Callable[[BaseExceptionGroup[BaseExcT_1]], bool]
|
|
524
|
+
| Callable[[ExceptionGroup[ExcT_1]], bool]
|
|
525
|
+
| None
|
|
526
|
+
) = None,
|
|
527
|
+
):
|
|
528
|
+
# The type hint on the `self` and `check` parameters uses different formats
|
|
529
|
+
# that are *very* hard to reconcile while adhering to the overloads, so we cast
|
|
530
|
+
# it to avoid an error when passing it to super().__init__
|
|
531
|
+
check = cast(
|
|
532
|
+
"Callable[["
|
|
533
|
+
"BaseExceptionGroup[ExcT_1|BaseExcT_1|BaseExceptionGroup[BaseExcT_2]]"
|
|
534
|
+
"], bool]",
|
|
535
|
+
check,
|
|
536
|
+
)
|
|
537
|
+
super().__init__(match, check)
|
|
538
|
+
self.expected_exceptions: tuple[
|
|
539
|
+
type[BaseExcT_co] | Matcher[BaseExcT_co] | RaisesGroup[BaseException], ...
|
|
540
|
+
] = (
|
|
541
|
+
exception,
|
|
542
|
+
*other_exceptions,
|
|
543
|
+
)
|
|
544
|
+
self.allow_unwrapped = allow_unwrapped
|
|
545
|
+
self.flatten_subgroups: bool = flatten_subgroups
|
|
546
|
+
self.is_baseexceptiongroup: bool = False
|
|
547
|
+
|
|
548
|
+
if allow_unwrapped and other_exceptions:
|
|
549
|
+
raise ValueError(
|
|
550
|
+
"You cannot specify multiple exceptions with `allow_unwrapped=True.`"
|
|
551
|
+
" If you want to match one of multiple possible exceptions you should"
|
|
552
|
+
" use a `Matcher`."
|
|
553
|
+
" E.g. `Matcher(check=lambda e: isinstance(e, (...)))`",
|
|
554
|
+
)
|
|
555
|
+
if allow_unwrapped and isinstance(exception, RaisesGroup):
|
|
556
|
+
raise ValueError(
|
|
557
|
+
"`allow_unwrapped=True` has no effect when expecting a `RaisesGroup`."
|
|
558
|
+
" You might want it in the expected `RaisesGroup`, or"
|
|
559
|
+
" `flatten_subgroups=True` if you don't care about the structure.",
|
|
560
|
+
)
|
|
561
|
+
if allow_unwrapped and (match is not None or check is not None):
|
|
562
|
+
raise ValueError(
|
|
563
|
+
"`allow_unwrapped=True` bypasses the `match` and `check` parameters"
|
|
564
|
+
" if the exception is unwrapped. If you intended to match/check the"
|
|
565
|
+
" exception you should use a `Matcher` object. If you want to match/check"
|
|
566
|
+
" the exceptiongroup when the exception *is* wrapped you need to"
|
|
567
|
+
" do e.g. `if isinstance(exc.value, ExceptionGroup):"
|
|
568
|
+
" assert RaisesGroup(...).matches(exc.value)` afterwards.",
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# verify `expected_exceptions` and set `self.is_baseexceptiongroup`
|
|
572
|
+
for exc in self.expected_exceptions:
|
|
573
|
+
if isinstance(exc, RaisesGroup):
|
|
574
|
+
if self.flatten_subgroups:
|
|
575
|
+
raise ValueError(
|
|
576
|
+
"You cannot specify a nested structure inside a RaisesGroup with"
|
|
577
|
+
" `flatten_subgroups=True`. The parameter will flatten subgroups"
|
|
578
|
+
" in the raised exceptiongroup before matching, which would never"
|
|
579
|
+
" match a nested structure.",
|
|
580
|
+
)
|
|
581
|
+
self.is_baseexceptiongroup |= exc.is_baseexceptiongroup
|
|
582
|
+
exc._nested = True
|
|
583
|
+
elif isinstance(exc, Matcher):
|
|
584
|
+
if exc.exception_type is not None:
|
|
585
|
+
# Matcher __init__ assures it's a subclass of BaseException
|
|
586
|
+
self.is_baseexceptiongroup |= not issubclass(
|
|
587
|
+
exc.exception_type,
|
|
588
|
+
Exception,
|
|
589
|
+
)
|
|
590
|
+
exc._nested = True
|
|
591
|
+
elif isinstance(exc, type) and issubclass(exc, BaseException):
|
|
592
|
+
self.is_baseexceptiongroup |= not issubclass(exc, Exception)
|
|
593
|
+
else:
|
|
594
|
+
raise ValueError(
|
|
595
|
+
f'Invalid argument "{exc!r}" must be exception type, Matcher, or'
|
|
596
|
+
" RaisesGroup.",
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
@overload
|
|
600
|
+
def __enter__(
|
|
601
|
+
self: RaisesGroup[ExcT_1],
|
|
602
|
+
) -> ExceptionInfo[ExceptionGroup[ExcT_1]]: ...
|
|
603
|
+
@overload
|
|
604
|
+
def __enter__(
|
|
605
|
+
self: RaisesGroup[BaseExcT_1],
|
|
606
|
+
) -> ExceptionInfo[BaseExceptionGroup[BaseExcT_1]]: ...
|
|
607
|
+
|
|
608
|
+
def __enter__(self) -> ExceptionInfo[BaseExceptionGroup[BaseException]]:
|
|
609
|
+
self.excinfo: ExceptionInfo[BaseExceptionGroup[BaseExcT_co]] = (
|
|
610
|
+
ExceptionInfo.for_later()
|
|
611
|
+
)
|
|
612
|
+
return self.excinfo
|
|
613
|
+
|
|
614
|
+
def __repr__(self) -> str:
|
|
615
|
+
parameters = [
|
|
616
|
+
e.__name__ if isinstance(e, type) else repr(e)
|
|
617
|
+
for e in self.expected_exceptions
|
|
618
|
+
]
|
|
619
|
+
if self.allow_unwrapped:
|
|
620
|
+
parameters.append(f"allow_unwrapped={self.allow_unwrapped}")
|
|
621
|
+
if self.flatten_subgroups:
|
|
622
|
+
parameters.append(f"flatten_subgroups={self.flatten_subgroups}")
|
|
623
|
+
if self.match is not None:
|
|
624
|
+
# If no flags were specified, discard the redundant re.compile() here.
|
|
625
|
+
parameters.append(f"match={_match_pattern(self.match)!r}")
|
|
626
|
+
if self.check is not None:
|
|
627
|
+
parameters.append(f"check={repr_callable(self.check)}")
|
|
628
|
+
return f"RaisesGroup({', '.join(parameters)})"
|
|
629
|
+
|
|
630
|
+
def _unroll_exceptions(
|
|
631
|
+
self,
|
|
632
|
+
exceptions: Sequence[BaseException],
|
|
633
|
+
) -> Sequence[BaseException]:
|
|
634
|
+
"""Used if `flatten_subgroups=True`."""
|
|
635
|
+
res: list[BaseException] = []
|
|
636
|
+
for exc in exceptions:
|
|
637
|
+
if isinstance(exc, BaseExceptionGroup):
|
|
638
|
+
res.extend(self._unroll_exceptions(exc.exceptions))
|
|
639
|
+
|
|
640
|
+
else:
|
|
641
|
+
res.append(exc)
|
|
642
|
+
return res
|
|
643
|
+
|
|
644
|
+
@overload
|
|
645
|
+
def matches(
|
|
646
|
+
self: RaisesGroup[ExcT_1],
|
|
647
|
+
exc_val: BaseException | None,
|
|
648
|
+
) -> TypeGuard[ExceptionGroup[ExcT_1]]: ...
|
|
649
|
+
@overload
|
|
650
|
+
def matches(
|
|
651
|
+
self: RaisesGroup[BaseExcT_1],
|
|
652
|
+
exc_val: BaseException | None,
|
|
653
|
+
) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ...
|
|
654
|
+
|
|
655
|
+
def matches(
|
|
656
|
+
self,
|
|
657
|
+
exc_val: BaseException | None,
|
|
658
|
+
) -> TypeGuard[BaseExceptionGroup[BaseExcT_co]]:
|
|
659
|
+
"""Check if an exception matches the requirements of this RaisesGroup.
|
|
660
|
+
If it fails, `RaisesGroup.fail_reason` will be set.
|
|
661
|
+
|
|
662
|
+
Example::
|
|
663
|
+
|
|
664
|
+
with pytest.raises(TypeError) as excinfo:
|
|
665
|
+
...
|
|
666
|
+
assert RaisesGroups(ValueError).matches(excinfo.value.__cause__)
|
|
667
|
+
# the above line is equivalent to
|
|
668
|
+
myexc = excinfo.value.__cause
|
|
669
|
+
assert isinstance(myexc, BaseExceptionGroup)
|
|
670
|
+
assert len(myexc.exceptions) == 1
|
|
671
|
+
assert isinstance(myexc.exceptions[0], ValueError)
|
|
672
|
+
"""
|
|
673
|
+
self._fail_reason = None
|
|
674
|
+
if exc_val is None:
|
|
675
|
+
self._fail_reason = "exception is None"
|
|
676
|
+
return False
|
|
677
|
+
if not isinstance(exc_val, BaseExceptionGroup):
|
|
678
|
+
# we opt to only print type of the exception here, as the repr would
|
|
679
|
+
# likely be quite long
|
|
680
|
+
not_group_msg = f"{type(exc_val).__name__!r} is not an exception group"
|
|
681
|
+
if len(self.expected_exceptions) > 1:
|
|
682
|
+
self._fail_reason = not_group_msg
|
|
683
|
+
return False
|
|
684
|
+
# if we have 1 expected exception, check if it would work even if
|
|
685
|
+
# allow_unwrapped is not set
|
|
686
|
+
res = self._check_expected(self.expected_exceptions[0], exc_val)
|
|
687
|
+
if res is None and self.allow_unwrapped:
|
|
688
|
+
return True
|
|
689
|
+
|
|
690
|
+
if res is None:
|
|
691
|
+
self._fail_reason = (
|
|
692
|
+
f"{not_group_msg}, but would match with `allow_unwrapped=True`"
|
|
693
|
+
)
|
|
694
|
+
elif self.allow_unwrapped:
|
|
695
|
+
self._fail_reason = res
|
|
696
|
+
else:
|
|
697
|
+
self._fail_reason = not_group_msg
|
|
698
|
+
return False
|
|
699
|
+
|
|
700
|
+
actual_exceptions: Sequence[BaseException] = exc_val.exceptions
|
|
701
|
+
if self.flatten_subgroups:
|
|
702
|
+
actual_exceptions = self._unroll_exceptions(actual_exceptions)
|
|
703
|
+
|
|
704
|
+
if not self._check_match(exc_val):
|
|
705
|
+
old_reason = self._fail_reason
|
|
706
|
+
if (
|
|
707
|
+
len(actual_exceptions) == len(self.expected_exceptions) == 1
|
|
708
|
+
and isinstance(expected := self.expected_exceptions[0], type)
|
|
709
|
+
and isinstance(actual := actual_exceptions[0], expected)
|
|
710
|
+
and self._check_match(actual)
|
|
711
|
+
):
|
|
712
|
+
assert self.match is not None, "can't be None if _check_match failed"
|
|
713
|
+
assert self._fail_reason is old_reason is not None
|
|
714
|
+
self._fail_reason += f", but matched the expected {self._repr_expected(expected)}. You might want RaisesGroup(Matcher({expected.__name__}, match={_match_pattern(self.match)!r}))"
|
|
715
|
+
else:
|
|
716
|
+
self._fail_reason = old_reason
|
|
717
|
+
return False
|
|
718
|
+
|
|
719
|
+
# do the full check on expected exceptions
|
|
720
|
+
if not self._check_exceptions(
|
|
721
|
+
exc_val,
|
|
722
|
+
actual_exceptions,
|
|
723
|
+
):
|
|
724
|
+
assert self._fail_reason is not None
|
|
725
|
+
old_reason = self._fail_reason
|
|
726
|
+
# if we're not expecting a nested structure, and there is one, do a second
|
|
727
|
+
# pass where we try flattening it
|
|
728
|
+
if (
|
|
729
|
+
not self.flatten_subgroups
|
|
730
|
+
and not any(
|
|
731
|
+
isinstance(e, RaisesGroup) for e in self.expected_exceptions
|
|
732
|
+
)
|
|
733
|
+
and any(isinstance(e, BaseExceptionGroup) for e in actual_exceptions)
|
|
734
|
+
and self._check_exceptions(
|
|
735
|
+
exc_val,
|
|
736
|
+
self._unroll_exceptions(exc_val.exceptions),
|
|
737
|
+
)
|
|
738
|
+
):
|
|
739
|
+
# only indent if it's a single-line reason. In a multi-line there's already
|
|
740
|
+
# indented lines that this does not belong to.
|
|
741
|
+
indent = " " if "\n" not in self._fail_reason else ""
|
|
742
|
+
self._fail_reason = (
|
|
743
|
+
old_reason
|
|
744
|
+
+ f"\n{indent}Did you mean to use `flatten_subgroups=True`?"
|
|
745
|
+
)
|
|
746
|
+
else:
|
|
747
|
+
self._fail_reason = old_reason
|
|
748
|
+
return False
|
|
749
|
+
|
|
750
|
+
# Only run `self.check` once we know `exc_val` is of the correct type.
|
|
751
|
+
# TODO: if this fails, we should say the *group* did not match
|
|
752
|
+
return self._check_check(exc_val)
|
|
753
|
+
|
|
754
|
+
@staticmethod
|
|
755
|
+
def _check_expected(
|
|
756
|
+
expected_type: (
|
|
757
|
+
type[BaseException] | Matcher[BaseException] | RaisesGroup[BaseException]
|
|
758
|
+
),
|
|
759
|
+
exception: BaseException,
|
|
760
|
+
) -> str | None:
|
|
761
|
+
"""Helper method for `RaisesGroup.matches` and `RaisesGroup._check_exceptions`
|
|
762
|
+
to check one of potentially several expected exceptions."""
|
|
763
|
+
if isinstance(expected_type, type):
|
|
764
|
+
return _check_raw_type(expected_type, exception)
|
|
765
|
+
res = expected_type.matches(exception)
|
|
766
|
+
if res:
|
|
767
|
+
return None
|
|
768
|
+
assert expected_type.fail_reason is not None
|
|
769
|
+
if expected_type.fail_reason.startswith("\n"):
|
|
770
|
+
return f"\n{expected_type!r}: {indent(expected_type.fail_reason, ' ')}"
|
|
771
|
+
return f"{expected_type!r}: {expected_type.fail_reason}"
|
|
772
|
+
|
|
773
|
+
@staticmethod
|
|
774
|
+
def _repr_expected(e: type[BaseException] | AbstractMatcher[BaseException]) -> str:
|
|
775
|
+
"""Get the repr of an expected type/Matcher/RaisesGroup, but we only want
|
|
776
|
+
the name if it's a type"""
|
|
777
|
+
if isinstance(e, type):
|
|
778
|
+
return _exception_type_name(e)
|
|
779
|
+
return repr(e)
|
|
780
|
+
|
|
781
|
+
@overload
|
|
782
|
+
def _check_exceptions(
|
|
783
|
+
self: RaisesGroup[ExcT_1],
|
|
784
|
+
_exc_val: Exception,
|
|
785
|
+
actual_exceptions: Sequence[Exception],
|
|
786
|
+
) -> TypeGuard[ExceptionGroup[ExcT_1]]: ...
|
|
787
|
+
@overload
|
|
788
|
+
def _check_exceptions(
|
|
789
|
+
self: RaisesGroup[BaseExcT_1],
|
|
790
|
+
_exc_val: BaseException,
|
|
791
|
+
actual_exceptions: Sequence[BaseException],
|
|
792
|
+
) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ...
|
|
793
|
+
|
|
794
|
+
def _check_exceptions(
|
|
795
|
+
self,
|
|
796
|
+
_exc_val: BaseException,
|
|
797
|
+
actual_exceptions: Sequence[BaseException],
|
|
798
|
+
) -> TypeGuard[BaseExceptionGroup[BaseExcT_co]]:
|
|
799
|
+
"""helper method for RaisesGroup.matches that attempts to pair up expected and actual exceptions"""
|
|
800
|
+
# full table with all results
|
|
801
|
+
results = ResultHolder(self.expected_exceptions, actual_exceptions)
|
|
802
|
+
|
|
803
|
+
# (indexes of) raised exceptions that haven't (yet) found an expected
|
|
804
|
+
remaining_actual = list(range(len(actual_exceptions)))
|
|
805
|
+
# (indexes of) expected exceptions that haven't found a matching raised
|
|
806
|
+
failed_expected: list[int] = []
|
|
807
|
+
# successful greedy matches
|
|
808
|
+
matches: dict[int, int] = {}
|
|
809
|
+
|
|
810
|
+
# loop over expected exceptions first to get a more predictable result
|
|
811
|
+
for i_exp, expected in enumerate(self.expected_exceptions):
|
|
812
|
+
for i_rem in remaining_actual:
|
|
813
|
+
res = self._check_expected(expected, actual_exceptions[i_rem])
|
|
814
|
+
results.set_result(i_exp, i_rem, res)
|
|
815
|
+
if res is None:
|
|
816
|
+
remaining_actual.remove(i_rem)
|
|
817
|
+
matches[i_exp] = i_rem
|
|
818
|
+
break
|
|
819
|
+
else:
|
|
820
|
+
failed_expected.append(i_exp)
|
|
821
|
+
|
|
822
|
+
# All exceptions matched up successfully
|
|
823
|
+
if not remaining_actual and not failed_expected:
|
|
824
|
+
return True
|
|
825
|
+
|
|
826
|
+
# in case of a single expected and single raised we simplify the output
|
|
827
|
+
if 1 == len(actual_exceptions) == len(self.expected_exceptions):
|
|
828
|
+
assert not matches
|
|
829
|
+
self._fail_reason = res
|
|
830
|
+
return False
|
|
831
|
+
|
|
832
|
+
# The test case is failing, so we can do a slow and exhaustive check to find
|
|
833
|
+
# duplicate matches etc that will be helpful in debugging
|
|
834
|
+
for i_exp, expected in enumerate(self.expected_exceptions):
|
|
835
|
+
for i_actual, actual in enumerate(actual_exceptions):
|
|
836
|
+
if results.has_result(i_exp, i_actual):
|
|
837
|
+
continue
|
|
838
|
+
results.set_result(
|
|
839
|
+
i_exp, i_actual, self._check_expected(expected, actual)
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
successful_str = (
|
|
843
|
+
f"{len(matches)} matched exception{'s' if len(matches) > 1 else ''}. "
|
|
844
|
+
if matches
|
|
845
|
+
else ""
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
# all expected were found
|
|
849
|
+
if not failed_expected and results.no_match_for_actual(remaining_actual):
|
|
850
|
+
self._fail_reason = f"{successful_str}Unexpected exception(s): {[actual_exceptions[i] for i in remaining_actual]!r}"
|
|
851
|
+
return False
|
|
852
|
+
# all raised exceptions were expected
|
|
853
|
+
if not remaining_actual and results.no_match_for_expected(failed_expected):
|
|
854
|
+
self._fail_reason = f"{successful_str}Too few exceptions raised, found no match for: [{', '.join(self._repr_expected(self.expected_exceptions[i]) for i in failed_expected)}]"
|
|
855
|
+
return False
|
|
856
|
+
|
|
857
|
+
# if there's only one remaining and one failed, and the unmatched didn't match anything else,
|
|
858
|
+
# we elect to only print why the remaining and the failed didn't match.
|
|
859
|
+
if (
|
|
860
|
+
1 == len(remaining_actual) == len(failed_expected)
|
|
861
|
+
and results.no_match_for_actual(remaining_actual)
|
|
862
|
+
and results.no_match_for_expected(failed_expected)
|
|
863
|
+
):
|
|
864
|
+
self._fail_reason = f"{successful_str}{results.get_result(failed_expected[0], remaining_actual[0])}"
|
|
865
|
+
return False
|
|
866
|
+
|
|
867
|
+
# there's both expected and raised exceptions without matches
|
|
868
|
+
s = ""
|
|
869
|
+
if matches:
|
|
870
|
+
s += f"\n{successful_str}"
|
|
871
|
+
indent_1 = " " * 2
|
|
872
|
+
indent_2 = " " * 4
|
|
873
|
+
|
|
874
|
+
if not remaining_actual:
|
|
875
|
+
s += "\nToo few exceptions raised!"
|
|
876
|
+
elif not failed_expected:
|
|
877
|
+
s += "\nUnexpected exception(s)!"
|
|
878
|
+
|
|
879
|
+
if failed_expected:
|
|
880
|
+
s += "\nThe following expected exceptions did not find a match:"
|
|
881
|
+
rev_matches = {v: k for k, v in matches.items()}
|
|
882
|
+
for i_failed in failed_expected:
|
|
883
|
+
s += (
|
|
884
|
+
f"\n{indent_1}{self._repr_expected(self.expected_exceptions[i_failed])}"
|
|
885
|
+
)
|
|
886
|
+
for i_actual, actual in enumerate(actual_exceptions):
|
|
887
|
+
if results.get_result(i_exp, i_actual) is None:
|
|
888
|
+
# we print full repr of match target
|
|
889
|
+
s += f"\n{indent_2}It matches {actual!r} which was paired with {self._repr_expected(self.expected_exceptions[rev_matches[i_actual]])}"
|
|
890
|
+
|
|
891
|
+
if remaining_actual:
|
|
892
|
+
s += "\nThe following raised exceptions did not find a match"
|
|
893
|
+
for i_actual in remaining_actual:
|
|
894
|
+
s += f"\n{indent_1}{actual_exceptions[i_actual]!r}:"
|
|
895
|
+
for i_exp, expected in enumerate(self.expected_exceptions):
|
|
896
|
+
res = results.get_result(i_exp, i_actual)
|
|
897
|
+
if i_exp in failed_expected:
|
|
898
|
+
assert res is not None
|
|
899
|
+
if res[0] != "\n":
|
|
900
|
+
s += "\n"
|
|
901
|
+
s += indent(res, indent_2)
|
|
902
|
+
if res is None:
|
|
903
|
+
# we print full repr of match target
|
|
904
|
+
s += f"\n{indent_2}It matches {self._repr_expected(expected)} which was paired with {actual_exceptions[matches[i_exp]]!r}"
|
|
905
|
+
|
|
906
|
+
if len(self.expected_exceptions) == len(actual_exceptions) and possible_match(
|
|
907
|
+
results
|
|
908
|
+
):
|
|
909
|
+
s += "\nThere exist a possible match when attempting an exhaustive check, but RaisesGroup uses a greedy algorithm. Please make your expected exceptions more stringent with `Matcher` etc so the greedy algorithm can function."
|
|
910
|
+
self._fail_reason = s
|
|
911
|
+
return False
|
|
912
|
+
|
|
913
|
+
def __exit__(
|
|
914
|
+
self,
|
|
915
|
+
exc_type: type[BaseException] | None,
|
|
916
|
+
exc_val: BaseException | None,
|
|
917
|
+
exc_tb: types.TracebackType | None,
|
|
918
|
+
) -> bool:
|
|
919
|
+
__tracebackhide__ = True
|
|
920
|
+
assert (
|
|
921
|
+
exc_type is not None
|
|
922
|
+
), f"DID NOT RAISE any exception, expected {self.expected_type()}"
|
|
923
|
+
assert (
|
|
924
|
+
self.excinfo is not None
|
|
925
|
+
), "Internal error - should have been constructed in __enter__"
|
|
926
|
+
|
|
927
|
+
group_str = (
|
|
928
|
+
"(group)"
|
|
929
|
+
if self.allow_unwrapped and not issubclass(exc_type, BaseExceptionGroup)
|
|
930
|
+
else "group"
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
assert self.matches(
|
|
934
|
+
exc_val,
|
|
935
|
+
), f"Raised exception {group_str} did not match: {self._fail_reason}"
|
|
936
|
+
|
|
937
|
+
# Cast to narrow the exception type now that it's verified.
|
|
938
|
+
exc_info = cast(
|
|
939
|
+
"tuple[type[BaseExceptionGroup[BaseExcT_co]], BaseExceptionGroup[BaseExcT_co], types.TracebackType]",
|
|
940
|
+
(exc_type, exc_val, exc_tb),
|
|
941
|
+
)
|
|
942
|
+
self.excinfo.fill_unfilled(exc_info)
|
|
943
|
+
return True
|
|
944
|
+
|
|
945
|
+
def expected_type(self) -> str:
|
|
946
|
+
subexcs = []
|
|
947
|
+
for e in self.expected_exceptions:
|
|
948
|
+
if isinstance(e, Matcher):
|
|
949
|
+
subexcs.append(str(e))
|
|
950
|
+
elif isinstance(e, RaisesGroup):
|
|
951
|
+
subexcs.append(e.expected_type())
|
|
952
|
+
elif isinstance(e, type):
|
|
953
|
+
subexcs.append(e.__name__)
|
|
954
|
+
else: # pragma: no cover
|
|
955
|
+
raise AssertionError("unknown type")
|
|
956
|
+
group_type = "Base" if self.is_baseexceptiongroup else ""
|
|
957
|
+
return f"{group_type}ExceptionGroup({', '.join(subexcs)})"
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
@final
|
|
961
|
+
class NotChecked: ...
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
class ResultHolder:
|
|
965
|
+
def __init__(
|
|
966
|
+
self,
|
|
967
|
+
expected_exceptions: tuple[
|
|
968
|
+
type[BaseException] | AbstractMatcher[BaseException], ...
|
|
969
|
+
],
|
|
970
|
+
actual_exceptions: Sequence[BaseException],
|
|
971
|
+
) -> None:
|
|
972
|
+
self.results: list[list[str | type[NotChecked] | None]] = [
|
|
973
|
+
[NotChecked for _ in expected_exceptions] for _ in actual_exceptions
|
|
974
|
+
]
|
|
975
|
+
|
|
976
|
+
def set_result(self, expected: int, actual: int, result: str | None) -> None:
|
|
977
|
+
self.results[actual][expected] = result
|
|
978
|
+
|
|
979
|
+
def get_result(self, expected: int, actual: int) -> str | None:
|
|
980
|
+
res = self.results[actual][expected]
|
|
981
|
+
# mypy doesn't support `assert res is not NotChecked`
|
|
982
|
+
assert not isinstance(res, type)
|
|
983
|
+
return res
|
|
984
|
+
|
|
985
|
+
def has_result(self, expected: int, actual: int) -> bool:
|
|
986
|
+
return self.results[actual][expected] is not NotChecked
|
|
987
|
+
|
|
988
|
+
def no_match_for_expected(self, expected: list[int]) -> bool:
|
|
989
|
+
for i in expected:
|
|
990
|
+
for actual_results in self.results:
|
|
991
|
+
assert actual_results[i] is not NotChecked
|
|
992
|
+
if actual_results[i] is None:
|
|
993
|
+
return False
|
|
994
|
+
return True
|
|
995
|
+
|
|
996
|
+
def no_match_for_actual(self, actual: list[int]) -> bool:
|
|
997
|
+
for i in actual:
|
|
998
|
+
for res in self.results[i]:
|
|
999
|
+
assert res is not NotChecked
|
|
1000
|
+
if res is None:
|
|
1001
|
+
return False
|
|
1002
|
+
return True
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def possible_match(results: ResultHolder, used: set[int] | None = None) -> bool:
|
|
1006
|
+
if used is None:
|
|
1007
|
+
used = set()
|
|
1008
|
+
curr_row = len(used)
|
|
1009
|
+
if curr_row == len(results.results):
|
|
1010
|
+
return True
|
|
1011
|
+
|
|
1012
|
+
for i, val in enumerate(results.results[curr_row]):
|
|
1013
|
+
if val is None and i not in used and possible_match(results, used | {i}):
|
|
1014
|
+
return True
|
|
1015
|
+
return False
|