pine2pyne 0.2.4__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 (75) hide show
  1. pine2pyne/__init__.py +84 -0
  2. pine2pyne/__main__.py +12 -0
  3. pine2pyne/ast_nodes.py +405 -0
  4. pine2pyne/ast_walk.py +93 -0
  5. pine2pyne/block_value_lowering.py +694 -0
  6. pine2pyne/bool_builtins.py +42 -0
  7. pine2pyne/bool_value_typing.py +580 -0
  8. pine2pyne/cli.py +247 -0
  9. pine2pyne/codegen.py +886 -0
  10. pine2pyne/compat/__init__.py +19 -0
  11. pine2pyne/compat/__main__.py +6 -0
  12. pine2pyne/compat/cli.py +114 -0
  13. pine2pyne/compat/comparison.py +105 -0
  14. pine2pyne/compat/corpus.py +73 -0
  15. pine2pyne/compat/report.py +95 -0
  16. pine2pyne/compat/runner.py +63 -0
  17. pine2pyne/condition_typing.py +260 -0
  18. pine2pyne/conditional_history_calls.py +96 -0
  19. pine2pyne/core/__init__.py +26 -0
  20. pine2pyne/core/contracts.py +125 -0
  21. pine2pyne/di.py +173 -0
  22. pine2pyne/emit/__init__.py +5 -0
  23. pine2pyne/emit/legacy.py +14 -0
  24. pine2pyne/emit/pynecomp/__init__.py +20 -0
  25. pine2pyne/emit/pynecomp/banner.py +30 -0
  26. pine2pyne/emit/pynecomp/blank_lines.py +69 -0
  27. pine2pyne/emit/pynecomp/emitter.py +61 -0
  28. pine2pyne/emit/pynecomp/imports.py +65 -0
  29. pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
  30. pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
  31. pine2pyne/emit/pynecomp/passes/base.py +69 -0
  32. pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
  33. pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
  34. pine2pyne/emit/pynecomp/passes/imports.py +112 -0
  35. pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
  36. pine2pyne/emit/pynecomp/passes/library.py +205 -0
  37. pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
  38. pine2pyne/emit/pynecomp/printer.py +288 -0
  39. pine2pyne/emit/pynecomp/unparser.py +186 -0
  40. pine2pyne/emitted_names.py +152 -0
  41. pine2pyne/errors.py +69 -0
  42. pine2pyne/for_loop_end_bound.py +172 -0
  43. pine2pyne/grouped_history_lowering.py +310 -0
  44. pine2pyne/import_resolver.py +469 -0
  45. pine2pyne/input_hoisting.py +195 -0
  46. pine2pyne/legacy_function_names.py +74 -0
  47. pine2pyne/legacy_sessions.py +68 -0
  48. pine2pyne/lexer.py +630 -0
  49. pine2pyne/library_collision.py +434 -0
  50. pine2pyne/namespace_members.py +247 -0
  51. pine2pyne/node_ids.py +235 -0
  52. pine2pyne/once_lowering.py +75 -0
  53. pine2pyne/overload_resolution.py +475 -0
  54. pine2pyne/parse_repair.py +398 -0
  55. pine2pyne/parser.py +1873 -0
  56. pine2pyne/pine_builtins.py +394 -0
  57. pine2pyne/pine_semantics.py +604 -0
  58. pine2pyne/pipeline/__init__.py +18 -0
  59. pine2pyne/pipeline/service.py +70 -0
  60. pine2pyne/pipeline/stages.py +73 -0
  61. pine2pyne/scope_renamer.py +965 -0
  62. pine2pyne/strategy_when_lowering.py +101 -0
  63. pine2pyne/strict_boolean.py +101 -0
  64. pine2pyne/symbol_table.py +166 -0
  65. pine2pyne/tokens.py +143 -0
  66. pine2pyne/transformer.py +2510 -0
  67. pine2pyne/type_inference.py +422 -0
  68. pine2pyne/udt_field_defaults.py +149 -0
  69. pine2pyne/value_blocks.py +357 -0
  70. pine2pyne-0.2.4.dist-info/METADATA +99 -0
  71. pine2pyne-0.2.4.dist-info/RECORD +75 -0
  72. pine2pyne-0.2.4.dist-info/WHEEL +5 -0
  73. pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
  74. pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
  75. pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,19 @@
