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.
Files changed (119) hide show
  1. iop/__init__.py +276 -18
  2. iop/__main__.py +3 -3
  3. iop/cli/__init__.py +1 -0
  4. iop/cli/formatting.py +45 -0
  5. iop/{_cli.py → cli/main.py} +238 -216
  6. iop/cli/parser.py +165 -0
  7. iop/cli/types.py +73 -0
  8. iop/cls/IOP/Common.cls +11 -2
  9. iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
  10. iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
  11. iop/cls/IOP/Utils.cls +814 -7
  12. iop/cls/IOP/Wrapper.cls +1 -1
  13. iop/components/__init__.py +16 -0
  14. iop/{_async_request.py → components/async_request.py} +7 -7
  15. iop/components/business_host.py +363 -0
  16. iop/components/business_operation.py +84 -0
  17. iop/{_business_process.py → components/business_process.py} +145 -125
  18. iop/components/business_service.py +106 -0
  19. iop/{_common.py → components/common.py} +160 -164
  20. iop/{_debugpy.py → components/debugpy.py} +11 -11
  21. iop/{_generator_request.py → components/generator_request.py} +4 -4
  22. iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
  23. iop/{_log_manager.py → components/log_manager.py} +1 -1
  24. iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
  25. iop/components/polling_business_service.py +48 -0
  26. iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
  27. iop/{_private_session_process.py → components/private_session_process.py} +2 -2
  28. iop/{_settings.py → components/settings.py} +23 -3
  29. iop/messages/__init__.py +7 -0
  30. iop/messages/base.py +61 -0
  31. iop/{_decorators.py → messages/decorators.py} +8 -2
  32. iop/messages/dispatch.py +351 -0
  33. iop/{_persistent_message.py → messages/persistent.py} +24 -19
  34. iop/{_serialization.py → messages/serialization.py} +34 -21
  35. iop/{_message_validator.py → messages/validation.py} +15 -13
  36. iop/migration/__init__.py +1 -0
  37. iop/migration/io.py +61 -0
  38. iop/migration/manifest.py +534 -0
  39. iop/migration/plans.py +80 -0
  40. iop/migration/utils.py +1126 -0
  41. iop/production/__init__.py +55 -0
  42. iop/production/actions.py +525 -0
  43. iop/production/common.py +103 -0
  44. iop/production/component.py +234 -0
  45. iop/production/declarations.py +190 -0
  46. iop/production/declarative.py +214 -0
  47. iop/production/diff.py +327 -0
  48. iop/production/import_.py +296 -0
  49. iop/production/inspection.py +92 -0
  50. iop/production/model.py +1143 -0
  51. iop/production/planning.py +678 -0
  52. iop/production/reconstruction.py +341 -0
  53. iop/production/rendering.py +610 -0
  54. iop/production/runtime.py +91 -0
  55. iop/production/source_inference.py +729 -0
  56. iop/production/types.py +546 -0
  57. iop/production/validation.py +452 -0
  58. iop/runtime/__init__.py +1 -0
  59. iop/runtime/director.py +415 -0
  60. iop/runtime/environment.py +59 -0
  61. iop/{_iris.py → runtime/iris.py} +2 -2
  62. iop/runtime/local.py +145 -0
  63. iop/{_director_protocol.py → runtime/protocol.py} +25 -8
  64. iop/runtime/remote/__init__.py +12 -0
  65. iop/runtime/remote/client.py +90 -0
  66. iop/runtime/remote/director.py +283 -0
  67. iop/runtime/remote/migration.py +57 -0
  68. iop/runtime/remote/settings.py +69 -0
  69. iop/runtime/remote/setup.py +72 -0
  70. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
  71. iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
  72. iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
  73. iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
  74. grongier/__init__.py +0 -0
  75. grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
  76. grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
  77. grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
  78. grongier/cls/Grongier/PEX/Common.cls +0 -10
  79. grongier/cls/Grongier/PEX/Director.cls +0 -10
  80. grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
  81. grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
  82. grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
  83. grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
  84. grongier/cls/Grongier/PEX/Message.cls +0 -13
  85. grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
  86. grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
  87. grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
  88. grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
  89. grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
  90. grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
  91. grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
  92. grongier/cls/Grongier/PEX/Test.cls +0 -10
  93. grongier/cls/Grongier/PEX/Utils.cls +0 -10
  94. grongier/cls/Grongier/Service/WSGI.cls +0 -4
  95. grongier/pex/__init__.py +0 -75
  96. grongier/pex/__main__.py +0 -4
  97. grongier/pex/_business_host.py +0 -1
  98. grongier/pex/_cli.py +0 -4
  99. grongier/pex/_common.py +0 -1
  100. grongier/pex/_director.py +0 -1
  101. grongier/pex/_utils.py +0 -1
  102. grongier/pex/wsgi/handlers.py +0 -104
  103. iop/_business_host.py +0 -322
  104. iop/_business_operation.py +0 -77
  105. iop/_business_service.py +0 -67
  106. iop/_director.py +0 -333
  107. iop/_dispatch.py +0 -194
  108. iop/_local.py +0 -106
  109. iop/_message.py +0 -45
  110. iop/_polling_business_service.py +0 -6
  111. iop/_remote.py +0 -473
  112. iop/_utils.py +0 -912
  113. iop/cls/IOP/Service/WSGI.cls +0 -310
  114. iop/wsgi/handlers.py +0 -112
  115. iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
  116. iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
  117. iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
  118. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
  119. {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