process-bigraph-lang 0.1.0__py3-none-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.
- process_bigraph_lang/__init__.py +0 -0
- process_bigraph_lang/bin/.gitignore +4 -0
- process_bigraph_lang/bin/cli-native.exe +4 -0
- process_bigraph_lang/py.typed +0 -0
- process_bigraph_lang/v2/__init__.py +56 -0
- process_bigraph_lang/v2/__main__.py +204 -0
- process_bigraph_lang/v2/api.py +192 -0
- process_bigraph_lang/v2/check.py +73 -0
- process_bigraph_lang/v2/interfaces.py +236 -0
- process_bigraph_lang/v2/langium.py +38 -0
- process_bigraph_lang/v2/normalize.py +384 -0
- process_bigraph_lang/v2/sbml.py +234 -0
- process_bigraph_lang/v2/types.py +244 -0
- process_bigraph_lang-0.1.0.dist-info/METADATA +82 -0
- process_bigraph_lang-0.1.0.dist-info/RECORD +17 -0
- process_bigraph_lang-0.1.0.dist-info/WHEEL +5 -0
- process_bigraph_lang-0.1.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The `.pblang` v2 language, Python side.
|
|
3
|
+
|
|
4
|
+
The v2 language is a tree-shaped, verifiable projection of a process-bigraph composite document
|
|
5
|
+
(see `docs/v2/design.md`). The grammar, the checker, the compiler and the decompiler are TypeScript (`dsl/`),
|
|
6
|
+
run here through `cli-native`; this package is the Python entry to them, plus what has to be Python:
|
|
7
|
+
|
|
8
|
+
- `compile_text` / `compile_file`: `.pblang` text -> parsed, checked -> a `{"schema", "state"}` document or a
|
|
9
|
+
CompositeSpec mapping (`CompileError` when the checker finds errors)
|
|
10
|
+
- `decompile`: a document or a CompositeSpec mapping -> canonical `.pblang` text plus sidecar assets
|
|
11
|
+
- `check_file` / `check_text`: the checker's findings (`Diagnostic`s)
|
|
12
|
+
- `describe_module` / `InterfaceRegistry`: interface declarations ("headers") from a package's classes, as the
|
|
13
|
+
JSON manifest the CLI reads; `render_interfaces` has the CLI print the `.pblang` header file
|
|
14
|
+
- `normalize_document` / `equivalent` / `diff_documents`: equivalence of two documents modulo representation
|
|
15
|
+
|
|
16
|
+
There is no Python model of a composite: a Python tool that builds composites targets the DSL text.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from process_bigraph_lang.v2.api import (
|
|
20
|
+
CompileError,
|
|
21
|
+
Compiled,
|
|
22
|
+
Printed,
|
|
23
|
+
compile_file,
|
|
24
|
+
compile_text,
|
|
25
|
+
decompile,
|
|
26
|
+
dumps,
|
|
27
|
+
loads,
|
|
28
|
+
read_json,
|
|
29
|
+
write_json,
|
|
30
|
+
)
|
|
31
|
+
from process_bigraph_lang.v2.check import Diagnostic, check_file, check_text
|
|
32
|
+
from process_bigraph_lang.v2.interfaces import InterfaceRegistry, describe_module, render_interfaces
|
|
33
|
+
from process_bigraph_lang.v2.normalize import deep_equal, diff_documents, equivalent, normalize_document
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"CompileError",
|
|
37
|
+
"Compiled",
|
|
38
|
+
"Diagnostic",
|
|
39
|
+
"InterfaceRegistry",
|
|
40
|
+
"Printed",
|
|
41
|
+
"check_file",
|
|
42
|
+
"check_text",
|
|
43
|
+
"compile_file",
|
|
44
|
+
"compile_text",
|
|
45
|
+
"decompile",
|
|
46
|
+
"deep_equal",
|
|
47
|
+
"describe_module",
|
|
48
|
+
"diff_documents",
|
|
49
|
+
"dumps",
|
|
50
|
+
"equivalent",
|
|
51
|
+
"loads",
|
|
52
|
+
"normalize_document",
|
|
53
|
+
"read_json",
|
|
54
|
+
"render_interfaces",
|
|
55
|
+
"write_json",
|
|
56
|
+
]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`python -m process_bigraph_lang.v2 compile <file.pblang> [--interfaces m.json]... [--to document|spec] [-o out]`:
|
|
3
|
+
parse and check the text, then write the document (JSON) or the CompositeSpec (yaml); errors stop it.
|
|
4
|
+
`python -m process_bigraph_lang.v2 decompile <doc.json | spec.composite.yaml>`: a document as `.pblang` text.
|
|
5
|
+
`python -m process_bigraph_lang.v2 check <file.pblang | doc.json> [--interfaces m.json]...`: the checker's findings.
|
|
6
|
+
`python -m process_bigraph_lang.v2 diff <a.json> <b.json>`: explain how two documents differ after normalization.
|
|
7
|
+
`python -m process_bigraph_lang.v2 describe <module>...` / `describe-model <sbml>`: headers from code or SBML.
|
|
8
|
+
|
|
9
|
+
Documents are JSON (`bigraph_schema.json_codec` encoding, so numpy arrays survive) or CompositeSpec yaml.
|
|
10
|
+
Sidecar assets of `decompile` are written next to the output when `--out` is given, else listed on stderr.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from process_bigraph_lang.v2 import (
|
|
21
|
+
CompileError,
|
|
22
|
+
InterfaceRegistry,
|
|
23
|
+
compile_file,
|
|
24
|
+
decompile,
|
|
25
|
+
diff_documents,
|
|
26
|
+
equivalent,
|
|
27
|
+
loads,
|
|
28
|
+
render_interfaces,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load(path: Path) -> dict[str, Any]:
|
|
33
|
+
text = path.read_text(encoding="utf-8")
|
|
34
|
+
if path.suffix in (".yaml", ".yml"):
|
|
35
|
+
import yaml
|
|
36
|
+
|
|
37
|
+
loaded = yaml.safe_load(text)
|
|
38
|
+
else:
|
|
39
|
+
loaded = loads(text)
|
|
40
|
+
if not isinstance(loaded, dict):
|
|
41
|
+
raise SystemExit(f"{path}: not a document")
|
|
42
|
+
return loaded
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _manifests(args: argparse.Namespace) -> list[Path]:
|
|
46
|
+
return [Path(m) for m in getattr(args, "interfaces", None) or []]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _decompile(args: argparse.Namespace) -> int:
|
|
50
|
+
path = Path(args.document)
|
|
51
|
+
printed = decompile(_load(path), name=path.stem.replace(".composite", ""), manifests=_manifests(args))
|
|
52
|
+
if args.out:
|
|
53
|
+
out = Path(args.out)
|
|
54
|
+
out.write_text(printed.text, encoding="utf-8")
|
|
55
|
+
for asset_name, content in printed.assets.items():
|
|
56
|
+
target = out.parent / asset_name
|
|
57
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
target.write_bytes(content)
|
|
59
|
+
print(f"wrote {out} and {len(printed.assets)} asset(s)", file=sys.stderr)
|
|
60
|
+
else:
|
|
61
|
+
sys.stdout.write(printed.text)
|
|
62
|
+
for asset_name in printed.assets:
|
|
63
|
+
print(f"asset (not written): {asset_name}", file=sys.stderr)
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _compile(args: argparse.Namespace) -> int:
|
|
68
|
+
try:
|
|
69
|
+
compiled = compile_file(args.document, manifests=_manifests(args), to=args.to, strict=args.strict)
|
|
70
|
+
except CompileError as error:
|
|
71
|
+
for diagnostic in error.diagnostics:
|
|
72
|
+
print(diagnostic, file=sys.stderr)
|
|
73
|
+
print("compile failed", file=sys.stderr)
|
|
74
|
+
return 1
|
|
75
|
+
for diagnostic in compiled.diagnostics:
|
|
76
|
+
print(diagnostic, file=sys.stderr)
|
|
77
|
+
if args.to == "spec":
|
|
78
|
+
import yaml
|
|
79
|
+
|
|
80
|
+
text = yaml.safe_dump(compiled.document, sort_keys=False)
|
|
81
|
+
else:
|
|
82
|
+
from process_bigraph_lang.v2 import dumps
|
|
83
|
+
|
|
84
|
+
text = dumps(compiled.document) + "\n"
|
|
85
|
+
if args.out:
|
|
86
|
+
Path(args.out).write_text(text, encoding="utf-8")
|
|
87
|
+
print(f"wrote {args.out}", file=sys.stderr)
|
|
88
|
+
else:
|
|
89
|
+
sys.stdout.write(text)
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _diff(args: argparse.Namespace) -> int:
|
|
94
|
+
import process_bigraph as pg # type: ignore[import-untyped]
|
|
95
|
+
|
|
96
|
+
core = pg.allocate_core()
|
|
97
|
+
a, b = _load(Path(args.a)), _load(Path(args.b))
|
|
98
|
+
if equivalent(a, b, core):
|
|
99
|
+
return 0
|
|
100
|
+
print(diff_documents(a, b, core))
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _describe(args: argparse.Namespace) -> int:
|
|
105
|
+
registry = InterfaceRegistry.from_modules(*args.modules)
|
|
106
|
+
if args.manifest:
|
|
107
|
+
registry.write_manifest(Path(args.manifest))
|
|
108
|
+
files = {
|
|
109
|
+
module: render_interfaces([d for d in registry.decls if d.module == module], module) for module in args.modules
|
|
110
|
+
}
|
|
111
|
+
if args.out and (len(files) > 1 or Path(args.out).is_dir()):
|
|
112
|
+
out_dir = Path(args.out)
|
|
113
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
for module, text in files.items():
|
|
115
|
+
(out_dir / f"{module}.pblang").write_text(text, encoding="utf-8")
|
|
116
|
+
print(f"wrote {len(files)} interface file(s) to {out_dir}", file=sys.stderr)
|
|
117
|
+
elif args.out:
|
|
118
|
+
Path(args.out).write_text(next(iter(files.values())), encoding="utf-8")
|
|
119
|
+
else:
|
|
120
|
+
sys.stdout.write("".join(files.values()))
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _check(args: argparse.Namespace) -> int:
|
|
125
|
+
from process_bigraph_lang.v2.check import check_file, check_text
|
|
126
|
+
|
|
127
|
+
source = Path(args.document)
|
|
128
|
+
manifests = _manifests(args)
|
|
129
|
+
if source.suffix in (".json", ".yaml", ".yml"): # a document: decompile it, then check the text
|
|
130
|
+
printed = decompile(_load(source), name=source.stem, manifests=manifests)
|
|
131
|
+
diagnostics = check_text(printed.text, manifests)
|
|
132
|
+
else:
|
|
133
|
+
diagnostics = check_file(source, manifests)
|
|
134
|
+
for diagnostic in diagnostics:
|
|
135
|
+
print(diagnostic)
|
|
136
|
+
errors = sum(1 for d in diagnostics if d.severity == "error")
|
|
137
|
+
print(f"{errors} error(s), {sum(1 for d in diagnostics if d.severity == 'warning')} warning(s)", file=sys.stderr)
|
|
138
|
+
return 1 if errors else 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _describe_model(args: argparse.Namespace) -> int:
|
|
142
|
+
import json
|
|
143
|
+
|
|
144
|
+
from process_bigraph_lang.v2.sbml import describe_sbml, print_model_header
|
|
145
|
+
|
|
146
|
+
header = describe_sbml(args.model)
|
|
147
|
+
if args.manifest:
|
|
148
|
+
Path(args.manifest).write_text(json.dumps(header.to_dict(), indent=2) + "\n", encoding="utf-8")
|
|
149
|
+
text = print_model_header(header)
|
|
150
|
+
if args.out:
|
|
151
|
+
Path(args.out).write_text(text, encoding="utf-8")
|
|
152
|
+
else:
|
|
153
|
+
sys.stdout.write(text)
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv: list[str] | None = None) -> int:
|
|
158
|
+
parser = argparse.ArgumentParser(prog="python -m process_bigraph_lang.v2")
|
|
159
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
160
|
+
compile_cmd = commands.add_parser("compile", help="parse and check a .pblang file, then write the document")
|
|
161
|
+
compile_cmd.add_argument("document", help="a .pblang file (file(...) references resolve next to it)")
|
|
162
|
+
compile_cmd.add_argument("--interfaces", action="append", help="a manifest.json (repeatable)")
|
|
163
|
+
compile_cmd.add_argument("--to", choices=["document", "spec"], default="document")
|
|
164
|
+
compile_cmd.add_argument("--strict", action="store_true", help="warnings fail the compile too")
|
|
165
|
+
compile_cmd.add_argument("-o", "--out", help="write the document (JSON) or CompositeSpec (yaml) here")
|
|
166
|
+
compile_cmd.set_defaults(run=_compile)
|
|
167
|
+
decompile_cmd = commands.add_parser(
|
|
168
|
+
"decompile", aliases=["print"], help="print a document or CompositeSpec as .pblang text"
|
|
169
|
+
)
|
|
170
|
+
decompile_cmd.add_argument("document")
|
|
171
|
+
decompile_cmd.add_argument("--out", help="write the text here (assets go next to it)")
|
|
172
|
+
decompile_cmd.add_argument(
|
|
173
|
+
"--interfaces", action="append", help="a manifest.json (repeatable); matching addresses print as imports"
|
|
174
|
+
)
|
|
175
|
+
decompile_cmd.set_defaults(run=_decompile)
|
|
176
|
+
describe_cmd = commands.add_parser("describe", help="generate interface declarations from Python modules")
|
|
177
|
+
describe_cmd.add_argument("modules", nargs="+", help="importable module names (run in the package's env)")
|
|
178
|
+
describe_cmd.add_argument("--manifest", help="write the JSON manifest here")
|
|
179
|
+
describe_cmd.add_argument(
|
|
180
|
+
"--out", help="the .pblang interface file (one module) or a directory (one file per module)"
|
|
181
|
+
)
|
|
182
|
+
describe_cmd.set_defaults(run=_describe)
|
|
183
|
+
model_cmd = commands.add_parser("describe-model", help="generate a model header from an SBML file")
|
|
184
|
+
model_cmd.add_argument("model", help="an SBML file")
|
|
185
|
+
model_cmd.add_argument("--manifest", help="write the JSON form here")
|
|
186
|
+
model_cmd.add_argument("--out", help="write the .pblang model header here")
|
|
187
|
+
model_cmd.set_defaults(run=_describe_model)
|
|
188
|
+
check_cmd = commands.add_parser("check", help="statically check a .pblang file or a document against manifests")
|
|
189
|
+
check_cmd.add_argument(
|
|
190
|
+
"document", help="a .pblang file, or a document (.json) / CompositeSpec (.yaml) to decompile and check"
|
|
191
|
+
)
|
|
192
|
+
check_cmd.add_argument("--interfaces", action="append", help="a manifest.json (repeatable)")
|
|
193
|
+
check_cmd.set_defaults(run=_check)
|
|
194
|
+
diff_cmd = commands.add_parser("diff", help="compare two documents after normalization (exit 1 if they differ)")
|
|
195
|
+
diff_cmd.add_argument("a")
|
|
196
|
+
diff_cmd.add_argument("b")
|
|
197
|
+
diff_cmd.set_defaults(run=_diff)
|
|
198
|
+
args = parser.parse_args(argv)
|
|
199
|
+
result: int = args.run(args)
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
sys.exit(main())
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The public Python API of v2: text in, checked documents out; documents in, text out. All of it runs in the DSL
|
|
3
|
+
CLI (`cli-native`, TypeScript): parsing, checking, compiling and decompiling. There is no Python model of a
|
|
4
|
+
composite, so a Python tool that builds composites targets the DSL text, where the result is checked, reviewed
|
|
5
|
+
and documented (docs/v2/design.md, section 5.3).
|
|
6
|
+
|
|
7
|
+
- `compile_text` / `compile_file`: `.pblang` text -> parse -> check -> a process-bigraph `{"schema", "state"}`
|
|
8
|
+
document or a CompositeSpec mapping. An error-level finding stops the compile (`CompileError` carries every
|
|
9
|
+
finding); warnings and infos come back on the result.
|
|
10
|
+
- `decompile`: a document or a CompositeSpec mapping -> canonical `.pblang` text plus sidecar assets.
|
|
11
|
+
|
|
12
|
+
Documents cross the CLI boundary as JSON in `bigraph_schema.json_codec`'s encoding (numpy arrays tagged), with
|
|
13
|
+
tuples tagged as `{"__tuple__": true, "data": [...]}` so that they survive too.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import tempfile
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Literal
|
|
23
|
+
|
|
24
|
+
from bigraph_schema import json_codec # type: ignore[import-untyped]
|
|
25
|
+
|
|
26
|
+
from process_bigraph_lang.v2.check import Diagnostic, check_file, registry_manifests
|
|
27
|
+
from process_bigraph_lang.v2.interfaces import InterfaceRegistry
|
|
28
|
+
from process_bigraph_lang.v2.langium import run_cli
|
|
29
|
+
|
|
30
|
+
Target = Literal["document", "spec"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Compiled:
|
|
35
|
+
"""What `compile_text` / `compile_file` return: the artifact and the findings that did not stop the compile."""
|
|
36
|
+
|
|
37
|
+
document: dict[str, Any]
|
|
38
|
+
diagnostics: list[Diagnostic] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def warnings(self) -> list[Diagnostic]:
|
|
42
|
+
return [d for d in self.diagnostics if d.severity == "warning"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Printed:
|
|
47
|
+
"""What `decompile` returns: the `.pblang` text and its sidecar assets (`<name>.assets/...`), by path."""
|
|
48
|
+
|
|
49
|
+
text: str
|
|
50
|
+
assets: dict[str, bytes] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CompileError(Exception):
|
|
54
|
+
"""The text did not pass the checker; `diagnostics` holds every finding, errors first."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, diagnostics: list[Diagnostic]):
|
|
57
|
+
self.diagnostics = diagnostics
|
|
58
|
+
errors = [d for d in diagnostics if d.severity == "error"]
|
|
59
|
+
super().__init__("\n".join(str(d) for d in errors or diagnostics))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def compile_text(
|
|
63
|
+
text: str,
|
|
64
|
+
*,
|
|
65
|
+
registry: InterfaceRegistry | None = None,
|
|
66
|
+
manifests: list[Path] | None = None,
|
|
67
|
+
to: Target = "document",
|
|
68
|
+
strict: bool = False,
|
|
69
|
+
force: bool = False,
|
|
70
|
+
assets: dict[str, bytes] | None = None,
|
|
71
|
+
) -> Compiled:
|
|
72
|
+
"""Compile v2 text. `assets` holds the sidecar files `file(...)` references name, by path."""
|
|
73
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
74
|
+
source = Path(tmp) / "document.pblang"
|
|
75
|
+
source.write_text(text, encoding="utf-8")
|
|
76
|
+
for name, content in (assets or {}).items():
|
|
77
|
+
target = Path(tmp) / name
|
|
78
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
target.write_bytes(content)
|
|
80
|
+
return compile_file(source, registry=registry, manifests=manifests, to=to, strict=strict, force=force)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compile_file(
|
|
84
|
+
path: str | os.PathLike[str],
|
|
85
|
+
*,
|
|
86
|
+
registry: InterfaceRegistry | None = None,
|
|
87
|
+
manifests: list[Path] | None = None,
|
|
88
|
+
to: Target = "document",
|
|
89
|
+
strict: bool = False,
|
|
90
|
+
force: bool = False,
|
|
91
|
+
) -> Compiled:
|
|
92
|
+
"""
|
|
93
|
+
Compile a `.pblang` file; `file(...)` references resolve next to it. `force` compiles despite the checker's
|
|
94
|
+
errors (parse errors still stop it): the escape for documents that rely on defaults the checker flags.
|
|
95
|
+
"""
|
|
96
|
+
source = Path(path).resolve()
|
|
97
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
98
|
+
manifest_paths = [*(manifests or []), *registry_manifests(registry, Path(tmp))]
|
|
99
|
+
diagnostics = check_file(source, manifest_paths)
|
|
100
|
+
fatal = set() if force else {"error", "warning"} if strict else {"error"}
|
|
101
|
+
if any(d.severity in fatal for d in diagnostics) or any(d.code == "P001" for d in diagnostics):
|
|
102
|
+
raise CompileError(diagnostics)
|
|
103
|
+
out = Path(tmp) / ("compiled.yaml" if to == "spec" else "compiled.json")
|
|
104
|
+
args = ["compile", str(source), "--to", to, "--out", str(out), *(["--force"] if force else [])]
|
|
105
|
+
if manifest_paths:
|
|
106
|
+
args += ["--interfaces", *[str(Path(m).resolve()) for m in manifest_paths]]
|
|
107
|
+
run_cli(*args)
|
|
108
|
+
text = out.read_text(encoding="utf-8")
|
|
109
|
+
if to == "spec":
|
|
110
|
+
import yaml
|
|
111
|
+
|
|
112
|
+
return Compiled(yaml.safe_load(text), diagnostics)
|
|
113
|
+
return Compiled(loads(text), diagnostics)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def decompile(
|
|
117
|
+
mapping: dict[str, Any],
|
|
118
|
+
*,
|
|
119
|
+
name: str = "document",
|
|
120
|
+
registry: InterfaceRegistry | None = None,
|
|
121
|
+
manifests: list[Path] | None = None,
|
|
122
|
+
) -> Printed:
|
|
123
|
+
"""Canonical `.pblang` text (and sidecar assets) for a document or a CompositeSpec mapping (told apart by keys)."""
|
|
124
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
125
|
+
source = Path(tmp) / f"{name}.json"
|
|
126
|
+
source.write_text(dumps(mapping), encoding="utf-8")
|
|
127
|
+
out = Path(tmp) / f"{name}.pblang"
|
|
128
|
+
args = ["decompile", str(source), "--name", name, "--out", str(out)]
|
|
129
|
+
manifest_paths = [*(manifests or []), *registry_manifests(registry, Path(tmp))]
|
|
130
|
+
if manifest_paths:
|
|
131
|
+
args += ["--interfaces", *[str(Path(m).resolve()) for m in manifest_paths]]
|
|
132
|
+
run_cli(*args)
|
|
133
|
+
printed = Printed(out.read_text(encoding="utf-8"))
|
|
134
|
+
assets_dir = Path(tmp) / f"{name}.assets"
|
|
135
|
+
if assets_dir.is_dir():
|
|
136
|
+
for asset in sorted(assets_dir.iterdir()):
|
|
137
|
+
printed.assets[f"{name}.assets/{asset.name}"] = asset.read_bytes()
|
|
138
|
+
return printed
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# --- the JSON that crosses the CLI boundary ----------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def dumps(document: Any) -> str:
|
|
145
|
+
"""`bigraph_schema.json_codec` encoding, plus tuples tagged so that they come back as tuples."""
|
|
146
|
+
return str(json_codec.dumps(_tag_tuples(document)))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def loads(text: str) -> Any:
|
|
150
|
+
return _untag_tuples(json_codec.loads(text))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _tag_tuples(value: Any) -> Any:
|
|
154
|
+
if isinstance(value, tuple):
|
|
155
|
+
return {"__tuple__": True, "data": [_tag_tuples(v) for v in value]}
|
|
156
|
+
if isinstance(value, dict):
|
|
157
|
+
return {k: _tag_tuples(v) for k, v in value.items()}
|
|
158
|
+
if isinstance(value, list):
|
|
159
|
+
return [_tag_tuples(v) for v in value]
|
|
160
|
+
return value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _untag_tuples(value: Any) -> Any:
|
|
164
|
+
if isinstance(value, dict):
|
|
165
|
+
if value.get("__tuple__") is True and isinstance(value.get("data"), list):
|
|
166
|
+
return tuple(_untag_tuples(v) for v in value["data"])
|
|
167
|
+
return {k: _untag_tuples(v) for k, v in value.items()}
|
|
168
|
+
if isinstance(value, list):
|
|
169
|
+
return [_untag_tuples(v) for v in value]
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def write_json(document: Any, path: str | os.PathLike[str]) -> None:
|
|
174
|
+
Path(path).write_text(dumps(document) + "\n", encoding="utf-8")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def read_json(path: str | os.PathLike[str]) -> Any:
|
|
178
|
+
return loads(Path(path).read_text(encoding="utf-8"))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
__all__ = [
|
|
182
|
+
"CompileError",
|
|
183
|
+
"Compiled",
|
|
184
|
+
"Printed",
|
|
185
|
+
"compile_file",
|
|
186
|
+
"compile_text",
|
|
187
|
+
"decompile",
|
|
188
|
+
"dumps",
|
|
189
|
+
"loads",
|
|
190
|
+
"read_json",
|
|
191
|
+
"write_json",
|
|
192
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Static checks of a composite, run by the DSL CLI's `check` command (the checker lives in TypeScript,
|
|
3
|
+
`dsl/src/language/v2/check.ts`, so the editor, the browser and the CLI share one implementation).
|
|
4
|
+
|
|
5
|
+
`check_file` runs `cli-native check --json` on a `.pblang` file with header manifests; `check_text` does the same
|
|
6
|
+
for text. Both return the findings of the `pblang-diagnostics/1` envelope as `Diagnostic`s.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from process_bigraph_lang.v2.interfaces import InterfaceRegistry
|
|
19
|
+
from process_bigraph_lang.v2.langium import run_cli
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Diagnostic:
|
|
24
|
+
code: str
|
|
25
|
+
slug: str
|
|
26
|
+
severity: str # error | warning | info | hint
|
|
27
|
+
path: str # document path, slash-joined (as `fill_sites` spells it)
|
|
28
|
+
message: str
|
|
29
|
+
line: int | None = None
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return f"{self.severity:7} {self.code} {self.slug:24} {self.path}: {self.message}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_text(
|
|
36
|
+
text: str, manifests: list[Path] | None = None, registry: InterfaceRegistry | None = None
|
|
37
|
+
) -> list[Diagnostic]:
|
|
38
|
+
"""Check v2 text against header manifests and/or a registry (written as a manifest)."""
|
|
39
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
40
|
+
source = Path(tmp) / "document.pblang"
|
|
41
|
+
source.write_text(text, encoding="utf-8")
|
|
42
|
+
return check_file(source, [*(manifests or []), *registry_manifests(registry, Path(tmp))])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_file(path: str | os.PathLike[str], manifests: list[Path] | None = None) -> list[Diagnostic]:
|
|
46
|
+
"""Check a `.pblang` file against header manifests through the CLI."""
|
|
47
|
+
args = ["check", str(Path(path).resolve()), "--json"]
|
|
48
|
+
if manifests:
|
|
49
|
+
args += ["--interfaces", *[str(Path(m).resolve()) for m in manifests]]
|
|
50
|
+
result = run_cli(*args, check=False)
|
|
51
|
+
if not result.stdout.strip():
|
|
52
|
+
raise RuntimeError(f"Langium CLI failed on {path}: {result.stderr.strip()}")
|
|
53
|
+
envelope: dict[str, Any] = json.loads(result.stdout)
|
|
54
|
+
return [
|
|
55
|
+
Diagnostic(
|
|
56
|
+
d["code"],
|
|
57
|
+
d["slug"],
|
|
58
|
+
d["severity"],
|
|
59
|
+
d["path"],
|
|
60
|
+
d["message"],
|
|
61
|
+
d.get("range", {}).get("start", {}).get("line"),
|
|
62
|
+
)
|
|
63
|
+
for d in envelope.get("diagnostics", [])
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def registry_manifests(registry: InterfaceRegistry | None, directory: Path) -> list[Path]:
|
|
68
|
+
"""A registry written as a manifest file in `directory` (none when the registry is empty)."""
|
|
69
|
+
if registry is None or not registry.decls:
|
|
70
|
+
return []
|
|
71
|
+
manifest = directory / "registry.manifest.json"
|
|
72
|
+
manifest.write_text(json.dumps(registry.to_manifest(), default=str), encoding="utf-8")
|
|
73
|
+
return [manifest]
|