python-tty 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- python_tty/__init__.py +56 -12
- python_tty/executor/__init__.py +21 -2
- python_tty/testing/__init__.py +12 -0
- python_tty/testing/capture.py +72 -0
- python_tty/testing/decorators.py +67 -0
- python_tty/testing/discovery.py +263 -0
- python_tty/testing/harness.py +295 -0
- python_tty/testing/results.py +17 -0
- python_tty-0.2.2.dist-info/METADATA +290 -0
- {python_tty-0.2.0.dist-info → python_tty-0.2.2.dist-info}/RECORD +14 -8
- {python_tty-0.2.0.dist-info → python_tty-0.2.2.dist-info}/WHEEL +1 -1
- python_tty-0.2.0.dist-info/METADATA +0 -215
- {python_tty-0.2.0.dist-info → python_tty-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {python_tty-0.2.0.dist-info → python_tty-0.2.2.dist-info}/licenses/NOTICE +0 -0
- {python_tty-0.2.0.dist-info → python_tty-0.2.2.dist-info}/top_level.txt +0 -0
python_tty/__init__.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from python_tty.config import Config
|
|
5
|
+
from python_tty.console_factory import ConsoleFactory
|
|
6
|
+
from python_tty.runtime.events import (
|
|
7
|
+
EventBase,
|
|
8
|
+
RuntimeEvent,
|
|
9
|
+
RuntimeEventKind,
|
|
10
|
+
UIEvent,
|
|
11
|
+
UIEventLevel,
|
|
12
|
+
UIEventListener,
|
|
13
|
+
UIEventSpeaker,
|
|
14
|
+
)
|
|
15
|
+
from python_tty.runtime.router import proxy_print
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
15
18
|
"UIEvent",
|
|
@@ -23,3 +26,44 @@ __all__ = [
|
|
|
23
26
|
"Config",
|
|
24
27
|
"proxy_print",
|
|
25
28
|
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __getattr__(name):
|
|
32
|
+
if name == "Config":
|
|
33
|
+
from python_tty.config import Config
|
|
34
|
+
return Config
|
|
35
|
+
if name == "ConsoleFactory":
|
|
36
|
+
from python_tty.console_factory import ConsoleFactory
|
|
37
|
+
return ConsoleFactory
|
|
38
|
+
if name == "proxy_print":
|
|
39
|
+
from python_tty.runtime.router import proxy_print
|
|
40
|
+
return proxy_print
|
|
41
|
+
if name in {
|
|
42
|
+
"UIEvent",
|
|
43
|
+
"UIEventLevel",
|
|
44
|
+
"EventBase",
|
|
45
|
+
"RuntimeEvent",
|
|
46
|
+
"RuntimeEventKind",
|
|
47
|
+
"UIEventListener",
|
|
48
|
+
"UIEventSpeaker",
|
|
49
|
+
}:
|
|
50
|
+
from python_tty.runtime.events import (
|
|
51
|
+
EventBase,
|
|
52
|
+
RuntimeEvent,
|
|
53
|
+
RuntimeEventKind,
|
|
54
|
+
UIEvent,
|
|
55
|
+
UIEventLevel,
|
|
56
|
+
UIEventListener,
|
|
57
|
+
UIEventSpeaker,
|
|
58
|
+
)
|
|
59
|
+
mapping = {
|
|
60
|
+
"UIEvent": UIEvent,
|
|
61
|
+
"UIEventLevel": UIEventLevel,
|
|
62
|
+
"EventBase": EventBase,
|
|
63
|
+
"RuntimeEvent": RuntimeEvent,
|
|
64
|
+
"RuntimeEventKind": RuntimeEventKind,
|
|
65
|
+
"UIEventListener": UIEventListener,
|
|
66
|
+
"UIEventSpeaker": UIEventSpeaker,
|
|
67
|
+
}
|
|
68
|
+
return mapping[name]
|
|
69
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
python_tty/executor/__init__.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from python_tty.executor.executor import CommandExecutor
|
|
5
|
+
from python_tty.executor.models import Invocation, RunState, RunStatus
|
|
3
6
|
|
|
4
7
|
__all__ = [
|
|
5
8
|
"CommandExecutor",
|
|
@@ -7,4 +10,20 @@ __all__ = [
|
|
|
7
10
|
"RunState",
|
|
8
11
|
"RunStatus",
|
|
9
12
|
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name):
|
|
16
|
+
if name == "CommandExecutor":
|
|
17
|
+
from python_tty.executor.executor import CommandExecutor
|
|
18
|
+
return CommandExecutor
|
|
19
|
+
if name == "Invocation":
|
|
20
|
+
from python_tty.executor.models import Invocation
|
|
21
|
+
return Invocation
|
|
22
|
+
if name == "RunState":
|
|
23
|
+
from python_tty.executor.models import RunState
|
|
24
|
+
return RunState
|
|
25
|
+
if name == "RunStatus":
|
|
26
|
+
from python_tty.executor.models import RunStatus
|
|
27
|
+
return RunStatus
|
|
28
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
10
29
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from python_tty.testing.decorators import tty_testable
|
|
2
|
+
from python_tty.testing.discovery import discover_tests
|
|
3
|
+
from python_tty.testing.harness import CommandHarness, InvocationHarness
|
|
4
|
+
from python_tty.testing.results import TestRunResult
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"tty_testable",
|
|
8
|
+
"discover_tests",
|
|
9
|
+
"CommandHarness",
|
|
10
|
+
"InvocationHarness",
|
|
11
|
+
"TestRunResult",
|
|
12
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from typing import Dict, Iterable, List, Optional
|
|
3
|
+
|
|
4
|
+
from python_tty.runtime.events import RuntimeEvent, UIEvent
|
|
5
|
+
from python_tty.runtime.router import BaseRouter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CollectingRouter(BaseRouter):
|
|
9
|
+
"""In-memory router used by test harnesses to capture UI events."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.events: List[object] = []
|
|
13
|
+
|
|
14
|
+
def emit(self, event):
|
|
15
|
+
self.events.append(event)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@contextmanager
|
|
19
|
+
def capture_executor_events(executor, sink: List[object]):
|
|
20
|
+
"""Temporarily intercept executor.publish_event and append events to sink."""
|
|
21
|
+
|
|
22
|
+
original = executor.publish_event
|
|
23
|
+
|
|
24
|
+
def _publish(run_id, event):
|
|
25
|
+
sink.append(event)
|
|
26
|
+
return original(run_id, event)
|
|
27
|
+
|
|
28
|
+
executor.publish_event = _publish
|
|
29
|
+
try:
|
|
30
|
+
yield sink
|
|
31
|
+
finally:
|
|
32
|
+
executor.publish_event = original
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_output_records(runtime_events: Iterable[object], ui_events: Optional[Iterable[object]] = None):
|
|
36
|
+
"""Normalize runtime/UI events into a lightweight output record list."""
|
|
37
|
+
|
|
38
|
+
outputs: List[Dict[str, object]] = []
|
|
39
|
+
for event in runtime_events:
|
|
40
|
+
outputs.append(_event_to_record(event, channel="runtime"))
|
|
41
|
+
for event in ui_events or []:
|
|
42
|
+
outputs.append(_event_to_record(event, channel="ui"))
|
|
43
|
+
return outputs
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _event_to_record(event: object, channel: str):
|
|
47
|
+
if isinstance(event, RuntimeEvent):
|
|
48
|
+
kind = getattr(event.kind, "value", event.kind)
|
|
49
|
+
else:
|
|
50
|
+
kind = None
|
|
51
|
+
if isinstance(event, (RuntimeEvent, UIEvent)):
|
|
52
|
+
level = getattr(getattr(event, "level", None), "value", getattr(event, "level", None))
|
|
53
|
+
return {
|
|
54
|
+
"channel": channel,
|
|
55
|
+
"kind": kind,
|
|
56
|
+
"message": getattr(event, "msg", None),
|
|
57
|
+
"level": level,
|
|
58
|
+
"source": getattr(event, "source", None),
|
|
59
|
+
"event_type": getattr(event, "event_type", None),
|
|
60
|
+
"run_id": getattr(event, "run_id", None),
|
|
61
|
+
"seq": getattr(event, "seq", None),
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
"channel": channel,
|
|
65
|
+
"kind": kind,
|
|
66
|
+
"message": str(event),
|
|
67
|
+
"level": None,
|
|
68
|
+
"source": None,
|
|
69
|
+
"event_type": None,
|
|
70
|
+
"run_id": None,
|
|
71
|
+
"seq": None,
|
|
72
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from python_tty.commands import BaseCommands
|
|
5
|
+
from python_tty.commands.registry import CommandInfo
|
|
6
|
+
from python_tty.exceptions.console_exception import ConsoleInitException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
TESTABLE_ATTR = "__tty_testable__"
|
|
10
|
+
TEST_META_ATTR = "__tty_test_meta__"
|
|
11
|
+
TEST_SCOPE_ATTR = "__tty_test_scope__"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def tty_testable(target=None, **meta):
|
|
15
|
+
"""Mark command classes/methods as discoverable by python_tty.testing."""
|
|
16
|
+
|
|
17
|
+
if target is None:
|
|
18
|
+
return lambda real_target: tty_testable(real_target, **meta)
|
|
19
|
+
|
|
20
|
+
if inspect.isclass(target):
|
|
21
|
+
if not issubclass(target, BaseCommands):
|
|
22
|
+
raise ConsoleInitException(
|
|
23
|
+
"@tty_testable class target must inherit BaseCommands"
|
|
24
|
+
)
|
|
25
|
+
_mark_testable(target, scope="class", meta=meta)
|
|
26
|
+
return target
|
|
27
|
+
|
|
28
|
+
if callable(target):
|
|
29
|
+
func = _unwrap_callable(target)
|
|
30
|
+
if not _is_registered_command_method(func):
|
|
31
|
+
raise ConsoleInitException(
|
|
32
|
+
"@tty_testable method target must be a registered command method "
|
|
33
|
+
"(decorate with @register_command first)"
|
|
34
|
+
)
|
|
35
|
+
_mark_testable(func, scope="method", meta=meta)
|
|
36
|
+
return target
|
|
37
|
+
|
|
38
|
+
raise ConsoleInitException(
|
|
39
|
+
"@tty_testable target must be a BaseCommands subclass or registered command method"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _unwrap_callable(value):
|
|
44
|
+
return getattr(value, "__func__", value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_registered_command_method(func) -> bool:
|
|
48
|
+
if not callable(func):
|
|
49
|
+
return False
|
|
50
|
+
info = getattr(func, "info", None)
|
|
51
|
+
return isinstance(info, CommandInfo)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _mark_testable(target, scope: str, meta: Dict[str, Any]):
|
|
55
|
+
existing_meta = _read_meta(target)
|
|
56
|
+
merged_meta = dict(existing_meta)
|
|
57
|
+
merged_meta.update(meta)
|
|
58
|
+
setattr(target, TESTABLE_ATTR, True)
|
|
59
|
+
setattr(target, TEST_META_ATTR, merged_meta)
|
|
60
|
+
setattr(target, TEST_SCOPE_ATTR, scope)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _read_meta(target):
|
|
64
|
+
meta = getattr(target, TEST_META_ATTR, None)
|
|
65
|
+
if isinstance(meta, dict):
|
|
66
|
+
return meta
|
|
67
|
+
return {}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import types
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
|
5
|
+
|
|
6
|
+
from python_tty.commands import BaseCommands
|
|
7
|
+
from python_tty.commands.registry import COMMAND_REGISTRY
|
|
8
|
+
from python_tty.consoles.registry import REGISTRY
|
|
9
|
+
from python_tty.testing.decorators import TEST_META_ATTR, TESTABLE_ATTR
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DiscoveredTestCase:
|
|
14
|
+
"""Metadata for a single command test case discovered from command classes."""
|
|
15
|
+
|
|
16
|
+
command_id: str
|
|
17
|
+
command_name: str
|
|
18
|
+
console_name: Optional[str]
|
|
19
|
+
console_cls: Optional[type]
|
|
20
|
+
commands_cls: type
|
|
21
|
+
method_name: str
|
|
22
|
+
method: object
|
|
23
|
+
class_marked: bool
|
|
24
|
+
method_marked: bool
|
|
25
|
+
test_meta: Dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DiscoveredTestSuite:
|
|
29
|
+
"""Stable suite wrapper returned by discover_tests()."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, cases: Sequence[DiscoveredTestCase]):
|
|
32
|
+
ordered = sorted(cases, key=lambda c: c.command_id)
|
|
33
|
+
deduped: Dict[str, DiscoveredTestCase] = {}
|
|
34
|
+
for case in ordered:
|
|
35
|
+
deduped[case.command_id] = case
|
|
36
|
+
self._cases: Tuple[DiscoveredTestCase, ...] = tuple(deduped.values())
|
|
37
|
+
self._cases_by_id: Dict[str, DiscoveredTestCase] = dict(deduped)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def cases(self) -> Tuple[DiscoveredTestCase, ...]:
|
|
41
|
+
return self._cases
|
|
42
|
+
|
|
43
|
+
def list_cases(self) -> List[DiscoveredTestCase]:
|
|
44
|
+
return list(self._cases)
|
|
45
|
+
|
|
46
|
+
def get_case(self, command_id: str) -> Optional[DiscoveredTestCase]:
|
|
47
|
+
return self._cases_by_id.get(command_id)
|
|
48
|
+
|
|
49
|
+
def ids(self) -> List[str]:
|
|
50
|
+
return [case.command_id for case in self._cases]
|
|
51
|
+
|
|
52
|
+
def __len__(self):
|
|
53
|
+
return len(self._cases)
|
|
54
|
+
|
|
55
|
+
def __iter__(self):
|
|
56
|
+
return iter(self._cases)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def discover_tests(*targets, command_ids: Optional[Iterable[str]] = None):
|
|
60
|
+
"""Explicitly discover command test cases from modules/classes/methods/ids."""
|
|
61
|
+
|
|
62
|
+
expanded_targets = _expand_targets(targets)
|
|
63
|
+
explicit_command_ids: Set[str] = set(command_ids or [])
|
|
64
|
+
commands_classes: List[type] = []
|
|
65
|
+
explicit_classes: Set[type] = set()
|
|
66
|
+
explicit_methods: Set[object] = set()
|
|
67
|
+
|
|
68
|
+
for target in expanded_targets:
|
|
69
|
+
if isinstance(target, str):
|
|
70
|
+
explicit_command_ids.add(target)
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if inspect.isclass(target) and issubclass(target, BaseCommands):
|
|
74
|
+
commands_classes.append(target)
|
|
75
|
+
explicit_classes.add(target)
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if isinstance(target, types.ModuleType):
|
|
79
|
+
commands_classes.extend(_collect_commands_classes_from_module(target))
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if isinstance(target, BaseCommands):
|
|
83
|
+
commands_cls = target.__class__
|
|
84
|
+
commands_classes.append(commands_cls)
|
|
85
|
+
explicit_classes.add(commands_cls)
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
func = _unwrap_callable(target)
|
|
89
|
+
if callable(func) and hasattr(func, "info"):
|
|
90
|
+
explicit_methods.add(func)
|
|
91
|
+
owner_cls = _resolve_owner_commands_cls(func)
|
|
92
|
+
if owner_cls is not None:
|
|
93
|
+
commands_classes.append(owner_cls)
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
raise ValueError(f"Unsupported discover_tests target: {target!r}")
|
|
97
|
+
|
|
98
|
+
if explicit_command_ids:
|
|
99
|
+
commands_classes.extend(_collect_classes_by_command_ids(explicit_command_ids))
|
|
100
|
+
|
|
101
|
+
unique_classes = _unique_ordered(commands_classes)
|
|
102
|
+
cases: List[DiscoveredTestCase] = []
|
|
103
|
+
|
|
104
|
+
for commands_cls in unique_classes:
|
|
105
|
+
class_marked = bool(getattr(commands_cls, TESTABLE_ATTR, False))
|
|
106
|
+
class_meta = _read_meta(commands_cls)
|
|
107
|
+
is_explicit_class = commands_cls in explicit_classes
|
|
108
|
+
console_bindings = _resolve_console_bindings(commands_cls)
|
|
109
|
+
command_defs = COMMAND_REGISTRY.collect_from_commands_cls(commands_cls)
|
|
110
|
+
|
|
111
|
+
for command_def in command_defs:
|
|
112
|
+
method = _resolve_method(commands_cls, command_def.func)
|
|
113
|
+
method_marked = bool(getattr(method, TESTABLE_ATTR, False))
|
|
114
|
+
method_meta = _read_meta(method)
|
|
115
|
+
method_selected = _is_method_selected(method, explicit_methods)
|
|
116
|
+
|
|
117
|
+
for console_name, console_cls in console_bindings:
|
|
118
|
+
command_id = _build_command_id(console_name, command_def.func_name)
|
|
119
|
+
by_id = command_id in explicit_command_ids
|
|
120
|
+
include = is_explicit_class or class_marked or method_marked or method_selected or by_id
|
|
121
|
+
if not include:
|
|
122
|
+
continue
|
|
123
|
+
case_meta = dict(class_meta)
|
|
124
|
+
case_meta.update(method_meta)
|
|
125
|
+
cases.append(
|
|
126
|
+
DiscoveredTestCase(
|
|
127
|
+
command_id=command_id,
|
|
128
|
+
command_name=command_def.func_name,
|
|
129
|
+
console_name=console_name,
|
|
130
|
+
console_cls=console_cls,
|
|
131
|
+
commands_cls=commands_cls,
|
|
132
|
+
method_name=method.__name__,
|
|
133
|
+
method=method,
|
|
134
|
+
class_marked=class_marked,
|
|
135
|
+
method_marked=method_marked,
|
|
136
|
+
test_meta=case_meta,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return DiscoveredTestSuite(cases)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _expand_targets(targets) -> List[object]:
|
|
144
|
+
expanded: List[object] = []
|
|
145
|
+
for target in targets:
|
|
146
|
+
if target is None:
|
|
147
|
+
continue
|
|
148
|
+
if isinstance(target, (list, tuple, set)):
|
|
149
|
+
expanded.extend(_expand_targets(tuple(target)))
|
|
150
|
+
continue
|
|
151
|
+
expanded.append(target)
|
|
152
|
+
return expanded
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _collect_commands_classes_from_module(module: types.ModuleType) -> List[type]:
|
|
156
|
+
classes: List[type] = []
|
|
157
|
+
for _, cls in inspect.getmembers(module, inspect.isclass):
|
|
158
|
+
if not issubclass(cls, BaseCommands) or cls is BaseCommands:
|
|
159
|
+
continue
|
|
160
|
+
if cls.__module__ != module.__name__:
|
|
161
|
+
continue
|
|
162
|
+
classes.append(cls)
|
|
163
|
+
return classes
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _collect_classes_by_command_ids(command_ids: Iterable[str]) -> List[type]:
|
|
167
|
+
classes: List[type] = []
|
|
168
|
+
for command_id in command_ids:
|
|
169
|
+
console_name = _extract_console_name(command_id)
|
|
170
|
+
if not console_name:
|
|
171
|
+
continue
|
|
172
|
+
for registry_name, console_cls, _ in REGISTRY.iter_consoles():
|
|
173
|
+
if registry_name != console_name:
|
|
174
|
+
continue
|
|
175
|
+
commands_cls = COMMAND_REGISTRY.get_commands_cls(console_cls)
|
|
176
|
+
if commands_cls is not None:
|
|
177
|
+
classes.append(commands_cls)
|
|
178
|
+
return classes
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _resolve_console_bindings(commands_cls) -> List[Tuple[str, Optional[type]]]:
|
|
182
|
+
bindings: List[Tuple[str, Optional[type]]] = []
|
|
183
|
+
for console_name, console_cls, _ in REGISTRY.iter_consoles():
|
|
184
|
+
resolved = COMMAND_REGISTRY.get_commands_cls(console_cls)
|
|
185
|
+
if resolved is commands_cls:
|
|
186
|
+
bindings.append((console_name, console_cls))
|
|
187
|
+
if bindings:
|
|
188
|
+
return bindings
|
|
189
|
+
fallback = getattr(commands_cls, "console_name", None)
|
|
190
|
+
if not fallback:
|
|
191
|
+
fallback = commands_cls.__name__.lower()
|
|
192
|
+
return [(fallback, None)]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _extract_console_name(command_id: str) -> Optional[str]:
|
|
196
|
+
if not isinstance(command_id, str):
|
|
197
|
+
return None
|
|
198
|
+
if not command_id.startswith("cmd:"):
|
|
199
|
+
return None
|
|
200
|
+
parts = command_id.split(":", 2)
|
|
201
|
+
if len(parts) != 3:
|
|
202
|
+
return None
|
|
203
|
+
return parts[1] or None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _resolve_owner_commands_cls(func) -> Optional[type]:
|
|
207
|
+
module = inspect.getmodule(func)
|
|
208
|
+
if module is None:
|
|
209
|
+
return None
|
|
210
|
+
qualname = getattr(func, "__qualname__", "")
|
|
211
|
+
qualname = qualname.split(".<locals>", 1)[0]
|
|
212
|
+
if "." not in qualname:
|
|
213
|
+
return None
|
|
214
|
+
cls_path = qualname.rsplit(".", 1)[0]
|
|
215
|
+
owner = module
|
|
216
|
+
for part in cls_path.split("."):
|
|
217
|
+
owner = getattr(owner, part, None)
|
|
218
|
+
if owner is None:
|
|
219
|
+
return None
|
|
220
|
+
if inspect.isclass(owner) and issubclass(owner, BaseCommands):
|
|
221
|
+
return owner
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _resolve_method(commands_cls, fallback):
|
|
226
|
+
name = getattr(fallback, "__name__", None)
|
|
227
|
+
if name and hasattr(commands_cls, name):
|
|
228
|
+
return getattr(commands_cls, name)
|
|
229
|
+
return fallback
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_method_selected(method, selected_methods: Set[object]) -> bool:
|
|
233
|
+
method = _unwrap_callable(method)
|
|
234
|
+
for selected in selected_methods:
|
|
235
|
+
if _unwrap_callable(selected) is method:
|
|
236
|
+
return True
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _unwrap_callable(value):
|
|
241
|
+
return getattr(value, "__func__", value)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _read_meta(target) -> Dict[str, Any]:
|
|
245
|
+
meta = getattr(target, TEST_META_ATTR, None)
|
|
246
|
+
if isinstance(meta, dict):
|
|
247
|
+
return dict(meta)
|
|
248
|
+
return {}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _unique_ordered(values: Iterable[type]) -> List[type]:
|
|
252
|
+
seen: Set[type] = set()
|
|
253
|
+
ordered: List[type] = []
|
|
254
|
+
for value in values:
|
|
255
|
+
if value in seen:
|
|
256
|
+
continue
|
|
257
|
+
seen.add(value)
|
|
258
|
+
ordered.append(value)
|
|
259
|
+
return ordered
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _build_command_id(console_name: str, command_name: str):
|
|
263
|
+
return f"cmd:{console_name}:{command_name}"
|