@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,626 @@
|
|
|
1
|
+
from __future__ import annotations # isort: split
|
|
2
|
+
|
|
3
|
+
import __future__ # Regular import, not special!
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
import functools
|
|
7
|
+
import importlib
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
import socket as stdlib_socket
|
|
11
|
+
import sys
|
|
12
|
+
import types
|
|
13
|
+
from pathlib import Path, PurePath
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
16
|
+
|
|
17
|
+
import attrs
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
import trio
|
|
21
|
+
import trio.testing
|
|
22
|
+
from trio._tests.pytest_plugin import RUN_SLOW, skip_if_optional_else_raise
|
|
23
|
+
|
|
24
|
+
from .. import _core, _util
|
|
25
|
+
from .._core._tests.tutil import slow
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Iterable, Iterator
|
|
29
|
+
|
|
30
|
+
mypy_cache_updated = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
try: # If installed, check both versions of this class.
|
|
34
|
+
from typing_extensions import Protocol as Protocol_ext
|
|
35
|
+
except ImportError: # pragma: no cover
|
|
36
|
+
Protocol_ext = Protocol
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_mypy_cache_updated() -> None:
|
|
40
|
+
# This pollutes the `empty` dir. Should this be changed?
|
|
41
|
+
try:
|
|
42
|
+
from mypy.api import run
|
|
43
|
+
except ImportError as error:
|
|
44
|
+
skip_if_optional_else_raise(error)
|
|
45
|
+
|
|
46
|
+
global mypy_cache_updated
|
|
47
|
+
if not mypy_cache_updated:
|
|
48
|
+
# mypy cache was *probably* already updated by the other tests,
|
|
49
|
+
# but `pytest -k ...` might run just this test on its own
|
|
50
|
+
result = run(
|
|
51
|
+
[
|
|
52
|
+
"--config-file=",
|
|
53
|
+
"--cache-dir=./.mypy_cache",
|
|
54
|
+
"--no-error-summary",
|
|
55
|
+
"-c",
|
|
56
|
+
"import trio",
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
assert not result[1] # stderr
|
|
60
|
+
assert not result[0] # stdout
|
|
61
|
+
mypy_cache_updated = True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_core_is_properly_reexported() -> None:
|
|
65
|
+
# Each export from _core should be re-exported by exactly one of these
|
|
66
|
+
# three modules:
|
|
67
|
+
sources = [trio, trio.lowlevel, trio.testing]
|
|
68
|
+
for symbol in dir(_core):
|
|
69
|
+
if symbol.startswith("_"):
|
|
70
|
+
continue
|
|
71
|
+
found = 0
|
|
72
|
+
for source in sources:
|
|
73
|
+
if symbol in dir(source) and getattr(source, symbol) is getattr(
|
|
74
|
+
_core,
|
|
75
|
+
symbol,
|
|
76
|
+
):
|
|
77
|
+
found += 1
|
|
78
|
+
print(symbol, found)
|
|
79
|
+
assert found == 1
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def class_is_final(cls: type) -> bool:
|
|
83
|
+
"""Check if a class cannot be subclassed."""
|
|
84
|
+
try:
|
|
85
|
+
# new_class() handles metaclasses properly, type(...) does not.
|
|
86
|
+
types.new_class("SubclassTester", (cls,))
|
|
87
|
+
except TypeError:
|
|
88
|
+
return True
|
|
89
|
+
else:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def iter_modules(
|
|
94
|
+
module: types.ModuleType,
|
|
95
|
+
only_public: bool,
|
|
96
|
+
) -> Iterator[types.ModuleType]:
|
|
97
|
+
yield module
|
|
98
|
+
for name, class_ in module.__dict__.items():
|
|
99
|
+
if name.startswith("_") and only_public:
|
|
100
|
+
continue
|
|
101
|
+
if not isinstance(class_, ModuleType):
|
|
102
|
+
continue
|
|
103
|
+
if not class_.__name__.startswith(module.__name__): # pragma: no cover
|
|
104
|
+
continue
|
|
105
|
+
if class_ is module: # pragma: no cover
|
|
106
|
+
continue
|
|
107
|
+
yield from iter_modules(class_, only_public)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
PUBLIC_MODULES = list(iter_modules(trio, only_public=True))
|
|
111
|
+
ALL_MODULES = list(iter_modules(trio, only_public=False))
|
|
112
|
+
PUBLIC_MODULE_NAMES = [m.__name__ for m in PUBLIC_MODULES]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# It doesn't make sense for downstream redistributors to run this test, since
|
|
116
|
+
# they might be using a newer version of Python with additional symbols which
|
|
117
|
+
# won't be reflected in trio.socket, and this shouldn't cause downstream test
|
|
118
|
+
# runs to start failing.
|
|
119
|
+
@pytest.mark.redistributors_should_skip
|
|
120
|
+
@pytest.mark.skipif(
|
|
121
|
+
sys.version_info[:4] == (3, 14, 0, "beta"),
|
|
122
|
+
# 12 pass, 16 fail
|
|
123
|
+
reason="several tools don't support 3.14",
|
|
124
|
+
)
|
|
125
|
+
# Static analysis tools often have trouble with alpha releases, where Python's
|
|
126
|
+
# internals are in flux, grammar may not have settled down, etc.
|
|
127
|
+
@pytest.mark.skipif(
|
|
128
|
+
sys.version_info.releaselevel == "alpha",
|
|
129
|
+
reason="skip static introspection tools on Python dev/alpha releases",
|
|
130
|
+
)
|
|
131
|
+
@pytest.mark.parametrize("modname", PUBLIC_MODULE_NAMES)
|
|
132
|
+
@pytest.mark.parametrize("tool", ["pylint", "jedi", "mypy", "pyright_verifytypes"])
|
|
133
|
+
@pytest.mark.filterwarnings(
|
|
134
|
+
# https://github.com/pypa/setuptools/issues/3274
|
|
135
|
+
"ignore:module 'sre_constants' is deprecated:DeprecationWarning",
|
|
136
|
+
)
|
|
137
|
+
def test_static_tool_sees_all_symbols(tool: str, modname: str, tmp_path: Path) -> None:
|
|
138
|
+
module = importlib.import_module(modname)
|
|
139
|
+
|
|
140
|
+
def no_underscores(symbols: Iterable[str]) -> set[str]:
|
|
141
|
+
return {symbol for symbol in symbols if not symbol.startswith("_")}
|
|
142
|
+
|
|
143
|
+
runtime_names = no_underscores(dir(module))
|
|
144
|
+
|
|
145
|
+
# ignore deprecated module `tests` being invisible
|
|
146
|
+
if modname == "trio":
|
|
147
|
+
runtime_names.discard("tests")
|
|
148
|
+
|
|
149
|
+
# Ignore any __future__ feature objects, if imported under that name.
|
|
150
|
+
for name in __future__.all_feature_names:
|
|
151
|
+
if getattr(module, name, None) is getattr(__future__, name):
|
|
152
|
+
runtime_names.remove(name)
|
|
153
|
+
|
|
154
|
+
if tool == "pylint":
|
|
155
|
+
try:
|
|
156
|
+
from pylint.lint import PyLinter
|
|
157
|
+
except ImportError as error:
|
|
158
|
+
skip_if_optional_else_raise(error)
|
|
159
|
+
|
|
160
|
+
linter = PyLinter()
|
|
161
|
+
assert module.__file__ is not None
|
|
162
|
+
ast = linter.get_ast(module.__file__, modname)
|
|
163
|
+
static_names = no_underscores(ast) # type: ignore[arg-type]
|
|
164
|
+
elif tool == "jedi":
|
|
165
|
+
if sys.implementation.name != "cpython":
|
|
166
|
+
pytest.skip("jedi does not support pypy")
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
import jedi
|
|
170
|
+
except ImportError as error:
|
|
171
|
+
skip_if_optional_else_raise(error)
|
|
172
|
+
|
|
173
|
+
# Simulate typing "import trio; trio.<TAB>"
|
|
174
|
+
script = jedi.Script(f"import {modname}; {modname}.")
|
|
175
|
+
completions = script.complete()
|
|
176
|
+
static_names = no_underscores(c.name for c in completions)
|
|
177
|
+
elif tool == "mypy":
|
|
178
|
+
if sys.implementation.name != "cpython":
|
|
179
|
+
# https://github.com/python/mypy/issues/20329
|
|
180
|
+
pytest.skip("mypy does not support pypy")
|
|
181
|
+
|
|
182
|
+
if not RUN_SLOW: # pragma: no cover
|
|
183
|
+
pytest.skip("use --run-slow to check against mypy")
|
|
184
|
+
|
|
185
|
+
cache = Path.cwd() / ".mypy_cache"
|
|
186
|
+
|
|
187
|
+
_ensure_mypy_cache_updated()
|
|
188
|
+
|
|
189
|
+
trio_cache = next(cache.glob("*/trio"))
|
|
190
|
+
_, modname = (modname + ".").split(".", 1)
|
|
191
|
+
modname = modname[:-1]
|
|
192
|
+
mod_cache = trio_cache / modname if modname else trio_cache
|
|
193
|
+
if mod_cache.is_dir(): # pragma: no coverage
|
|
194
|
+
mod_cache = mod_cache / "__init__.data.json"
|
|
195
|
+
else:
|
|
196
|
+
mod_cache = trio_cache / (modname + ".data.json")
|
|
197
|
+
|
|
198
|
+
assert mod_cache.exists()
|
|
199
|
+
assert mod_cache.is_file()
|
|
200
|
+
with mod_cache.open() as cache_file:
|
|
201
|
+
cache_json = json.loads(cache_file.read())
|
|
202
|
+
static_names = no_underscores(
|
|
203
|
+
key
|
|
204
|
+
for key, value in cache_json["names"].items()
|
|
205
|
+
if not key.startswith(".") and value["kind"] == "Gdef"
|
|
206
|
+
)
|
|
207
|
+
elif tool == "pyright_verifytypes":
|
|
208
|
+
if not RUN_SLOW: # pragma: no cover
|
|
209
|
+
pytest.skip("use --run-slow to check against pyright")
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
import pyright # noqa: F401
|
|
213
|
+
except ImportError as error:
|
|
214
|
+
skip_if_optional_else_raise(error)
|
|
215
|
+
import subprocess
|
|
216
|
+
|
|
217
|
+
res = subprocess.run(
|
|
218
|
+
["pyright", f"--verifytypes={modname}", "--outputjson"],
|
|
219
|
+
capture_output=True,
|
|
220
|
+
)
|
|
221
|
+
current_result = json.loads(res.stdout)
|
|
222
|
+
|
|
223
|
+
static_names = {
|
|
224
|
+
x["name"][len(modname) + 1 :]
|
|
225
|
+
for x in current_result["typeCompleteness"]["symbols"]
|
|
226
|
+
if x["name"].startswith(modname)
|
|
227
|
+
}
|
|
228
|
+
else: # pragma: no cover
|
|
229
|
+
raise AssertionError()
|
|
230
|
+
|
|
231
|
+
# It's expected that the static set will contain more names than the
|
|
232
|
+
# runtime set:
|
|
233
|
+
# - static tools are sometimes sloppy and include deleted names
|
|
234
|
+
# - some symbols are platform-specific at runtime, but always show up in
|
|
235
|
+
# static analysis (e.g. in trio.socket or trio.lowlevel)
|
|
236
|
+
# So we check that the runtime names are a subset of the static names.
|
|
237
|
+
missing_names = runtime_names - static_names
|
|
238
|
+
|
|
239
|
+
# ignore warnings about deprecated module tests
|
|
240
|
+
missing_names -= {"tests"}
|
|
241
|
+
|
|
242
|
+
if missing_names: # pragma: no cover
|
|
243
|
+
print(f"{tool} can't see the following names in {modname}:")
|
|
244
|
+
print()
|
|
245
|
+
for name in sorted(missing_names):
|
|
246
|
+
print(f" {name}")
|
|
247
|
+
raise AssertionError()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@slow
|
|
251
|
+
# see comment on test_static_tool_sees_all_symbols
|
|
252
|
+
@pytest.mark.redistributors_should_skip
|
|
253
|
+
# Static analysis tools often have trouble with alpha releases, where Python's
|
|
254
|
+
# internals are in flux, grammar may not have settled down, etc.
|
|
255
|
+
@pytest.mark.skipif(
|
|
256
|
+
sys.version_info.releaselevel == "alpha",
|
|
257
|
+
reason="skip static introspection tools on Python dev/alpha releases",
|
|
258
|
+
)
|
|
259
|
+
@pytest.mark.parametrize("module_name", PUBLIC_MODULE_NAMES)
|
|
260
|
+
@pytest.mark.parametrize("tool", ["jedi", "mypy"])
|
|
261
|
+
def test_static_tool_sees_class_members(
|
|
262
|
+
tool: str,
|
|
263
|
+
module_name: str,
|
|
264
|
+
tmp_path: Path,
|
|
265
|
+
) -> None:
|
|
266
|
+
module = PUBLIC_MODULES[PUBLIC_MODULE_NAMES.index(module_name)]
|
|
267
|
+
|
|
268
|
+
# ignore hidden, but not dunder, symbols
|
|
269
|
+
def no_hidden(symbols: Iterable[str]) -> set[str]:
|
|
270
|
+
return {
|
|
271
|
+
symbol
|
|
272
|
+
for symbol in symbols
|
|
273
|
+
if (not symbol.startswith("_")) or symbol.startswith("__")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if tool == "jedi" and sys.implementation.name != "cpython":
|
|
277
|
+
pytest.skip("jedi does not support pypy")
|
|
278
|
+
|
|
279
|
+
if tool == "mypy" and sys.implementation.name != "cpython":
|
|
280
|
+
# https://github.com/python/mypy/issues/20329
|
|
281
|
+
pytest.skip("mypy does not support pypy")
|
|
282
|
+
|
|
283
|
+
if tool == "mypy":
|
|
284
|
+
cache = Path.cwd() / ".mypy_cache"
|
|
285
|
+
|
|
286
|
+
_ensure_mypy_cache_updated()
|
|
287
|
+
|
|
288
|
+
trio_cache = next(cache.glob("*/trio"))
|
|
289
|
+
modname = module_name
|
|
290
|
+
_, modname = (modname + ".").split(".", 1)
|
|
291
|
+
modname = modname[:-1]
|
|
292
|
+
mod_cache = trio_cache / modname if modname else trio_cache
|
|
293
|
+
if mod_cache.is_dir():
|
|
294
|
+
mod_cache = mod_cache / "__init__.data.json"
|
|
295
|
+
else:
|
|
296
|
+
mod_cache = trio_cache / (modname + ".data.json")
|
|
297
|
+
|
|
298
|
+
assert mod_cache.exists()
|
|
299
|
+
assert mod_cache.is_file()
|
|
300
|
+
with mod_cache.open() as cache_file:
|
|
301
|
+
cache_json = json.loads(cache_file.read())
|
|
302
|
+
|
|
303
|
+
# skip a bunch of file-system activity (probably can un-memoize?)
|
|
304
|
+
@functools.lru_cache
|
|
305
|
+
def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit-any]
|
|
306
|
+
topname, *modname, name = symbol.split(".")
|
|
307
|
+
version = next(cache.glob("3.*/"))
|
|
308
|
+
mod_cache = version / topname
|
|
309
|
+
if not mod_cache.is_dir():
|
|
310
|
+
mod_cache = version / (topname + ".data.json")
|
|
311
|
+
|
|
312
|
+
if modname:
|
|
313
|
+
for piece in modname[:-1]:
|
|
314
|
+
mod_cache /= piece
|
|
315
|
+
next_cache = mod_cache / modname[-1]
|
|
316
|
+
if next_cache.is_dir(): # pragma: no coverage
|
|
317
|
+
mod_cache = next_cache / "__init__.data.json"
|
|
318
|
+
else:
|
|
319
|
+
mod_cache = mod_cache / (modname[-1] + ".data.json")
|
|
320
|
+
elif mod_cache.is_dir():
|
|
321
|
+
mod_cache /= "__init__.data.json"
|
|
322
|
+
with mod_cache.open() as f:
|
|
323
|
+
return json.loads(f.read())["names"][name] # type: ignore[no-any-return]
|
|
324
|
+
|
|
325
|
+
errors: dict[str, object] = {}
|
|
326
|
+
for class_name, class_ in module.__dict__.items():
|
|
327
|
+
if not isinstance(class_, type):
|
|
328
|
+
continue
|
|
329
|
+
if module_name == "trio.socket" and class_name in dir(stdlib_socket):
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
# Ignore classes that don't use attrs, they only define their members once
|
|
333
|
+
# __init__ is called (and reason they don't use attrs is because they're going
|
|
334
|
+
# to be reimplemented in pytest).
|
|
335
|
+
# Not 100% that's the case, and it works locally, so whatever /shrug
|
|
336
|
+
if module_name == "trio.testing" and class_name in ("_RaisesGroup", "_Matcher"):
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
# dir() and inspect.getmembers doesn't display properties from the metaclass
|
|
340
|
+
# also ignore some dunder methods that tend to differ but are of no consequence
|
|
341
|
+
ignore_names = set(dir(type(class_))) | {
|
|
342
|
+
"__annotations__",
|
|
343
|
+
"__attrs_attrs__",
|
|
344
|
+
"__attrs_own_setattr__",
|
|
345
|
+
"__callable_proto_members_only__",
|
|
346
|
+
"__class_getitem__",
|
|
347
|
+
"__final__",
|
|
348
|
+
"__getstate__",
|
|
349
|
+
"__match_args__",
|
|
350
|
+
"__order__",
|
|
351
|
+
"__orig_bases__",
|
|
352
|
+
"__parameters__",
|
|
353
|
+
"__protocol_attrs__",
|
|
354
|
+
"__setstate__",
|
|
355
|
+
"__slots__",
|
|
356
|
+
"__weakref__",
|
|
357
|
+
# ignore errors about dunders inherited from stdlib that tools might
|
|
358
|
+
# not see
|
|
359
|
+
"__copy__",
|
|
360
|
+
"__deepcopy__",
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if type(class_) is type:
|
|
364
|
+
# C extension classes don't have these dunders, but Python classes do
|
|
365
|
+
ignore_names.add("__firstlineno__")
|
|
366
|
+
ignore_names.add("__static_attributes__")
|
|
367
|
+
|
|
368
|
+
# inspect.getmembers sees `name` and `value` in Enums, otherwise
|
|
369
|
+
# it behaves the same way as `dir`
|
|
370
|
+
# runtime_names = no_underscores(dir(class_))
|
|
371
|
+
runtime_names = (
|
|
372
|
+
no_hidden(x[0] for x in inspect.getmembers(class_)) - ignore_names
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if tool == "jedi":
|
|
376
|
+
try:
|
|
377
|
+
import jedi
|
|
378
|
+
except ImportError as error:
|
|
379
|
+
skip_if_optional_else_raise(error)
|
|
380
|
+
|
|
381
|
+
script = jedi.Script(
|
|
382
|
+
f"from {module_name} import {class_name}; {class_name}.",
|
|
383
|
+
)
|
|
384
|
+
completions = script.complete()
|
|
385
|
+
static_names = no_hidden(c.name for c in completions) - ignore_names
|
|
386
|
+
|
|
387
|
+
elif tool == "mypy":
|
|
388
|
+
# load the cached type information
|
|
389
|
+
cached_type_info = cache_json["names"][class_name]
|
|
390
|
+
assert (
|
|
391
|
+
"node" not in cached_type_info
|
|
392
|
+
), "previously this was an 'if' but it seems it's no longer possible for this cache to contain 'node', if this assert raises for you please let us know!"
|
|
393
|
+
cached_type_info = lookup_symbol(cached_type_info["cross_ref"])
|
|
394
|
+
|
|
395
|
+
assert "node" in cached_type_info
|
|
396
|
+
node = cached_type_info["node"]
|
|
397
|
+
static_names = no_hidden(
|
|
398
|
+
k for k in node.get("names", ()) if not k.startswith(".")
|
|
399
|
+
)
|
|
400
|
+
for symbol in node["mro"][1:]:
|
|
401
|
+
node = lookup_symbol(symbol)["node"]
|
|
402
|
+
static_names |= no_hidden(
|
|
403
|
+
k for k in node.get("names", ()) if not k.startswith(".")
|
|
404
|
+
)
|
|
405
|
+
static_names -= ignore_names
|
|
406
|
+
|
|
407
|
+
else: # pragma: no cover
|
|
408
|
+
raise AssertionError("unknown tool")
|
|
409
|
+
|
|
410
|
+
missing = runtime_names - static_names
|
|
411
|
+
extra = static_names - runtime_names
|
|
412
|
+
|
|
413
|
+
# using .remove() instead of .delete() to get an error in case they start not
|
|
414
|
+
# being missing
|
|
415
|
+
|
|
416
|
+
if (
|
|
417
|
+
tool == "jedi"
|
|
418
|
+
and BaseException in class_.__mro__
|
|
419
|
+
and sys.version_info >= (3, 11)
|
|
420
|
+
):
|
|
421
|
+
missing.remove("add_note")
|
|
422
|
+
|
|
423
|
+
if (
|
|
424
|
+
tool == "mypy"
|
|
425
|
+
and BaseException in class_.__mro__
|
|
426
|
+
and sys.version_info >= (3, 11)
|
|
427
|
+
):
|
|
428
|
+
extra.remove("__notes__")
|
|
429
|
+
|
|
430
|
+
if tool == "mypy" and attrs.has(class_):
|
|
431
|
+
# e.g. __trio__core__run_CancelScope_AttrsAttributes__
|
|
432
|
+
before = len(extra)
|
|
433
|
+
extra = {e for e in extra if not e.endswith("AttrsAttributes__")}
|
|
434
|
+
assert len(extra) == before - 1
|
|
435
|
+
|
|
436
|
+
if attrs.has(class_):
|
|
437
|
+
# dynamically created attribute by attrs?
|
|
438
|
+
missing.remove("__attrs_props__")
|
|
439
|
+
|
|
440
|
+
# dir does not see `__signature__` on enums until 3.14
|
|
441
|
+
if (
|
|
442
|
+
tool == "mypy"
|
|
443
|
+
and enum.Enum in class_.__mro__
|
|
444
|
+
and sys.version_info >= (3, 12)
|
|
445
|
+
and sys.version_info < (3, 14)
|
|
446
|
+
):
|
|
447
|
+
extra.remove("__signature__")
|
|
448
|
+
|
|
449
|
+
# TODO: this *should* be visible via `dir`!!
|
|
450
|
+
if tool == "mypy" and class_ == trio.Nursery:
|
|
451
|
+
extra.remove("cancel_scope")
|
|
452
|
+
|
|
453
|
+
# These are (mostly? solely?) *runtime* attributes, often set in
|
|
454
|
+
# __init__, which doesn't show up with dir() or inspect.getmembers,
|
|
455
|
+
# but we get them in the way we query mypy & jedi
|
|
456
|
+
EXTRAS = {
|
|
457
|
+
trio.DTLSChannel: {"peer_address", "endpoint"},
|
|
458
|
+
trio.DTLSEndpoint: {"socket", "incoming_packets_buffer"},
|
|
459
|
+
trio.Process: {"args", "pid", "stderr", "stdin", "stdio", "stdout"},
|
|
460
|
+
trio.SSLListener: {"transport_listener"},
|
|
461
|
+
trio.SSLStream: {"transport_stream"},
|
|
462
|
+
trio.SocketListener: {"socket"},
|
|
463
|
+
trio.SocketStream: {"socket"},
|
|
464
|
+
trio.testing.MemoryReceiveStream: {"close_hook", "receive_some_hook"},
|
|
465
|
+
trio.testing.MemorySendStream: {
|
|
466
|
+
"close_hook",
|
|
467
|
+
"send_all_hook",
|
|
468
|
+
"wait_send_all_might_not_block_hook",
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
if tool == "mypy" and class_ in EXTRAS:
|
|
472
|
+
before = len(extra)
|
|
473
|
+
extra -= EXTRAS[class_]
|
|
474
|
+
assert len(extra) == before - len(EXTRAS[class_])
|
|
475
|
+
|
|
476
|
+
# TODO: why is this? Is it a problem?
|
|
477
|
+
# see https://github.com/python-trio/trio/pull/2631#discussion_r1185615916
|
|
478
|
+
if class_ == trio.StapledStream:
|
|
479
|
+
extra.remove("receive_stream")
|
|
480
|
+
extra.remove("send_stream")
|
|
481
|
+
|
|
482
|
+
# I have not researched why these are missing, should maybe create an issue
|
|
483
|
+
# upstream with jedi
|
|
484
|
+
if tool == "jedi" and sys.version_info >= (3, 11):
|
|
485
|
+
if class_ in (
|
|
486
|
+
trio.DTLSChannel,
|
|
487
|
+
trio.MemoryReceiveChannel,
|
|
488
|
+
trio.MemorySendChannel,
|
|
489
|
+
trio.SSLListener,
|
|
490
|
+
trio.SocketListener,
|
|
491
|
+
):
|
|
492
|
+
missing.remove("__aenter__")
|
|
493
|
+
missing.remove("__aexit__")
|
|
494
|
+
if class_ in (trio.DTLSChannel, trio.MemoryReceiveChannel):
|
|
495
|
+
missing.remove("__aiter__")
|
|
496
|
+
missing.remove("__anext__")
|
|
497
|
+
|
|
498
|
+
if class_ in (trio.Path, trio.WindowsPath, trio.PosixPath):
|
|
499
|
+
# These are from inherited subclasses.
|
|
500
|
+
missing -= PurePath.__dict__.keys()
|
|
501
|
+
# These are unix-only.
|
|
502
|
+
if tool == "mypy" and sys.platform == "win32":
|
|
503
|
+
missing -= {"owner", "is_mount", "group"}
|
|
504
|
+
if tool == "jedi" and sys.platform == "win32":
|
|
505
|
+
extra -= {"owner", "is_mount", "group"}
|
|
506
|
+
|
|
507
|
+
# not sure why jedi in particular ignores this (static?) method in 3.13
|
|
508
|
+
if (
|
|
509
|
+
tool == "jedi"
|
|
510
|
+
and sys.version_info[:2] == (3, 13)
|
|
511
|
+
and class_ in (trio.Path, trio.WindowsPath, trio.PosixPath)
|
|
512
|
+
):
|
|
513
|
+
missing.remove("with_segments")
|
|
514
|
+
|
|
515
|
+
# tuple subclasses are weird
|
|
516
|
+
if issubclass(class_, tuple):
|
|
517
|
+
extra.remove("__reversed__")
|
|
518
|
+
missing.remove("__getnewargs__")
|
|
519
|
+
|
|
520
|
+
if sys.version_info >= (3, 13) and attrs.has(class_):
|
|
521
|
+
missing.remove("__replace__")
|
|
522
|
+
|
|
523
|
+
if sys.version_info >= (3, 14):
|
|
524
|
+
# these depend on whether a class has processed deferred annotations.
|
|
525
|
+
# (which might or might not happen and we don't know)
|
|
526
|
+
missing.discard("__annotate_func__")
|
|
527
|
+
missing.discard("__annotations_cache__")
|
|
528
|
+
|
|
529
|
+
if missing or extra: # pragma: no cover
|
|
530
|
+
errors[f"{module_name}.{class_name}"] = {
|
|
531
|
+
"missing": missing,
|
|
532
|
+
"extra": extra,
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# `assert not errors` will not print the full content of errors, even with
|
|
536
|
+
# `--verbose`, so we manually print it
|
|
537
|
+
if errors: # pragma: no cover
|
|
538
|
+
from pprint import pprint
|
|
539
|
+
|
|
540
|
+
print(f"\n{tool} can't see the following symbols in {module_name}:")
|
|
541
|
+
pprint(errors)
|
|
542
|
+
assert not errors
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def test_nopublic_is_final() -> None:
|
|
546
|
+
"""Check all NoPublicConstructor classes are also @final."""
|
|
547
|
+
assert class_is_final(_util.NoPublicConstructor) # This is itself final.
|
|
548
|
+
|
|
549
|
+
for module in ALL_MODULES:
|
|
550
|
+
for class_ in module.__dict__.values():
|
|
551
|
+
if isinstance(class_, _util.NoPublicConstructor):
|
|
552
|
+
assert class_is_final(class_)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_classes_are_final() -> None:
|
|
556
|
+
# Sanity checks.
|
|
557
|
+
assert not class_is_final(object)
|
|
558
|
+
assert class_is_final(bool)
|
|
559
|
+
|
|
560
|
+
for module in PUBLIC_MODULES:
|
|
561
|
+
for name, class_ in module.__dict__.items():
|
|
562
|
+
if not isinstance(class_, type):
|
|
563
|
+
continue
|
|
564
|
+
# Deprecated classes are exported with a leading underscore
|
|
565
|
+
if name.startswith("_"): # pragma: no cover
|
|
566
|
+
continue
|
|
567
|
+
|
|
568
|
+
# Abstract classes can be subclassed, because that's the whole
|
|
569
|
+
# point of ABCs
|
|
570
|
+
if inspect.isabstract(class_):
|
|
571
|
+
continue
|
|
572
|
+
# Same with protocols, but only direct children.
|
|
573
|
+
if Protocol in class_.__bases__ or Protocol_ext in class_.__bases__:
|
|
574
|
+
continue
|
|
575
|
+
# Exceptions are allowed to be subclassed, because exception
|
|
576
|
+
# subclassing isn't used to inherit behavior.
|
|
577
|
+
if issubclass(class_, BaseException):
|
|
578
|
+
continue
|
|
579
|
+
# These are classes that are conceptually abstract, but
|
|
580
|
+
# inspect.isabstract returns False for boring reasons.
|
|
581
|
+
if class_ is trio.abc.Instrument or class_ is trio.socket.SocketType:
|
|
582
|
+
continue
|
|
583
|
+
# ... insert other special cases here ...
|
|
584
|
+
|
|
585
|
+
# The `Path` class needs to support inheritance to allow `WindowsPath` and `PosixPath`.
|
|
586
|
+
if class_ is trio.Path:
|
|
587
|
+
continue
|
|
588
|
+
# don't care about the *Statistics classes
|
|
589
|
+
if name.endswith("Statistics"):
|
|
590
|
+
continue
|
|
591
|
+
|
|
592
|
+
assert class_is_final(class_)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# Plugin might not be running, especially if running from an installed version.
|
|
596
|
+
@pytest.mark.skipif(
|
|
597
|
+
not hasattr(attrs.field, "trio_modded"),
|
|
598
|
+
reason="Pytest plugin not installed.",
|
|
599
|
+
)
|
|
600
|
+
def test_pyright_recognizes_init_attributes() -> None:
|
|
601
|
+
"""Check whether we provide `alias` for all underscore prefixed attributes.
|
|
602
|
+
|
|
603
|
+
Attrs always sets the `alias` attribute on fields, so a pytest plugin is used
|
|
604
|
+
to monkeypatch `field()` to record whether an alias was defined in the metadata.
|
|
605
|
+
See `_trio_check_attrs_aliases`.
|
|
606
|
+
"""
|
|
607
|
+
for module in PUBLIC_MODULES:
|
|
608
|
+
for class_ in module.__dict__.values():
|
|
609
|
+
if not attrs.has(class_):
|
|
610
|
+
continue
|
|
611
|
+
if isinstance(class_, _util.NoPublicConstructor):
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
attributes = [
|
|
615
|
+
attr
|
|
616
|
+
for attr in attrs.fields(class_)
|
|
617
|
+
if attr.init
|
|
618
|
+
if attr.alias
|
|
619
|
+
not in (
|
|
620
|
+
attr.name,
|
|
621
|
+
# trio_original_args may not be present in autoattribs
|
|
622
|
+
attr.metadata.get("trio_original_args", {}).get("alias"),
|
|
623
|
+
)
|
|
624
|
+
]
|
|
625
|
+
|
|
626
|
+
assert attributes == [], class_
|