python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.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 (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -0,0 +1,662 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import multiprocessing
5
+ import os
6
+ from collections.abc import Callable, Iterable
7
+ from concurrent.futures import (
8
+ FIRST_COMPLETED,
9
+ Future,
10
+ ProcessPoolExecutor,
11
+ as_completed,
12
+ wait,
13
+ )
14
+ from concurrent.futures.process import BrokenProcessPool
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from time import monotonic, perf_counter
18
+
19
+ from .lsp_server import build_outline_semantic_model
20
+ from .parse_artifacts import (
21
+ OUTLINE_PRODUCER_VERSION,
22
+ ArtifactKey,
23
+ ArtifactStore,
24
+ CachedOutlineArtifact,
25
+ artifact_cache_disabled,
26
+ artifact_key,
27
+ load_outline_artifact,
28
+ store_outline_artifact,
29
+ )
30
+ from .project_config import ProjectPathConfig, workspace_include_loader
31
+ from .semantic_builder import SemanticModel
32
+ from .source_reader import read_source_text
33
+
34
+ _MEBIBYTE = 1024 * 1024
35
+ _WORKER_MEMORY_BYTES = 64 * _MEBIBYTE
36
+ _AUTO_WORKER_LIMIT = 8
37
+
38
+
39
+ class ParallelOutlineError(RuntimeError):
40
+ """Raised when a parallel outline operation cannot complete."""
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class OutlineTask:
45
+ ordinal: int
46
+ source_path: str
47
+ defines: tuple[str, ...]
48
+ return_text: bool
49
+ workspace_root: str = ""
50
+ include_paths: tuple[str, ...] = ()
51
+ project_config: ProjectPathConfig | None = None
52
+ producer_version: str = OUTLINE_PRODUCER_VERSION
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class _OutlineWorkItem:
57
+ ordinal: int
58
+ source_path: str
59
+ return_text: bool
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class _OutlineWorkerContext:
64
+ defines: tuple[str, ...]
65
+ include_paths: tuple[str, ...]
66
+ project_config: ProjectPathConfig | None
67
+ include_files: tuple[str, ...] = ()
68
+
69
+
70
+ _outline_worker_context: _OutlineWorkerContext | None = None
71
+ _outline_worker_include_loader: Callable[[str, str], tuple[str, str] | None] | None = (
72
+ None
73
+ )
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class OutlineResult:
78
+ ordinal: int
79
+ source_path: str
80
+ text: str
81
+ model: SemanticModel | None
82
+ lines_processed: int
83
+ symbols_discovered: int
84
+ read_error: str = ""
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class ParallelBuildStats:
89
+ configured_workers: int
90
+ effective_workers: int
91
+ files_completed: int
92
+ elapsed_seconds: float
93
+ fallbacks: int
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class OutlineBatch:
98
+ results: tuple[OutlineResult, ...]
99
+ stats: ParallelBuildStats
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class _PreparedArtifact:
104
+ store: ArtifactStore
105
+ key: ArtifactKey
106
+
107
+
108
+ def parse_worker_setting(value: str) -> int:
109
+ if value == "auto":
110
+ return 0
111
+ try:
112
+ workers = int(value)
113
+ except ValueError as error:
114
+ raise ValueError(
115
+ "workers must be 'auto' or an integer from 1 through 32"
116
+ ) from error
117
+ if not 1 <= workers <= 32:
118
+ raise ValueError("workers must be 'auto' or an integer from 1 through 32")
119
+ return workers
120
+
121
+
122
+ def resolve_worker_count(
123
+ configured_workers: int,
124
+ *,
125
+ task_count: int,
126
+ cpu_count: int | None = None,
127
+ memory_budget_bytes: int | None = None,
128
+ ) -> int:
129
+ if not 0 <= configured_workers <= 32:
130
+ raise ValueError("workers must be 'auto' or an integer from 1 through 32")
131
+ if task_count <= 0:
132
+ return 0
133
+ if configured_workers:
134
+ return min(configured_workers, task_count)
135
+
136
+ detected_cpus = os.cpu_count() if cpu_count is None else cpu_count
137
+ candidates = [task_count, _AUTO_WORKER_LIMIT, max(1, (detected_cpus or 1) - 1)]
138
+ if memory_budget_bytes is not None:
139
+ candidates.append(max(1, memory_budget_bytes // _WORKER_MEMORY_BYTES))
140
+ return min(candidates)
141
+
142
+
143
+ def _parse_outline_source(
144
+ ordinal: int,
145
+ source_path: str,
146
+ return_text: bool,
147
+ *,
148
+ defines: tuple[str, ...],
149
+ include_paths: tuple[str, ...],
150
+ include_loader: Callable[[str, str], tuple[str, str] | None] | None,
151
+ ) -> OutlineResult:
152
+ try:
153
+ text = read_source_text(Path(source_path))
154
+ except (OSError, UnicodeError) as error:
155
+ return OutlineResult(
156
+ ordinal=ordinal,
157
+ source_path=source_path,
158
+ text="",
159
+ model=None,
160
+ lines_processed=0,
161
+ symbols_discovered=0,
162
+ read_error=str(error),
163
+ )
164
+
165
+ try:
166
+ model = build_outline_semantic_model(
167
+ text,
168
+ source_path,
169
+ defines=defines,
170
+ include_paths=include_paths,
171
+ include_loader=include_loader,
172
+ )
173
+ except Exception as error:
174
+ raise ParallelOutlineError(f"failed to parse {source_path}: {error}") from error
175
+ return OutlineResult(
176
+ ordinal=ordinal,
177
+ source_path=source_path,
178
+ text=text if return_text else "",
179
+ model=model,
180
+ lines_processed=text.count("\n")
181
+ + (0 if not text or text.endswith(("\n", "\r")) else 1),
182
+ symbols_discovered=sum(len(items) for items in model.index.name_index.values()),
183
+ )
184
+
185
+
186
+ def _parse_outline_task(task: OutlineTask) -> OutlineResult:
187
+ return _parse_outline_source(
188
+ task.ordinal,
189
+ task.source_path,
190
+ task.return_text,
191
+ defines=task.defines,
192
+ include_paths=task.include_paths,
193
+ include_loader=workspace_include_loader(
194
+ task.project_config,
195
+ task.include_paths,
196
+ ),
197
+ )
198
+
199
+
200
+ def _initialize_outline_worker(context: _OutlineWorkerContext) -> None:
201
+ global _outline_worker_context, _outline_worker_include_loader
202
+ _outline_worker_context = context
203
+ _outline_worker_include_loader = _inventory_include_loader(context)
204
+
205
+
206
+ def _inventory_include_loader(
207
+ context: _OutlineWorkerContext,
208
+ ) -> Callable[[str, str], tuple[str, str] | None] | None:
209
+ if not context.include_files:
210
+ return workspace_include_loader(
211
+ context.project_config,
212
+ context.include_paths,
213
+ )
214
+ known_files = {
215
+ os.path.normcase(os.path.normpath(path)): path for path in context.include_files
216
+ }
217
+ search_paths = tuple(os.path.normpath(path) for path in context.include_paths)
218
+
219
+ def load(parent_file: str, include_name: str) -> tuple[str, str] | None:
220
+ normalized_name = include_name.replace("\\", os.sep).replace("/", os.sep)
221
+ parent = os.path.dirname(os.path.normpath(parent_file))
222
+ for base in (parent, *search_paths):
223
+ candidate = os.path.normcase(
224
+ os.path.normpath(os.path.join(base, normalized_name))
225
+ )
226
+ selected = known_files.get(candidate)
227
+ if selected is None:
228
+ continue
229
+ selected_path = Path(selected)
230
+ if (
231
+ context.project_config is not None
232
+ and context.project_config.excludes_workspace_path(selected_path)
233
+ ):
234
+ continue
235
+ try:
236
+ return read_source_text(selected_path), str(selected_path)
237
+ except (OSError, UnicodeError):
238
+ continue
239
+ return None
240
+
241
+ return load
242
+
243
+
244
+ def _parse_outline_work_item(item: _OutlineWorkItem) -> OutlineResult:
245
+ context = _outline_worker_context
246
+ if context is None:
247
+ raise ParallelOutlineError("outline worker context was not initialized")
248
+ return _parse_outline_source(
249
+ item.ordinal,
250
+ item.source_path,
251
+ item.return_text,
252
+ defines=context.defines,
253
+ include_paths=context.include_paths,
254
+ include_loader=_outline_worker_include_loader,
255
+ )
256
+
257
+
258
+ def _shared_worker_context(
259
+ tasks: tuple[OutlineTask, ...],
260
+ *,
261
+ include_files: tuple[str, ...],
262
+ ) -> _OutlineWorkerContext | None:
263
+ if not tasks:
264
+ return None
265
+ first = tasks[0]
266
+ context = _OutlineWorkerContext(
267
+ first.defines,
268
+ first.include_paths,
269
+ first.project_config,
270
+ include_files,
271
+ )
272
+ if all(
273
+ task.defines == context.defines
274
+ and task.include_paths == context.include_paths
275
+ and task.project_config == context.project_config
276
+ for task in tasks[1:]
277
+ ):
278
+ return context
279
+ return None
280
+
281
+
282
+ def run_outline_tasks(
283
+ tasks: Iterable[OutlineTask],
284
+ *,
285
+ configured_workers: int,
286
+ use_artifact_cache: bool = True,
287
+ memory_budget_bytes: int | None = None,
288
+ cpu_count: int | None = None,
289
+ on_complete: Callable[[OutlineResult], None] | None = None,
290
+ retain_results: bool = True,
291
+ task_runner: Callable[[OutlineTask], OutlineResult] | None = None,
292
+ include_files: Iterable[str | Path] = (),
293
+ heartbeat: Callable[[], object] | None = None,
294
+ force_heartbeat: Callable[[], object] | None = None,
295
+ active_worker_limit: Callable[[], int] | None = None,
296
+ ) -> OutlineBatch:
297
+ task_list = tuple(tasks)
298
+ selected_include_files = tuple(str(path) for path in include_files)
299
+ operation = _parse_outline_task if task_runner is None else task_runner
300
+ started = perf_counter()
301
+ effective_workers = resolve_worker_count(
302
+ configured_workers,
303
+ task_count=len(task_list),
304
+ cpu_count=cpu_count,
305
+ memory_budget_bytes=memory_budget_bytes,
306
+ )
307
+ if task_runner is None and use_artifact_cache and not artifact_cache_disabled():
308
+ cached_results, pending_tasks, prepared = _prepare_artifacts(task_list)
309
+ else:
310
+ cached_results, pending_tasks, prepared = [], task_list, {}
311
+ accepted: list[OutlineResult] = []
312
+ cached_completed = len(cached_results)
313
+ for result in cached_results:
314
+ if on_complete is not None:
315
+ on_complete(result)
316
+ if retain_results:
317
+ accepted.append(result)
318
+
319
+ if not pending_tasks:
320
+ return _batch(
321
+ configured_workers,
322
+ effective_workers,
323
+ accepted,
324
+ started,
325
+ files_completed=cached_completed,
326
+ fallbacks=0,
327
+ )
328
+
329
+ def complete(result: OutlineResult) -> None:
330
+ artifact = prepared.get((result.ordinal, result.source_path))
331
+ if artifact is not None and not result.read_error and result.model is not None:
332
+ with contextlib.suppress(Exception):
333
+ store_outline_artifact(
334
+ artifact.store,
335
+ artifact.key,
336
+ result.source_path,
337
+ result.model,
338
+ lines_processed=result.lines_processed,
339
+ symbols_discovered=result.symbols_discovered,
340
+ )
341
+ if on_complete is not None:
342
+ on_complete(result)
343
+
344
+ dispatch_workers = min(effective_workers, len(pending_tasks))
345
+ if dispatch_workers <= 1:
346
+ results, serial_completed = _run_serial(
347
+ pending_tasks,
348
+ complete,
349
+ retain_results,
350
+ operation,
351
+ )
352
+ accepted.extend(results)
353
+ _prune_artifacts(prepared)
354
+ return _batch(
355
+ configured_workers,
356
+ effective_workers,
357
+ accepted,
358
+ started,
359
+ files_completed=cached_completed + serial_completed,
360
+ fallbacks=0,
361
+ )
362
+
363
+ worker_context = (
364
+ _shared_worker_context(
365
+ pending_tasks,
366
+ include_files=selected_include_files,
367
+ )
368
+ if task_runner is None
369
+ else None
370
+ )
371
+ futures: dict[Future[OutlineResult], OutlineTask] = {}
372
+ executor: ProcessPoolExecutor | None = None
373
+ parallel_completed = 0
374
+ try:
375
+ context = multiprocessing.get_context("spawn")
376
+ if worker_context is None:
377
+ active_executor = ProcessPoolExecutor(
378
+ max_workers=dispatch_workers,
379
+ mp_context=context,
380
+ )
381
+ else:
382
+ active_executor = ProcessPoolExecutor(
383
+ max_workers=dispatch_workers,
384
+ mp_context=context,
385
+ initializer=_initialize_outline_worker,
386
+ initargs=(worker_context,),
387
+ )
388
+ executor = active_executor
389
+
390
+ def submit(task: OutlineTask) -> Future[OutlineResult]:
391
+ if worker_context is None:
392
+ return active_executor.submit(operation, task)
393
+ return active_executor.submit(
394
+ _parse_outline_work_item,
395
+ _OutlineWorkItem(
396
+ task.ordinal,
397
+ task.source_path,
398
+ task.return_text,
399
+ ),
400
+ )
401
+
402
+ task_iterator = iter(_largest_sources_first(pending_tasks))
403
+ tasks_exhausted = False
404
+
405
+ def desired_window() -> int:
406
+ if active_worker_limit is None:
407
+ return dispatch_workers
408
+ return max(1, min(dispatch_workers, active_worker_limit()))
409
+
410
+ def fill_submission_window() -> None:
411
+ nonlocal tasks_exhausted
412
+ while not tasks_exhausted and len(futures) < desired_window():
413
+ try:
414
+ task = next(task_iterator)
415
+ except StopIteration:
416
+ tasks_exhausted = True
417
+ return
418
+ futures[submit(task)] = task
419
+
420
+ fill_submission_window()
421
+ next_heartbeat = monotonic()
422
+ if heartbeat is not None:
423
+ (force_heartbeat or heartbeat)()
424
+ next_heartbeat = monotonic() + 0.5
425
+ while futures:
426
+ if heartbeat is None:
427
+ future = next(iter(as_completed(tuple(futures))))
428
+ else:
429
+ while True:
430
+ timeout = max(0.0, next_heartbeat - monotonic())
431
+ completed, _pending = wait(
432
+ tuple(futures),
433
+ timeout=timeout,
434
+ return_when=FIRST_COMPLETED,
435
+ )
436
+ now = monotonic()
437
+ if now >= next_heartbeat:
438
+ heartbeat()
439
+ next_heartbeat = now + 0.5
440
+ if completed:
441
+ future = next(iter(completed))
442
+ break
443
+ task = futures.pop(future)
444
+ if heartbeat is not None and parallel_completed == 0:
445
+ (force_heartbeat or heartbeat)()
446
+ try:
447
+ result = future.result()
448
+ except ParallelOutlineError:
449
+ raise
450
+ except Exception as error:
451
+ wrapped = ParallelOutlineError(
452
+ f"parallel outline execution failed for {task.source_path}: {error}"
453
+ )
454
+ if configured_workers == 0 and parallel_completed == 0:
455
+ _cancel_futures(futures)
456
+ _shutdown_failed_executor(executor)
457
+ results, serial_completed = _run_serial(
458
+ pending_tasks,
459
+ complete,
460
+ retain_results,
461
+ operation,
462
+ )
463
+ accepted.extend(results)
464
+ _prune_artifacts(prepared)
465
+ return _batch(
466
+ configured_workers,
467
+ 1,
468
+ accepted,
469
+ started,
470
+ files_completed=cached_completed + serial_completed,
471
+ fallbacks=1,
472
+ )
473
+ raise wrapped from error
474
+ parallel_completed += 1
475
+ complete(result)
476
+ if retain_results:
477
+ accepted.append(result)
478
+ fill_submission_window()
479
+ except ParallelOutlineError:
480
+ _cancel_futures(futures)
481
+ _shutdown_failed_executor(executor)
482
+ raise
483
+ except (OSError, BrokenProcessPool) as error:
484
+ _cancel_futures(futures)
485
+ _shutdown_failed_executor(executor)
486
+ if configured_workers == 0 and parallel_completed == 0:
487
+ results, serial_completed = _run_serial(
488
+ pending_tasks,
489
+ complete,
490
+ retain_results,
491
+ operation,
492
+ )
493
+ accepted.extend(results)
494
+ _prune_artifacts(prepared)
495
+ return _batch(
496
+ configured_workers,
497
+ 1,
498
+ accepted,
499
+ started,
500
+ files_completed=cached_completed + serial_completed,
501
+ fallbacks=1,
502
+ )
503
+ raise ParallelOutlineError(
504
+ f"parallel outline execution failed: {error}"
505
+ ) from error
506
+ except BaseException:
507
+ _cancel_futures(futures)
508
+ _shutdown_failed_executor(executor)
509
+ raise
510
+
511
+ assert executor is not None
512
+ executor.shutdown(wait=True, cancel_futures=False)
513
+ _prune_artifacts(prepared)
514
+
515
+ return _batch(
516
+ configured_workers,
517
+ effective_workers,
518
+ accepted,
519
+ started,
520
+ files_completed=cached_completed + parallel_completed,
521
+ fallbacks=0,
522
+ )
523
+
524
+
525
+ def _prepare_artifacts(
526
+ tasks: tuple[OutlineTask, ...],
527
+ ) -> tuple[
528
+ list[OutlineResult],
529
+ tuple[OutlineTask, ...],
530
+ dict[tuple[int, str], _PreparedArtifact],
531
+ ]:
532
+ cached_results: list[OutlineResult] = []
533
+ pending_tasks: list[OutlineTask] = []
534
+ prepared: dict[tuple[int, str], _PreparedArtifact] = {}
535
+ stores: dict[str, ArtifactStore] = {}
536
+ for task in tasks:
537
+ if not task.workspace_root:
538
+ pending_tasks.append(task)
539
+ continue
540
+ text = ""
541
+ cached: CachedOutlineArtifact | None = None
542
+ cache_check_succeeded = False
543
+ with contextlib.suppress(Exception):
544
+ text = read_source_text(Path(task.source_path))
545
+ key = artifact_key(
546
+ task.workspace_root,
547
+ task.source_path,
548
+ text,
549
+ task.defines,
550
+ include_paths=task.include_paths,
551
+ project_config=task.project_config,
552
+ producer_version=task.producer_version,
553
+ )
554
+ store = stores.setdefault(
555
+ task.workspace_root,
556
+ ArtifactStore(Path(task.workspace_root) / ".delphi-lsp" / "artifacts"),
557
+ )
558
+ prepared[(task.ordinal, task.source_path)] = _PreparedArtifact(store, key)
559
+ cached = load_outline_artifact(store, key, task.source_path)
560
+ cache_check_succeeded = True
561
+ if not cache_check_succeeded:
562
+ pending_tasks.append(task)
563
+ continue
564
+ if cached is None:
565
+ pending_tasks.append(task)
566
+ continue
567
+ cached_results.append(
568
+ OutlineResult(
569
+ ordinal=task.ordinal,
570
+ source_path=task.source_path,
571
+ text=text if task.return_text else "",
572
+ model=cached.model,
573
+ lines_processed=cached.lines_processed,
574
+ symbols_discovered=cached.symbols_discovered,
575
+ )
576
+ )
577
+ return cached_results, tuple(pending_tasks), prepared
578
+
579
+
580
+ def _prune_artifacts(
581
+ prepared: dict[tuple[int, str], _PreparedArtifact],
582
+ ) -> None:
583
+ live_keys_by_root: dict[Path, set[str]] = {}
584
+ stores: dict[Path, ArtifactStore] = {}
585
+ for artifact in prepared.values():
586
+ live_keys_by_root.setdefault(artifact.store.root, set()).add(
587
+ artifact.key.cache_key
588
+ )
589
+ stores[artifact.store.root] = artifact.store
590
+ for root, live_keys in live_keys_by_root.items():
591
+ with contextlib.suppress(Exception):
592
+ stores[root].prune(live_keys)
593
+
594
+
595
+ def _largest_sources_first(tasks: tuple[OutlineTask, ...]) -> tuple[OutlineTask, ...]:
596
+ sizes: dict[str, int] = {}
597
+ for task in tasks:
598
+ try:
599
+ sizes[task.source_path] = Path(task.source_path).stat().st_size
600
+ except OSError:
601
+ sizes[task.source_path] = -1
602
+ return tuple(
603
+ sorted(
604
+ tasks,
605
+ key=lambda task: (
606
+ -sizes[task.source_path],
607
+ task.ordinal,
608
+ ),
609
+ )
610
+ )
611
+
612
+
613
+ def _run_serial(
614
+ tasks: tuple[OutlineTask, ...],
615
+ on_complete: Callable[[OutlineResult], None] | None,
616
+ retain_results: bool,
617
+ operation: Callable[[OutlineTask], OutlineResult],
618
+ ) -> tuple[list[OutlineResult], int]:
619
+ results = []
620
+ for task in tasks:
621
+ result = operation(task)
622
+ if on_complete is not None:
623
+ on_complete(result)
624
+ if retain_results:
625
+ results.append(result)
626
+ return results, len(tasks)
627
+
628
+
629
+ def _cancel_futures(futures: dict[Future[OutlineResult], OutlineTask]) -> None:
630
+ for future in futures:
631
+ future.cancel()
632
+
633
+
634
+ def _shutdown_failed_executor(executor: ProcessPoolExecutor | None) -> None:
635
+ if executor is None:
636
+ return
637
+ try:
638
+ executor.shutdown(wait=False, cancel_futures=True)
639
+ except BaseException:
640
+ pass
641
+
642
+
643
+ def _batch(
644
+ configured_workers: int,
645
+ effective_workers: int,
646
+ results: list[OutlineResult],
647
+ started: float,
648
+ *,
649
+ files_completed: int,
650
+ fallbacks: int,
651
+ ) -> OutlineBatch:
652
+ ordered_results = tuple(sorted(results, key=lambda result: result.ordinal))
653
+ return OutlineBatch(
654
+ results=ordered_results,
655
+ stats=ParallelBuildStats(
656
+ configured_workers=configured_workers,
657
+ effective_workers=effective_workers,
658
+ files_completed=files_completed,
659
+ elapsed_seconds=perf_counter() - started,
660
+ fallbacks=fallbacks,
661
+ ),
662
+ )