1
+ """Byte-compatibility check against the closed PyneComp compiler's outputs."""
2
+ from .comparison import Comparison, ModuleComparator, Verdict
3
+ from .corpus import AssetsCorpus, Corpus, CorpusCase, default_assets_root
4
+ from .report import CompatReport
5
+ from .runner import CaseResult, CompatRunner, run_parallel
6
+
7
+ __all__ = [
8
+ 'AssetsCorpus',
9
+ 'CaseResult',
10
+ 'CompatReport',
11
+ 'CompatRunner',
12
+ 'Comparison',
13
+ 'Corpus',
14
+ 'CorpusCase',
15
+ 'ModuleComparator',
16
+ 'Verdict',
17
+ 'default_assets_root',
18
+ 'run_parallel',
19
+ ]
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == '__main__':
6
+ sys.exit(main())
@@ -0,0 +1,114 @@
1
+ """``python -m pine2pyne.compat`` / ``pine2pyne-compat``: byte compatibility with PyneComp.
2
+
3
+ Transpiles every Pine input of the reference corpus and compares the result
4
+ with what the closed PyneComp compiler produced for it::
5
+
6
+ python -m pine2pyne.compat # pynecomp profile, summary
7
+ python -m pine2pyne.compat -j 8 --worst 20 # parallel, closest mismatches
8
+ python -m pine2pyne.compat --json report.json # every case, machine readable
9
+ python -m pine2pyne.compat --diff indicator/heiken_ashi_candles_mdWsHY8m
10
+
11
+ Exit status: 0 when every compared case is byte-exact, 1 otherwise.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import difflib
17
+ import sys
18
+ import warnings
19
+ from pathlib import Path
20
+ from typing import Sequence
21
+
22
+ from dishka import Provider, Scope, from_context, make_container, provide
23
+
24
+ from ..core.contracts import OutputProfile, Transpiler, TranspilerSettings
25
+ from ..di import build_providers
26
+ from .comparison import ModuleComparator, Verdict
27
+ from .corpus import AssetsCorpus, Corpus, default_assets_root
28
+ from .report import CompatReport
29
+ from .runner import CompatRunner, run_parallel
30
+
31
+
32
+ class CompatProvider(Provider):
33
+ """The compatibility checker, on top of the transpiler providers."""
34
+
35
+ scope = Scope.APP
36
+
37
+ assets_root = from_context(provides=Path, scope=Scope.APP)
38
+
39
+ @provide
40
+ def corpus(self, assets_root: Path) -> Corpus:
41
+ return AssetsCorpus(assets_root)
42
+
43
+ @provide
44
+ def comparator(self) -> ModuleComparator:
45
+ return ModuleComparator()
46
+
47
+ runner = provide(CompatRunner)
48
+
49
+
50
+ def make_compat_container(profile: OutputProfile, assets_root: Path):
51
+ return make_container(
52
+ *build_providers(profile), CompatProvider(),
53
+ context={TranspilerSettings: TranspilerSettings(profile=profile), Path: assets_root},
54
+ )
55
+
56
+
57
+ def parse_args(argv: Sequence[str] | None) -> argparse.Namespace:
58
+ parser = argparse.ArgumentParser(prog='pine2pyne-compat', description=__doc__.split('\n')[0],
59
+ formatter_class=argparse.RawDescriptionHelpFormatter)
60
+ parser.add_argument('--assets', type=Path, default=default_assets_root(),
61
+ help='corpus root holding pine/ and pyne/ (default: %(default)s)')
62
+ parser.add_argument('--profile', default=OutputProfile.PYNECOMP.value,
63
+ choices=[profile.value for profile in OutputProfile], help='output profile to check')
64
+ parser.add_argument('-j', '--jobs', type=int, default=1, help='worker processes')
65
+ parser.add_argument('-k', '--filter', default='', help='only cases whose name contains this text')
66
+ parser.add_argument('--worst', type=int, default=0, help='list the N closest mismatches')
67
+ parser.add_argument('--json', type=Path, help='write every case to this JSON file')
68
+ parser.add_argument('--diff', metavar='CASE', help='print a unified diff for one case (name or part of it)')
69
+ return parser.parse_args(argv)
70
+
71
+
72
+ def _diff(container, needle: str) -> int:
73
+ corpus = container.get(Corpus)
74
+ matches = [case for case in corpus.cases() if needle in case.name]
75
+ if not matches:
76
+ print(f'no case matches {needle!r}', file=sys.stderr)
77
+ return 2
78
+ case = matches[0]
79
+ with warnings.catch_warnings():
80
+ warnings.simplefilter('ignore')
81
+ got = container.get(Transpiler).transpile(case.read_source())
82
+ expected = case.read_expected() or ''
83
+ sys.stdout.writelines(difflib.unified_diff(
84
+ got.splitlines(True), expected.splitlines(True), f'pine2pyne/{case.name}', f'pynecomp/{case.name}'))
85
+ return 0 if got == expected else 1
86
+
87
+
88
+ def main(argv: Sequence[str] | None = None) -> int:
89
+ args = parse_args(argv)
90
+ profile = OutputProfile.parse(args.profile)
91
+ container = make_compat_container(profile, args.assets)
92
+ try:
93
+ if args.diff:
94
+ return _diff(container, args.diff)
95
+ corpus = container.get(Corpus)
96
+ if args.filter:
97
+ corpus_cases = [case for case in corpus.cases() if args.filter in case.name]
98
+ else:
99
+ corpus_cases = list(corpus.cases())
100
+ if args.jobs > 1:
101
+ results = run_parallel(corpus_cases, profile, args.jobs)
102
+ else:
103
+ results = container.get(CompatRunner).run(corpus_cases)
104
+ report = CompatReport(profile.value, results)
105
+ print(report.to_text(worst=args.worst))
106
+ if args.json:
107
+ args.json.write_text(report.to_json(), encoding='utf-8')
108
+ return 0 if report.count(Verdict.EXACT) == report.compared else 1
109
+ finally:
110
+ container.close()
111
+
112
+
113
+ if __name__ == '__main__':
114
+ sys.exit(main())
@@ -0,0 +1,105 @@
1
+ """Comparing one generated module with the reference module."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ import difflib
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+
9
+
10
+ class Verdict(str, Enum):
11
+ #: byte-for-byte identical
12
+ EXACT = 'exact'
13
+ #: identical but for the newline at the end of the file (161 references have
14
+ #: none, at random: no property of the Pine source predicts it)
15
+ FINAL_NEWLINE = 'final-newline'
16
+ #: the same Python program, laid out differently
17
+ LAYOUT = 'layout'
18
+ #: a different program
19
+ DIFFERENT = 'different'
20
+ #: pine2pyne refused or crashed
21
+ ERROR = 'error'
22
+ #: no reference output to compare with
23
+ NO_REFERENCE = 'no-reference'
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Comparison:
28
+ verdict: Verdict
29
+ #: share of equal lines, 0..1 (difflib ratio over lines)
30
+ similarity: float = 0.0
31
+ #: 1-based line of the first difference in the reference module
32
+ first_difference_line: int = 0
33
+ #: region of the reference module the first difference is in
34
+ region: str = ''
35
+ got_line: str = ''
36
+ expected_line: str = ''
37
+ error: str = ''
38
+
39
+
40
+ def _program(source: str) -> str | None:
41
+ """The module's AST without its docstring, or None when it does not parse."""
42
+ try:
43
+ tree = ast.parse(source)
44
+ except SyntaxError:
45
+ return None
46
+ body = tree.body
47
+ if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) \
48
+ and isinstance(body[0].value.value, str):
49
+ body = body[1:]
50
+ return ast.dump(ast.Module(body=body, type_ignores=[]))
51
+
52
+
53
+ def region_of(lines: list[str], index: int) -> str:
54
+ """Name the part of a PyneComp module that 0-based line ``index`` is in."""
55
+ region = 'header'
56
+ in_signature = False
57
+ for position, line in enumerate(lines[:index + 1]):
58
+ stripped = line.strip()
59
+ if region == 'header':
60
+ if position > 0 and stripped == '"""':
61
+ region = 'imports'
62
+ continue
63
+ if in_signature:
64
+ if line.startswith(')'):
65
+ in_signature = False
66
+ region = 'body'
67
+ continue
68
+ if line.startswith('if __name__'):
69
+ region = 'footer'
70
+ elif line.startswith(('from ', 'import ')) and region in ('imports', 'header'):
71
+ region = 'imports'
72
+ elif line.startswith('@script.'):
73
+ region = 'decorator'
74
+ elif line.startswith('def main('):
75
+ region = 'signature'
76
+ in_signature = not line.rstrip().endswith(':')
77
+ if not in_signature:
78
+ region = 'body'
79
+ elif line and not line.startswith((' ', ')')) and region in ('imports', 'module'):
80
+ region = 'module'
81
+ return region
82
+
83
+
84
+ class ModuleComparator:
85
+ """Byte comparison first; on a mismatch, says whether only layout differs."""
86
+
87
+ def compare(self, got: str, expected: str) -> Comparison:
88
+ if got == expected:
89
+ return Comparison(Verdict.EXACT, similarity=1.0)
90
+ if got.rstrip('\n') == expected.rstrip('\n'):
91
+ return Comparison(Verdict.FINAL_NEWLINE, similarity=1.0, region='footer')
92
+ got_lines, expected_lines = got.split('\n'), expected.split('\n')
93
+ matcher = difflib.SequenceMatcher(None, got_lines, expected_lines, autojunk=False)
94
+ first = next((opcode for opcode in matcher.get_opcodes() if opcode[0] != 'equal'), None)
95
+ index = first[3] if first else min(len(got_lines), len(expected_lines))
96
+ got_index = first[1] if first else index
97
+ same_program = _program(got) is not None and _program(got) == _program(expected)
98
+ return Comparison(
99
+ Verdict.LAYOUT if same_program else Verdict.DIFFERENT,
100
+ similarity=matcher.ratio(),
101
+ first_difference_line=index + 1,
102
+ region=region_of(expected_lines, min(index, len(expected_lines) - 1)),
103
+ got_line=got_lines[got_index] if got_index < len(got_lines) else '<end of file>',
104
+ expected_line=expected_lines[index] if index < len(expected_lines) else '<end of file>',
105
+ )
@@ -0,0 +1,73 @@
1
+ """The reference corpus: Pine inputs and the closed compiler's outputs.
2
+
3
+ Layout of ``assets/``::
4
+
5
+ pine/<kind>/<bucket>/<stem>.pine kind: indicator | strategy
6
+ pyne/<kind>_<stem>.py
7
+ pine/lib/<author>/<name>/v<N>.pine
8
+ pyne/lib/<author>/<name>/v<N>.py
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Iterator, Protocol
15
+
16
+ SCRIPT_KINDS = ('indicator', 'strategy')
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class CorpusCase:
21
+ """One Pine input and the reference output for it."""
22
+
23
+ name: str
24
+ kind: str # indicator | strategy | library
25
+ bucket: str # bit-exact | verified | lib
26
+ pine_path: Path
27
+ expected_path: Path
28
+
29
+ def read_source(self) -> str:
30
+ return self.pine_path.read_text(encoding='utf-8')
31
+
32
+ def read_expected(self) -> str | None:
33
+ if not self.expected_path.exists():
34
+ return None
35
+ return self.expected_path.read_text(encoding='utf-8')
36
+
37
+
38
+ class Corpus(Protocol):
39
+ def cases(self) -> Iterator[CorpusCase]: ...
40
+
41
+
42
+ class AssetsCorpus:
43
+ """The ``assets/pine`` -> ``assets/pyne`` pairs of this repository."""
44
+
45
+ def __init__(self, root: Path) -> None:
46
+ self.root = Path(root)
47
+
48
+ @property
49
+ def pine_root(self) -> Path:
50
+ return self.root / 'pine'
51
+
52
+ @property
53
+ def pyne_root(self) -> Path:
54
+ return self.root / 'pyne'
55
+
56
+ def cases(self) -> Iterator[CorpusCase]:
57
+ for kind in SCRIPT_KINDS:
58
+ for pine_path in sorted((self.pine_root / kind).rglob('*.pine')):
59
+ bucket = pine_path.parent.name
60
+ yield CorpusCase(
61
+ name=f'{kind}/{pine_path.stem}', kind=kind, bucket=bucket, pine_path=pine_path,
62
+ expected_path=self.pyne_root / f'{kind}_{pine_path.stem}.py')
63
+ library_root = self.pine_root / 'lib'
64
+ for pine_path in sorted(library_root.rglob('*.pine')):
65
+ relative = pine_path.relative_to(library_root)
66
+ yield CorpusCase(
67
+ name=f'lib/{relative.with_suffix("").as_posix()}', kind='library', bucket='lib',
68
+ pine_path=pine_path, expected_path=self.pyne_root / 'lib' / relative.with_suffix('.py'))
69
+
70
+
71
+ def default_assets_root() -> Path:
72
+ """``assets/`` next to the package (a source checkout)."""
73
+ return Path(__file__).resolve().parents[2] / 'assets'
@@ -0,0 +1,95 @@
1
+ """Summaries of a compatibility run."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from collections import Counter
6
+ from dataclasses import dataclass
7
+ from statistics import mean, median
8
+ from typing import Sequence
9
+
10
+ from .comparison import Verdict
11
+ from .runner import CaseResult
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CompatReport:
16
+ profile: str
17
+ results: Sequence[CaseResult]
18
+
19
+ def count(self, verdict: Verdict) -> int:
20
+ return sum(1 for result in self.results if result.comparison.verdict is verdict)
21
+
22
+ @property
23
+ def compared(self) -> int:
24
+ return sum(1 for result in self.results if result.comparison.verdict is not Verdict.NO_REFERENCE)
25
+
26
+ def by_bucket(self) -> dict[str, Counter]:
27
+ buckets: dict[str, Counter] = {}
28
+ for result in self.results:
29
+ buckets.setdefault(f'{result.case.kind}/{result.case.bucket}', Counter())[result.comparison.verdict] += 1
30
+ return buckets
31
+
32
+ def regions(self) -> Counter:
33
+ return Counter(result.comparison.region for result in self.results
34
+ if result.comparison.verdict in (Verdict.DIFFERENT, Verdict.LAYOUT))
35
+
36
+ def errors(self) -> Counter:
37
+ return Counter(result.comparison.error.split(':')[0] for result in self.results
38
+ if result.comparison.verdict is Verdict.ERROR)
39
+
40
+ def similarities(self) -> list[float]:
41
+ return [result.comparison.similarity for result in self.results
42
+ if result.comparison.verdict not in (Verdict.ERROR, Verdict.NO_REFERENCE)]
43
+
44
+ def to_text(self, worst: int = 0) -> str:
45
+ compared = self.compared or 1
46
+ lines = [f'pine2pyne vs PyneComp reference outputs (profile: {self.profile})', '']
47
+ for verdict in Verdict:
48
+ number = self.count(verdict)
49
+ lines.append(f' {verdict.value:<13} {number:>5} ({100 * number / compared:5.1f}%)')
50
+ similarities = self.similarities()
51
+ if similarities:
52
+ lines.append(f' line similarity: mean {mean(similarities):.3f}, median {median(similarities):.3f}')
53
+ lines += ['', 'By bucket:']
54
+ for bucket, counter in sorted(self.by_bucket().items()):
55
+ parts = ', '.join(f'{verdict.value}={counter[verdict]}' for verdict in Verdict if counter[verdict])
56
+ lines.append(f' {bucket:<22} {parts}')
57
+ regions = self.regions()
58
+ if regions:
59
+ lines += ['', 'First difference by region:']
60
+ lines += [f' {region or "?":<10} {number:>5}' for region, number in regions.most_common()]
61
+ errors = self.errors()
62
+ if errors:
63
+ lines += ['', 'Errors:']
64
+ lines += [f' {name:<28} {number:>5}' for name, number in errors.most_common()]
65
+ if worst:
66
+ ranked = sorted((result for result in self.results
67
+ if result.comparison.verdict in (Verdict.DIFFERENT, Verdict.LAYOUT)),
68
+ key=lambda result: -result.comparison.similarity)[:worst]
69
+ if ranked:
70
+ lines += ['', f'Closest {len(ranked)} mismatches (first difference):']
71
+ for result in ranked:
72
+ comparison = result.comparison
73
+ lines.append(f' {result.case.name} [{comparison.similarity:.3f}, line '
74
+ f'{comparison.first_difference_line}, {comparison.region}]')
75
+ lines.append(f' - {comparison.got_line.strip()[:150]}')
76
+ lines.append(f' + {comparison.expected_line.strip()[:150]}')
77
+ return '\n'.join(lines)
78
+
79
+ def to_json(self) -> str:
80
+ return json.dumps({
81
+ 'profile': self.profile,
82
+ 'summary': {verdict.value: self.count(verdict) for verdict in Verdict},
83
+ 'cases': [{
84
+ 'name': result.case.name,
85
+ 'pine': str(result.case.pine_path),
86
+ 'expected': str(result.case.expected_path),
87
+ 'verdict': result.comparison.verdict.value,
88
+ 'similarity': round(result.comparison.similarity, 4),
89
+ 'first_difference_line': result.comparison.first_difference_line,
90
+ 'region': result.comparison.region,
91
+ 'got_line': result.comparison.got_line,
92
+ 'expected_line': result.comparison.expected_line,
93
+ 'error': result.comparison.error,
94
+ } for result in self.results],
95
+ }, ensure_ascii=False, indent=1)
@@ -0,0 +1,63 @@
1
+ """Running the corpus through a transpiler and collecting comparisons."""
2
+ from __future__ import annotations
3
+
4
+ import warnings
5
+ from concurrent.futures import ProcessPoolExecutor
6
+ from dataclasses import dataclass
7
+ from typing import Callable, Iterable, Sequence
8
+
9
+ from ..core.contracts import OutputProfile, Transpiler
10
+ from .comparison import Comparison, ModuleComparator, Verdict
11
+ from .corpus import CorpusCase
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CaseResult:
16
+ case: CorpusCase
17
+ comparison: Comparison
18
+
19
+
20
+ def _describe(error: BaseException) -> str:
21
+ text = str(error).strip().split('\n')[0]
22
+ return f'{type(error).__name__}: {text}'[:300]
23
+
24
+
25
+ class CompatRunner:
26
+ """Transpiles each case and compares the result with its reference."""
27
+
28
+ def __init__(self, transpiler: Transpiler, comparator: ModuleComparator) -> None:
29
+ self._transpiler = transpiler
30
+ self._comparator = comparator
31
+
32
+ def run_case(self, case: CorpusCase) -> CaseResult:
33
+ expected = case.read_expected()
34
+ if expected is None:
35
+ return CaseResult(case, Comparison(Verdict.NO_REFERENCE))
36
+ try:
37
+ with warnings.catch_warnings():
38
+ warnings.simplefilter('ignore')
39
+ got = self._transpiler.transpile(case.read_source())
40
+ except Exception as error: # a refusal or a crash is a result, not a harness failure
41
+ return CaseResult(case, Comparison(Verdict.ERROR, error=_describe(error)))
42
+ return CaseResult(case, self._comparator.compare(got, expected))
43
+
44
+ def run(self, cases: Iterable[CorpusCase], progress: Callable[[CaseResult], None] | None = None) -> list[CaseResult]:
45
+ results = []
46
+ for case in cases:
47
+ result = self.run_case(case)
48
+ if progress:
49
+ progress(result)
50
+ results.append(result)
51
+ return results
52
+
53
+
54
+ def _run_in_worker(arguments: tuple[str, CorpusCase]) -> CaseResult:
55
+ profile, case = arguments
56
+ from ..di import get_transpiler
57
+ return CompatRunner(get_transpiler(profile), ModuleComparator()).run_case(case)
58
+
59
+
60
+ def run_parallel(cases: Sequence[CorpusCase], profile: OutputProfile, jobs: int) -> list[CaseResult]:
61
+ """``cases`` in ``jobs`` worker processes, each with its own container."""
62
+ with ProcessPoolExecutor(max_workers=jobs) as pool:
63
+ return list(pool.map(_run_in_worker, [(profile.value, case) for case in cases], chunksize=8))