pine2pyne 0.2.4__tar.gz → 0.2.5__tar.gz
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.
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/MANIFEST.in +1 -0
- {pine2pyne-0.2.4/pine2pyne.egg-info → pine2pyne-0.2.5}/PKG-INFO +1 -1
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/__init__.py +1 -1
- {pine2pyne-0.2.4 → pine2pyne-0.2.5/pine2pyne.egg-info}/PKG-INFO +1 -1
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne.egg-info/SOURCES.txt +0 -7
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne.egg-info/entry_points.txt +0 -1
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pyproject.toml +2 -3
- pine2pyne-0.2.4/pine2pyne/compat/__init__.py +0 -19
- pine2pyne-0.2.4/pine2pyne/compat/__main__.py +0 -6
- pine2pyne-0.2.4/pine2pyne/compat/cli.py +0 -114
- pine2pyne-0.2.4/pine2pyne/compat/comparison.py +0 -105
- pine2pyne-0.2.4/pine2pyne/compat/corpus.py +0 -73
- pine2pyne-0.2.4/pine2pyne/compat/report.py +0 -95
- pine2pyne-0.2.4/pine2pyne/compat/runner.py +0 -63
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/LICENSE +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/README.md +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/__main__.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/ast_nodes.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/ast_walk.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/block_value_lowering.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/bool_builtins.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/bool_value_typing.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/cli.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/codegen.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/condition_typing.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/conditional_history_calls.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/core/__init__.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/core/contracts.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/di.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/__init__.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/legacy.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/__init__.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/banner.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/blank_lines.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/emitter.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/imports.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/__init__.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/annotations.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/base.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/builtins.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/hoisting.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/imports.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/legacy_colors.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/library.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/passes/rewrites.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/printer.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emit/pynecomp/unparser.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/emitted_names.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/errors.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/for_loop_end_bound.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/grouped_history_lowering.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/import_resolver.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/input_hoisting.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/legacy_function_names.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/legacy_sessions.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/lexer.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/library_collision.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/namespace_members.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/node_ids.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/once_lowering.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/overload_resolution.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/parse_repair.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/parser.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/pine_builtins.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/pine_semantics.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/pipeline/__init__.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/pipeline/service.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/pipeline/stages.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/scope_renamer.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/strategy_when_lowering.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/strict_boolean.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/symbol_table.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/tokens.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/transformer.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/type_inference.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/udt_field_defaults.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne/value_blocks.py +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne.egg-info/dependency_links.txt +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne.egg-info/requires.txt +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/pine2pyne.egg-info/top_level.txt +0 -0
- {pine2pyne-0.2.4 → pine2pyne-0.2.5}/setup.cfg +0 -0
|
@@ -47,13 +47,6 @@ pine2pyne.egg-info/dependency_links.txt
|
|
|
47
47
|
pine2pyne.egg-info/entry_points.txt
|
|
48
48
|
pine2pyne.egg-info/requires.txt
|
|
49
49
|
pine2pyne.egg-info/top_level.txt
|
|
50
|
-
pine2pyne/compat/__init__.py
|
|
51
|
-
pine2pyne/compat/__main__.py
|
|
52
|
-
pine2pyne/compat/cli.py
|
|
53
|
-
pine2pyne/compat/comparison.py
|
|
54
|
-
pine2pyne/compat/corpus.py
|
|
55
|
-
pine2pyne/compat/report.py
|
|
56
|
-
pine2pyne/compat/runner.py
|
|
57
50
|
pine2pyne/core/__init__.py
|
|
58
51
|
pine2pyne/core/contracts.py
|
|
59
52
|
pine2pyne/emit/__init__.py
|
|
@@ -47,8 +47,8 @@ runtime = ["opencode-pyneruntime[cli]>=6.9.1"]
|
|
|
47
47
|
# an optional argv and returns an int exit code, so it is a valid console_script
|
|
48
48
|
# target as-is. `python -m pine2pyne` keeps working via pine2pyne/__main__.py.
|
|
49
49
|
pine2pyne = "pine2pyne.cli:main"
|
|
50
|
-
#
|
|
51
|
-
|
|
50
|
+
# The byte-compatibility check against PyneComp (assets/pine -> assets/pyne) is not published:
|
|
51
|
+
# it needs assets/, so it runs from a checkout as `python -m pine2pyne.compat`.
|
|
52
52
|
|
|
53
53
|
[tool.setuptools]
|
|
54
54
|
# MUST be explicit. This is a flat-layout repo with 14 sibling top-level dirs
|
|
@@ -62,7 +62,6 @@ packages = [
|
|
|
62
62
|
"pine2pyne.emit",
|
|
63
63
|
"pine2pyne.emit.pynecomp",
|
|
64
64
|
"pine2pyne.emit.pynecomp.passes",
|
|
65
|
-
"pine2pyne.compat",
|
|
66
65
|
]
|
|
67
66
|
|
|
68
67
|
[tool.setuptools.dynamic]
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
]
|
|
@@ -1,114 +0,0 @@
|
|
|
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())
|
|
@@ -1,105 +0,0 @@
|
|
|
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
|
-
)
|
|
@@ -1,73 +0,0 @@
|
|
|
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'
|
|
@@ -1,95 +0,0 @@
|
|
|
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)
|
|
@@ -1,63 +0,0 @@
|
|
|
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))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|