iris-pex-embedded-python 3.7.2b2__py3-none-any.whl → 4.0.0__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.
- iop/__init__.py +276 -18
- iop/__main__.py +3 -3
- iop/cli/__init__.py +1 -0
- iop/cli/formatting.py +45 -0
- iop/{_cli.py → cli/main.py} +238 -216
- iop/cli/parser.py +165 -0
- iop/cli/types.py +73 -0
- iop/cls/IOP/Common.cls +11 -2
- iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
- iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
- iop/cls/IOP/Utils.cls +814 -7
- iop/cls/IOP/Wrapper.cls +1 -1
- iop/components/__init__.py +16 -0
- iop/{_async_request.py → components/async_request.py} +7 -7
- iop/components/business_host.py +363 -0
- iop/components/business_operation.py +84 -0
- iop/{_business_process.py → components/business_process.py} +145 -125
- iop/components/business_service.py +106 -0
- iop/{_common.py → components/common.py} +160 -164
- iop/{_debugpy.py → components/debugpy.py} +11 -11
- iop/{_generator_request.py → components/generator_request.py} +4 -4
- iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
- iop/{_log_manager.py → components/log_manager.py} +1 -1
- iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
- iop/components/polling_business_service.py +48 -0
- iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
- iop/{_private_session_process.py → components/private_session_process.py} +2 -2
- iop/{_settings.py → components/settings.py} +23 -3
- iop/messages/__init__.py +7 -0
- iop/messages/base.py +61 -0
- iop/{_decorators.py → messages/decorators.py} +8 -2
- iop/messages/dispatch.py +351 -0
- iop/{_persistent_message.py → messages/persistent.py} +24 -19
- iop/{_serialization.py → messages/serialization.py} +34 -21
- iop/{_message_validator.py → messages/validation.py} +15 -13
- iop/migration/__init__.py +1 -0
- iop/migration/io.py +61 -0
- iop/migration/manifest.py +534 -0
- iop/migration/plans.py +80 -0
- iop/migration/utils.py +1126 -0
- iop/production/__init__.py +55 -0
- iop/production/actions.py +525 -0
- iop/production/common.py +103 -0
- iop/production/component.py +234 -0
- iop/production/declarations.py +190 -0
- iop/production/declarative.py +214 -0
- iop/production/diff.py +327 -0
- iop/production/import_.py +296 -0
- iop/production/inspection.py +92 -0
- iop/production/model.py +1143 -0
- iop/production/planning.py +678 -0
- iop/production/reconstruction.py +341 -0
- iop/production/rendering.py +610 -0
- iop/production/runtime.py +91 -0
- iop/production/source_inference.py +729 -0
- iop/production/types.py +546 -0
- iop/production/validation.py +452 -0
- iop/runtime/__init__.py +1 -0
- iop/runtime/director.py +415 -0
- iop/runtime/environment.py +59 -0
- iop/{_iris.py → runtime/iris.py} +2 -2
- iop/runtime/local.py +145 -0
- iop/{_director_protocol.py → runtime/protocol.py} +25 -8
- iop/runtime/remote/__init__.py +12 -0
- iop/runtime/remote/client.py +90 -0
- iop/runtime/remote/director.py +283 -0
- iop/runtime/remote/migration.py +57 -0
- iop/runtime/remote/settings.py +69 -0
- iop/runtime/remote/setup.py +72 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
- iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
- iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
- iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
- grongier/__init__.py +0 -0
- grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
- grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
- grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
- grongier/cls/Grongier/PEX/Common.cls +0 -10
- grongier/cls/Grongier/PEX/Director.cls +0 -10
- grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
- grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
- grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
- grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/Message.cls +0 -13
- grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
- grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
- grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
- grongier/cls/Grongier/PEX/Test.cls +0 -10
- grongier/cls/Grongier/PEX/Utils.cls +0 -10
- grongier/cls/Grongier/Service/WSGI.cls +0 -4
- grongier/pex/__init__.py +0 -75
- grongier/pex/__main__.py +0 -4
- grongier/pex/_business_host.py +0 -1
- grongier/pex/_cli.py +0 -4
- grongier/pex/_common.py +0 -1
- grongier/pex/_director.py +0 -1
- grongier/pex/_utils.py +0 -1
- grongier/pex/wsgi/handlers.py +0 -104
- iop/_business_host.py +0 -322
- iop/_business_operation.py +0 -77
- iop/_business_service.py +0 -67
- iop/_director.py +0 -333
- iop/_dispatch.py +0 -194
- iop/_local.py +0 -106
- iop/_message.py +0 -45
- iop/_polling_business_service.py +0 -6
- iop/_remote.py +0 -473
- iop/_utils.py +0 -912
- iop/cls/IOP/Service/WSGI.cls +0 -310
- iop/wsgi/handlers.py +0 -112
- iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
- iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
- iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SourceConnection:
|
|
14
|
+
target: str
|
|
15
|
+
source: str
|
|
16
|
+
detail: str = ""
|
|
17
|
+
interaction: str = "request"
|
|
18
|
+
target_reference: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_PYTHON_REQUEST_METHODS = {
|
|
22
|
+
"send_request_sync",
|
|
23
|
+
"send_request_async",
|
|
24
|
+
"send_request_async_ng",
|
|
25
|
+
"send_generator_request",
|
|
26
|
+
}
|
|
27
|
+
_PYTHON_MULTI_REQUEST_METHODS = {"send_multi_request_sync"}
|
|
28
|
+
_OBJECTSCRIPT_STRING = r'"(?:""|[^"])*"'
|
|
29
|
+
_OBJECTSCRIPT_CLASS_RE = re.compile(
|
|
30
|
+
r"(?im)^\s*Class\s+(?P<class>[A-Za-z%][\w%]*(?:\.[A-Za-z%][\w%]*)*)\b"
|
|
31
|
+
)
|
|
32
|
+
_OBJECTSCRIPT_EXTENDS_RE = re.compile(
|
|
33
|
+
r"(?ims)^\s*Class\s+[A-Za-z%][\w%]*(?:\.[A-Za-z%][\w%]*)*"
|
|
34
|
+
r"\s+Extends\s+(?P<super>\([^)]+\)|[^{\[]+)"
|
|
35
|
+
)
|
|
36
|
+
_OBJECTSCRIPT_SEND_RE = re.compile(
|
|
37
|
+
rf"(?i)\b(?P<method>SendRequest(?:Sync|Async|AsyncNG)?)\s*"
|
|
38
|
+
rf"\(\s*(?P<target>{_OBJECTSCRIPT_STRING}|\.\.[A-Za-z%][\w%]*)"
|
|
39
|
+
)
|
|
40
|
+
_OBJECTSCRIPT_PROPERTY_DEFAULT_RE = re.compile(
|
|
41
|
+
rf"(?im)^\s*Property\s+(?P<name>[A-Za-z%][\w%]*)\b"
|
|
42
|
+
rf"[^\n]*\bInitialExpression\s*=\s*(?P<value>{_OBJECTSCRIPT_STRING})"
|
|
43
|
+
)
|
|
44
|
+
_IGNORED_SOURCE_PARTS = {
|
|
45
|
+
".git",
|
|
46
|
+
".hg",
|
|
47
|
+
".mypy_cache",
|
|
48
|
+
".pytest_cache",
|
|
49
|
+
".ruff_cache",
|
|
50
|
+
".tox",
|
|
51
|
+
".venv",
|
|
52
|
+
"__pycache__",
|
|
53
|
+
"build",
|
|
54
|
+
"dist",
|
|
55
|
+
"node_modules",
|
|
56
|
+
"venv",
|
|
57
|
+
}
|
|
58
|
+
_LOW_PRIORITY_SOURCE_PARTS = {"docs", "test", "tests"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def infer_source_connections(
|
|
62
|
+
class_name: str,
|
|
63
|
+
host_settings: dict[str, Any],
|
|
64
|
+
*,
|
|
65
|
+
iop: bool = False,
|
|
66
|
+
root: Path | None = None,
|
|
67
|
+
) -> list[SourceConnection]:
|
|
68
|
+
"""Infer static request targets from local Python or ObjectScript source."""
|
|
69
|
+
normalized_class_name = str(class_name or "").strip()
|
|
70
|
+
if not normalized_class_name:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
source_roots = _source_roots(root or Path.cwd(), host_settings)
|
|
74
|
+
source = _find_objectscript_source(normalized_class_name, source_roots)
|
|
75
|
+
python_class_name = _iop_python_class_name(
|
|
76
|
+
normalized_class_name,
|
|
77
|
+
host_settings,
|
|
78
|
+
source,
|
|
79
|
+
iop=iop,
|
|
80
|
+
)
|
|
81
|
+
if python_class_name:
|
|
82
|
+
return _infer_python_connections(
|
|
83
|
+
python_class_name,
|
|
84
|
+
host_settings,
|
|
85
|
+
source_roots,
|
|
86
|
+
)
|
|
87
|
+
return _infer_objectscript_connections(
|
|
88
|
+
normalized_class_name,
|
|
89
|
+
host_settings,
|
|
90
|
+
source_roots[0],
|
|
91
|
+
source=source,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _infer_python_connections(
|
|
96
|
+
class_name: str,
|
|
97
|
+
host_settings: dict[str, Any],
|
|
98
|
+
roots: tuple[Path, ...],
|
|
99
|
+
) -> list[SourceConnection]:
|
|
100
|
+
candidates = []
|
|
101
|
+
for root_index, source_root in enumerate(roots):
|
|
102
|
+
candidates.extend(
|
|
103
|
+
(root_index, info)
|
|
104
|
+
for info in _python_source_index(_root_key(source_root))
|
|
105
|
+
if _python_class_matches(class_name, info.class_name, info.module_hint)
|
|
106
|
+
)
|
|
107
|
+
candidates.sort(
|
|
108
|
+
key=lambda item: (
|
|
109
|
+
item[0],
|
|
110
|
+
_source_rank(item[1].path),
|
|
111
|
+
item[1].path,
|
|
112
|
+
item[1].class_name,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
connections: list[SourceConnection] = []
|
|
117
|
+
for _root_index, candidate in candidates[:1]:
|
|
118
|
+
connections.extend(
|
|
119
|
+
_python_connections_for_host_settings(
|
|
120
|
+
candidate.connections,
|
|
121
|
+
host_settings,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
return _unique_connections(connections)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _infer_objectscript_connections(
|
|
128
|
+
class_name: str,
|
|
129
|
+
host_settings: dict[str, Any],
|
|
130
|
+
root: Path,
|
|
131
|
+
*,
|
|
132
|
+
source: _ObjectScriptSource | None = None,
|
|
133
|
+
) -> list[SourceConnection]:
|
|
134
|
+
source = source or _objectscript_source_index(_root_key(root)).get(class_name)
|
|
135
|
+
if source is None:
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
property_defaults = _objectscript_property_defaults(source.text)
|
|
139
|
+
connections: list[SourceConnection] = []
|
|
140
|
+
for match in _OBJECTSCRIPT_SEND_RE.finditer(source.text):
|
|
141
|
+
method_name = match.group("method")
|
|
142
|
+
interaction = _objectscript_method_interaction(method_name)
|
|
143
|
+
target_expression = match.group("target").strip()
|
|
144
|
+
if target_expression.startswith('"'):
|
|
145
|
+
targets = [_decode_objectscript_string(target_expression)]
|
|
146
|
+
detail = f"{method_name} literal"
|
|
147
|
+
else:
|
|
148
|
+
setting_name = target_expression[2:]
|
|
149
|
+
targets = (
|
|
150
|
+
_split_targets(host_settings.get(setting_name, ""))
|
|
151
|
+
or property_defaults.get(setting_name, [])
|
|
152
|
+
)
|
|
153
|
+
detail = f"{method_name} {target_expression}"
|
|
154
|
+
|
|
155
|
+
for target in targets:
|
|
156
|
+
connections.append(
|
|
157
|
+
SourceConnection(
|
|
158
|
+
target=target,
|
|
159
|
+
source="ObjectScript source",
|
|
160
|
+
detail=detail,
|
|
161
|
+
interaction=interaction,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return _unique_connections(connections)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _iop_python_class_name(
|
|
168
|
+
class_name: str,
|
|
169
|
+
host_settings: dict[str, Any],
|
|
170
|
+
source: _ObjectScriptSource | None,
|
|
171
|
+
*,
|
|
172
|
+
iop: bool,
|
|
173
|
+
) -> str:
|
|
174
|
+
defaults = _objectscript_property_defaults(source.text) if source else {}
|
|
175
|
+
if source is not None and not _objectscript_extends_iop(source.text):
|
|
176
|
+
return ""
|
|
177
|
+
module = (
|
|
178
|
+
_string_setting(host_settings, "%module")
|
|
179
|
+
or _first_default(defaults, "%module")
|
|
180
|
+
)
|
|
181
|
+
classname = (
|
|
182
|
+
_string_setting(host_settings, "%classname")
|
|
183
|
+
or _first_default(defaults, "%classname")
|
|
184
|
+
)
|
|
185
|
+
is_iop = (
|
|
186
|
+
iop
|
|
187
|
+
or (source is not None and _objectscript_extends_iop(source.text))
|
|
188
|
+
or bool(module and classname)
|
|
189
|
+
)
|
|
190
|
+
if not is_iop:
|
|
191
|
+
return ""
|
|
192
|
+
if not module or not classname:
|
|
193
|
+
if class_name.startswith("Python."):
|
|
194
|
+
return class_name
|
|
195
|
+
return ""
|
|
196
|
+
return f"Python.{module}.{classname}"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True)
|
|
200
|
+
class _PythonClassInfo:
|
|
201
|
+
class_name: str
|
|
202
|
+
module_hint: str
|
|
203
|
+
path: str
|
|
204
|
+
connections: tuple[SourceConnection, ...]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(frozen=True)
|
|
208
|
+
class _ObjectScriptSource:
|
|
209
|
+
text: str
|
|
210
|
+
path: str
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@lru_cache(maxsize=8)
|
|
214
|
+
def _python_source_index(root: str) -> tuple[_PythonClassInfo, ...]:
|
|
215
|
+
root_path = Path(root)
|
|
216
|
+
infos: list[_PythonClassInfo] = []
|
|
217
|
+
for path in _iter_source_files(root_path, "*.py"):
|
|
218
|
+
try:
|
|
219
|
+
text = path.read_text(encoding="utf-8")
|
|
220
|
+
except OSError:
|
|
221
|
+
continue
|
|
222
|
+
try:
|
|
223
|
+
tree = ast.parse(text)
|
|
224
|
+
except SyntaxError:
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
module_hint = _python_module_hint(root_path, path)
|
|
228
|
+
for node in tree.body:
|
|
229
|
+
if not isinstance(node, ast.ClassDef):
|
|
230
|
+
continue
|
|
231
|
+
connections = tuple(_python_class_connections(node))
|
|
232
|
+
infos.append(
|
|
233
|
+
_PythonClassInfo(
|
|
234
|
+
class_name=node.name,
|
|
235
|
+
module_hint=module_hint,
|
|
236
|
+
path=str(path),
|
|
237
|
+
connections=connections,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
return tuple(infos)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@lru_cache(maxsize=8)
|
|
244
|
+
def _objectscript_source_index(root: str) -> dict[str, _ObjectScriptSource]:
|
|
245
|
+
root_path = Path(root)
|
|
246
|
+
sources: dict[str, _ObjectScriptSource] = {}
|
|
247
|
+
candidates: list[tuple[tuple[int, str], str, _ObjectScriptSource]] = []
|
|
248
|
+
for path in _iter_source_files(root_path, "*.cls"):
|
|
249
|
+
try:
|
|
250
|
+
text = path.read_text(encoding="utf-8")
|
|
251
|
+
except UnicodeDecodeError:
|
|
252
|
+
try:
|
|
253
|
+
text = path.read_text()
|
|
254
|
+
except OSError:
|
|
255
|
+
continue
|
|
256
|
+
except OSError:
|
|
257
|
+
continue
|
|
258
|
+
match = _OBJECTSCRIPT_CLASS_RE.search(text)
|
|
259
|
+
if match is None:
|
|
260
|
+
continue
|
|
261
|
+
class_name = match.group("class")
|
|
262
|
+
source = _ObjectScriptSource(text=text, path=str(path))
|
|
263
|
+
candidates.append(((_source_rank(str(path)), str(path)), class_name, source))
|
|
264
|
+
|
|
265
|
+
for _, class_name, source in sorted(candidates, key=lambda item: item[0]):
|
|
266
|
+
sources.setdefault(class_name, source)
|
|
267
|
+
return sources
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _find_objectscript_source(
|
|
271
|
+
class_name: str,
|
|
272
|
+
roots: tuple[Path, ...],
|
|
273
|
+
) -> _ObjectScriptSource | None:
|
|
274
|
+
for root in roots:
|
|
275
|
+
source = _objectscript_source_index(_root_key(root)).get(class_name)
|
|
276
|
+
if source is not None:
|
|
277
|
+
return source
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _python_class_connections(class_node: ast.ClassDef) -> list[SourceConnection]:
|
|
282
|
+
string_values = _python_string_values(class_node)
|
|
283
|
+
connections: list[SourceConnection] = []
|
|
284
|
+
|
|
285
|
+
for node in ast.walk(class_node):
|
|
286
|
+
if not isinstance(node, ast.Call):
|
|
287
|
+
continue
|
|
288
|
+
call_name = _python_call_name(node.func)
|
|
289
|
+
if call_name in _PYTHON_REQUEST_METHODS:
|
|
290
|
+
for target, detail, interaction, target_reference in _python_request_targets(
|
|
291
|
+
node,
|
|
292
|
+
string_values,
|
|
293
|
+
call_name,
|
|
294
|
+
):
|
|
295
|
+
connections.append(
|
|
296
|
+
SourceConnection(
|
|
297
|
+
target=target,
|
|
298
|
+
source="Python source",
|
|
299
|
+
detail=detail,
|
|
300
|
+
interaction=interaction,
|
|
301
|
+
target_reference=target_reference,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
elif call_name in _PYTHON_MULTI_REQUEST_METHODS:
|
|
305
|
+
for target, detail, interaction, target_reference in _python_multi_request_targets(
|
|
306
|
+
node,
|
|
307
|
+
string_values,
|
|
308
|
+
call_name,
|
|
309
|
+
):
|
|
310
|
+
connections.append(
|
|
311
|
+
SourceConnection(
|
|
312
|
+
target=target,
|
|
313
|
+
source="Python source",
|
|
314
|
+
detail=detail,
|
|
315
|
+
interaction=interaction,
|
|
316
|
+
target_reference=target_reference,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
return _unique_connections(connections)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _python_string_values(class_node: ast.ClassDef) -> dict[str, list[str]]:
|
|
323
|
+
values: dict[str, list[str]] = {}
|
|
324
|
+
for node in class_node.body:
|
|
325
|
+
if isinstance(node, ast.Assign):
|
|
326
|
+
literal = _python_literal_string(node.value)
|
|
327
|
+
if literal is None:
|
|
328
|
+
continue
|
|
329
|
+
for target in node.targets:
|
|
330
|
+
_add_python_assignment_value(
|
|
331
|
+
values,
|
|
332
|
+
target,
|
|
333
|
+
literal,
|
|
334
|
+
class_body=True,
|
|
335
|
+
)
|
|
336
|
+
elif isinstance(node, ast.AnnAssign):
|
|
337
|
+
literal = _python_literal_string(node.value)
|
|
338
|
+
if literal is None:
|
|
339
|
+
literal = _python_setting_default(node.annotation)
|
|
340
|
+
if literal is None:
|
|
341
|
+
continue
|
|
342
|
+
_add_python_assignment_value(
|
|
343
|
+
values,
|
|
344
|
+
node.target,
|
|
345
|
+
literal,
|
|
346
|
+
class_body=True,
|
|
347
|
+
)
|
|
348
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
349
|
+
for child in ast.walk(node):
|
|
350
|
+
if isinstance(child, ast.Assign):
|
|
351
|
+
literal = _python_literal_string(child.value)
|
|
352
|
+
if literal is None:
|
|
353
|
+
continue
|
|
354
|
+
for target in child.targets:
|
|
355
|
+
_add_python_assignment_value(
|
|
356
|
+
values,
|
|
357
|
+
target,
|
|
358
|
+
literal,
|
|
359
|
+
class_body=False,
|
|
360
|
+
)
|
|
361
|
+
elif isinstance(child, ast.AnnAssign):
|
|
362
|
+
literal = _python_literal_string(child.value)
|
|
363
|
+
if literal is None:
|
|
364
|
+
continue
|
|
365
|
+
_add_python_assignment_value(
|
|
366
|
+
values,
|
|
367
|
+
child.target,
|
|
368
|
+
literal,
|
|
369
|
+
class_body=False,
|
|
370
|
+
)
|
|
371
|
+
return values
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _python_request_targets(
|
|
375
|
+
call: ast.Call,
|
|
376
|
+
string_values: dict[str, list[str]],
|
|
377
|
+
call_name: str,
|
|
378
|
+
) -> list[tuple[str, str, str, str]]:
|
|
379
|
+
target_node = _python_call_target_node(call)
|
|
380
|
+
if target_node is None:
|
|
381
|
+
return []
|
|
382
|
+
return _resolve_python_targets(
|
|
383
|
+
target_node,
|
|
384
|
+
string_values,
|
|
385
|
+
call_name,
|
|
386
|
+
_python_call_interaction(call_name),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _python_multi_request_targets(
|
|
391
|
+
call: ast.Call,
|
|
392
|
+
string_values: dict[str, list[str]],
|
|
393
|
+
call_name: str,
|
|
394
|
+
) -> list[tuple[str, str, str, str]]:
|
|
395
|
+
if not call.args:
|
|
396
|
+
return []
|
|
397
|
+
collection = call.args[0]
|
|
398
|
+
if not isinstance(collection, (ast.List, ast.Tuple)):
|
|
399
|
+
return []
|
|
400
|
+
|
|
401
|
+
targets: list[tuple[str, str, str, str]] = []
|
|
402
|
+
for item in collection.elts:
|
|
403
|
+
if not isinstance(item, ast.Tuple) or not item.elts:
|
|
404
|
+
continue
|
|
405
|
+
targets.extend(
|
|
406
|
+
_resolve_python_targets(
|
|
407
|
+
item.elts[0],
|
|
408
|
+
string_values,
|
|
409
|
+
call_name,
|
|
410
|
+
"sync",
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
return targets
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _resolve_python_targets(
|
|
417
|
+
node: ast.AST,
|
|
418
|
+
string_values: dict[str, list[str]],
|
|
419
|
+
call_name: str,
|
|
420
|
+
interaction: str,
|
|
421
|
+
) -> list[tuple[str, str, str, str]]:
|
|
422
|
+
literal = _python_literal_string(node)
|
|
423
|
+
if literal is not None:
|
|
424
|
+
return [(literal, f"{call_name} literal", interaction, "")]
|
|
425
|
+
|
|
426
|
+
name = _python_reference_name(node)
|
|
427
|
+
if name is None:
|
|
428
|
+
return []
|
|
429
|
+
detail = f"{call_name} {name}"
|
|
430
|
+
values = string_values.get(name, [])
|
|
431
|
+
if not values:
|
|
432
|
+
return [("", detail, interaction, name)]
|
|
433
|
+
return [
|
|
434
|
+
(value, detail, interaction, name)
|
|
435
|
+
for value in values
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _python_connections_for_host_settings(
|
|
440
|
+
connections: tuple[SourceConnection, ...],
|
|
441
|
+
host_settings: dict[str, Any],
|
|
442
|
+
) -> list[SourceConnection]:
|
|
443
|
+
resolved: list[SourceConnection] = []
|
|
444
|
+
for connection in connections:
|
|
445
|
+
setting_name = _python_reference_setting_name(connection.target_reference)
|
|
446
|
+
setting_targets = (
|
|
447
|
+
_split_targets(host_settings.get(setting_name, ""))
|
|
448
|
+
if setting_name
|
|
449
|
+
else []
|
|
450
|
+
)
|
|
451
|
+
if setting_targets:
|
|
452
|
+
for target in setting_targets:
|
|
453
|
+
resolved.append(
|
|
454
|
+
SourceConnection(
|
|
455
|
+
target=target,
|
|
456
|
+
source=connection.source,
|
|
457
|
+
detail=connection.detail,
|
|
458
|
+
interaction=connection.interaction,
|
|
459
|
+
target_reference=connection.target_reference,
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
continue
|
|
463
|
+
if connection.target:
|
|
464
|
+
resolved.append(connection)
|
|
465
|
+
return resolved
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _python_reference_setting_name(reference: str) -> str:
|
|
469
|
+
if not reference.startswith("self."):
|
|
470
|
+
return ""
|
|
471
|
+
return reference.removeprefix("self.")
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _python_call_target_node(call: ast.Call) -> ast.AST | None:
|
|
475
|
+
for keyword in call.keywords:
|
|
476
|
+
if keyword.arg == "target":
|
|
477
|
+
return keyword.value
|
|
478
|
+
if call.args:
|
|
479
|
+
return call.args[0]
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _python_assignment_name(node: ast.AST) -> str | None:
|
|
484
|
+
if isinstance(node, ast.Name):
|
|
485
|
+
return node.id
|
|
486
|
+
return _python_reference_name(node)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _python_reference_name(node: ast.AST) -> str | None:
|
|
490
|
+
if isinstance(node, ast.Name):
|
|
491
|
+
return node.id
|
|
492
|
+
if (
|
|
493
|
+
isinstance(node, ast.Attribute)
|
|
494
|
+
and isinstance(node.value, ast.Name)
|
|
495
|
+
and node.value.id == "self"
|
|
496
|
+
):
|
|
497
|
+
return f"self.{node.attr}"
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _python_literal_string(node: ast.AST | None) -> str | None:
|
|
502
|
+
if node is None:
|
|
503
|
+
return None
|
|
504
|
+
try:
|
|
505
|
+
value = ast.literal_eval(node)
|
|
506
|
+
except (ValueError, SyntaxError):
|
|
507
|
+
return None
|
|
508
|
+
if isinstance(value, str) and value:
|
|
509
|
+
return value
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _python_setting_default(annotation: ast.AST) -> str | None:
|
|
514
|
+
for node in ast.walk(annotation):
|
|
515
|
+
if not isinstance(node, ast.Call):
|
|
516
|
+
continue
|
|
517
|
+
if _python_call_name(node.func) != "Setting":
|
|
518
|
+
continue
|
|
519
|
+
for keyword in node.keywords:
|
|
520
|
+
if keyword.arg == "default":
|
|
521
|
+
return _python_literal_string(keyword.value)
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _python_call_name(node: ast.AST) -> str | None:
|
|
526
|
+
if isinstance(node, ast.Attribute):
|
|
527
|
+
return node.attr
|
|
528
|
+
if isinstance(node, ast.Name):
|
|
529
|
+
return node.id
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _python_call_interaction(call_name: str) -> str:
|
|
534
|
+
normalized = str(call_name or "").lower()
|
|
535
|
+
if "async" in normalized:
|
|
536
|
+
return "async"
|
|
537
|
+
if "sync" in normalized:
|
|
538
|
+
return "sync"
|
|
539
|
+
return "request"
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _objectscript_method_interaction(method_name: str) -> str:
|
|
543
|
+
normalized = str(method_name or "").lower()
|
|
544
|
+
if "async" in normalized:
|
|
545
|
+
return "async"
|
|
546
|
+
if "sync" in normalized:
|
|
547
|
+
return "sync"
|
|
548
|
+
return "request"
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _python_class_matches(
|
|
552
|
+
requested_class: str,
|
|
553
|
+
actual_class: str,
|
|
554
|
+
module_hint: str,
|
|
555
|
+
) -> bool:
|
|
556
|
+
parts = [part for part in requested_class.split(".") if part]
|
|
557
|
+
if parts and parts[0] == "Python":
|
|
558
|
+
parts = parts[1:]
|
|
559
|
+
if not parts:
|
|
560
|
+
return False
|
|
561
|
+
if actual_class != parts[-1]:
|
|
562
|
+
return False
|
|
563
|
+
if len(parts) == 1:
|
|
564
|
+
return True
|
|
565
|
+
requested_module = ".".join(parts[:-1])
|
|
566
|
+
return (
|
|
567
|
+
module_hint == requested_module
|
|
568
|
+
or module_hint.endswith(f".{requested_module}")
|
|
569
|
+
or requested_module.endswith(f".{module_hint}")
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _python_module_hint(root: Path, path: Path) -> str:
|
|
574
|
+
try:
|
|
575
|
+
relative = path.relative_to(root)
|
|
576
|
+
except ValueError:
|
|
577
|
+
relative = path
|
|
578
|
+
without_suffix = relative.with_suffix("")
|
|
579
|
+
return ".".join(part for part in without_suffix.parts if part != "__init__")
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _add_python_string_value(
|
|
583
|
+
values: dict[str, list[str]],
|
|
584
|
+
name: str | None,
|
|
585
|
+
value: str,
|
|
586
|
+
) -> None:
|
|
587
|
+
if name is None:
|
|
588
|
+
return
|
|
589
|
+
targets = values.setdefault(name, [])
|
|
590
|
+
if value not in targets:
|
|
591
|
+
targets.append(value)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _add_python_assignment_value(
|
|
595
|
+
values: dict[str, list[str]],
|
|
596
|
+
target: ast.AST,
|
|
597
|
+
value: str,
|
|
598
|
+
*,
|
|
599
|
+
class_body: bool,
|
|
600
|
+
) -> None:
|
|
601
|
+
name = _python_assignment_name(target)
|
|
602
|
+
_add_python_string_value(values, name, value)
|
|
603
|
+
if class_body and isinstance(target, ast.Name):
|
|
604
|
+
_add_python_string_value(values, f"self.{target.id}", value)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _objectscript_property_defaults(text: str) -> dict[str, list[str]]:
|
|
608
|
+
defaults: dict[str, list[str]] = {}
|
|
609
|
+
for match in _OBJECTSCRIPT_PROPERTY_DEFAULT_RE.finditer(text):
|
|
610
|
+
defaults[match.group("name")] = [
|
|
611
|
+
_decode_objectscript_string(match.group("value"))
|
|
612
|
+
]
|
|
613
|
+
return defaults
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _objectscript_extends_iop(text: str) -> bool:
|
|
617
|
+
match = _OBJECTSCRIPT_EXTENDS_RE.search(text)
|
|
618
|
+
if match is None:
|
|
619
|
+
return False
|
|
620
|
+
supers = {
|
|
621
|
+
value.strip()
|
|
622
|
+
for value in re.split(r"[\s(),]+", match.group("super"))
|
|
623
|
+
if value.strip()
|
|
624
|
+
}
|
|
625
|
+
return any(value == "IOP.Common" or value.startswith("IOP.") for value in supers)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _string_setting(settings: dict[str, Any], name: str) -> str:
|
|
629
|
+
value = settings.get(name)
|
|
630
|
+
if value is None:
|
|
631
|
+
return ""
|
|
632
|
+
return str(value).strip()
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _first_default(defaults: dict[str, list[str]], name: str) -> str:
|
|
636
|
+
values = defaults.get(name, [])
|
|
637
|
+
if not values:
|
|
638
|
+
return ""
|
|
639
|
+
return str(values[0]).strip()
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _decode_objectscript_string(value: str) -> str:
|
|
643
|
+
return value[1:-1].replace('""', '"')
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _split_targets(value: Any) -> list[str]:
|
|
647
|
+
if value is None:
|
|
648
|
+
return []
|
|
649
|
+
return [
|
|
650
|
+
target.strip()
|
|
651
|
+
for target in str(value).split(",")
|
|
652
|
+
if target and target.strip()
|
|
653
|
+
]
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _source_roots(root: Path, host_settings: dict[str, Any]) -> tuple[Path, ...]:
|
|
657
|
+
candidates: list[Path] = []
|
|
658
|
+
for classpath in _classpath_parts(host_settings.get("%classpaths", "")):
|
|
659
|
+
path = Path(classpath).expanduser()
|
|
660
|
+
if not path.is_absolute():
|
|
661
|
+
path = root / path
|
|
662
|
+
candidates.append(path)
|
|
663
|
+
candidates.append(root)
|
|
664
|
+
|
|
665
|
+
roots: list[Path] = []
|
|
666
|
+
seen: set[str] = set()
|
|
667
|
+
for path in candidates:
|
|
668
|
+
key = _root_key(path)
|
|
669
|
+
if key in seen:
|
|
670
|
+
continue
|
|
671
|
+
seen.add(key)
|
|
672
|
+
roots.append(path)
|
|
673
|
+
return tuple(roots)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _classpath_parts(value: Any) -> list[str]:
|
|
677
|
+
text = str(value or "").strip()
|
|
678
|
+
if not text:
|
|
679
|
+
return []
|
|
680
|
+
separator = "|" if "|" in text else os.pathsep
|
|
681
|
+
return [part.strip() for part in text.split(separator) if part.strip()]
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _iter_source_files(root: Path, pattern: str):
|
|
685
|
+
try:
|
|
686
|
+
paths = root.rglob(pattern)
|
|
687
|
+
except OSError:
|
|
688
|
+
return
|
|
689
|
+
for path in paths:
|
|
690
|
+
if _is_ignored_path(path):
|
|
691
|
+
continue
|
|
692
|
+
if path.is_file():
|
|
693
|
+
yield path
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _is_ignored_path(path: Path) -> bool:
|
|
697
|
+
return any(part in _IGNORED_SOURCE_PARTS for part in path.parts)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _source_rank(path: str) -> int:
|
|
701
|
+
parts = set(Path(path).parts)
|
|
702
|
+
return 1 if parts & _LOW_PRIORITY_SOURCE_PARTS else 0
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _root_key(root: Path) -> str:
|
|
706
|
+
try:
|
|
707
|
+
return str(root.resolve())
|
|
708
|
+
except OSError:
|
|
709
|
+
return str(root.absolute())
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _unique_connections(connections: list[SourceConnection]) -> list[SourceConnection]:
|
|
713
|
+
unique: list[SourceConnection] = []
|
|
714
|
+
seen: set[tuple[str, str, str, str, str]] = set()
|
|
715
|
+
for connection in connections:
|
|
716
|
+
if not connection.target and not connection.target_reference:
|
|
717
|
+
continue
|
|
718
|
+
key = (
|
|
719
|
+
connection.target,
|
|
720
|
+
connection.source,
|
|
721
|
+
connection.detail,
|
|
722
|
+
connection.interaction,
|
|
723
|
+
connection.target_reference,
|
|
724
|
+
)
|
|
725
|
+
if key in seen:
|
|
726
|
+
continue
|
|
727
|
+
seen.add(key)
|
|
728
|
+
unique.append(connection)
|
|
729
|
+
return unique
|