morphir-python 0.0.2__tar.gz → 0.0.3__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.
- {morphir_python-0.0.2 → morphir_python-0.0.3}/.gitignore +6 -0
- {morphir_python-0.0.2 → morphir_python-0.0.3}/PKG-INFO +4 -3
- {morphir_python-0.0.2 → morphir_python-0.0.3}/README.md +1 -1
- {morphir_python-0.0.2 → morphir_python-0.0.3}/pyproject.toml +4 -2
- {morphir_python-0.0.2 → morphir_python-0.0.3}/src/morphir_tools/__init__.py +2 -2
- morphir_python-0.0.3/src/morphir_tools/_output.py +114 -0
- {morphir_python-0.0.2 → morphir_python-0.0.3}/src/morphir_tools/cli.py +176 -2
- morphir_python-0.0.3/src/morphir_tools/python/__init__.py +71 -0
- morphir_python-0.0.3/src/morphir_tools/python/_api.py +215 -0
- morphir_python-0.0.3/src/morphir_tools/python/_bridge.py +84 -0
- morphir_python-0.0.3/src/morphir_tools/python/_types.py +201 -0
- morphir_python-0.0.3/src/morphir_tools/python/diagnostics.py +33 -0
- {morphir_python-0.0.2 → morphir_python-0.0.3}/LICENSE +0 -0
- {morphir_python-0.0.2 → morphir_python-0.0.3}/src/morphir_tools/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: morphir-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: CLI tools and extensions for Morphir Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/finos/morphir-python
|
|
6
6
|
Project-URL: Documentation, https://morphir.finos.org
|
|
@@ -20,7 +20,8 @@ Classifier: Topic :: Software Development :: Code Generators
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
21
21
|
Classifier: Typing :: Typed
|
|
22
22
|
Requires-Python: >=3.14
|
|
23
|
-
Requires-Dist: morphir
|
|
23
|
+
Requires-Dist: morphir-python-native==0.1.0
|
|
24
|
+
Requires-Dist: morphir>=0.0.2
|
|
24
25
|
Requires-Dist: pygments>=2.20.0
|
|
25
26
|
Requires-Dist: typer>=0.15
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
@@ -86,7 +87,7 @@ format-version rules and the draft v4 status.
|
|
|
86
87
|
## Requirements
|
|
87
88
|
|
|
88
89
|
- Python 3.14+
|
|
89
|
-
- `morphir>=0.0.1` (installed as
|
|
90
|
+
- `morphir>=0.0.2` and `morphir-python-native==0.1.0` (installed as dependencies)
|
|
90
91
|
|
|
91
92
|
## License
|
|
92
93
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "morphir-python"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
4
4
|
description = "CLI tools and extensions for Morphir Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -22,7 +22,9 @@ authors = [
|
|
|
22
22
|
{ name = "FINOS", email = "morphir@finos.org" }
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"morphir>=0.0.
|
|
25
|
+
"morphir>=0.0.2",
|
|
26
|
+
# Exact qualified native compiler; checked again when the compiler first loads.
|
|
27
|
+
"morphir-python-native==0.1.0",
|
|
26
28
|
"typer>=0.15",
|
|
27
29
|
# Rich uses Pygments; older versions are affected by CVE-2026-4539.
|
|
28
30
|
"pygments>=2.20.0",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Safe publication of generated files.
|
|
2
|
+
|
|
3
|
+
Generation writes a complete tree into a new sibling staging directory and only
|
|
4
|
+
then moves it into place, so a failure never leaves partial output or touches
|
|
5
|
+
existing user files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path, PurePosixPath
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Iterable, Sequence
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def umask() -> int:
|
|
21
|
+
"""Return the process umask without changing it."""
|
|
22
|
+
mask = os.umask(0o022)
|
|
23
|
+
os.umask(mask)
|
|
24
|
+
return mask
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OutputError(Exception):
|
|
28
|
+
"""Output cannot be published safely; nothing was written."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_artifact_paths(paths: Iterable[str]) -> None:
|
|
32
|
+
"""Check that relative artifact paths stay inside one new directory tree.
|
|
33
|
+
|
|
34
|
+
Each path must be a normalized relative POSIX path. No two paths may name the
|
|
35
|
+
same file on a case-insensitive file system, and no path may be a parent
|
|
36
|
+
directory of another.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
OutputError: For the first invalid path or collision.
|
|
40
|
+
"""
|
|
41
|
+
seen: dict[str, str] = {}
|
|
42
|
+
directories: dict[str, str] = {}
|
|
43
|
+
for path in paths:
|
|
44
|
+
pure = PurePosixPath(path)
|
|
45
|
+
if (
|
|
46
|
+
not path
|
|
47
|
+
or "\\" in path
|
|
48
|
+
or pure.is_absolute()
|
|
49
|
+
or ":" in pure.parts[0]
|
|
50
|
+
or any(part in {"", ".", ".."} for part in path.split("/"))
|
|
51
|
+
):
|
|
52
|
+
raise OutputError(f"artifact path {path!r} is not a safe relative path")
|
|
53
|
+
key = path.casefold()
|
|
54
|
+
if key in seen:
|
|
55
|
+
raise OutputError(f"artifact paths {seen[key]!r} and {path!r} collide")
|
|
56
|
+
seen[key] = path
|
|
57
|
+
for parent in pure.parents:
|
|
58
|
+
if str(parent) != ".":
|
|
59
|
+
directories.setdefault(str(parent).casefold(), path)
|
|
60
|
+
for key, path in seen.items():
|
|
61
|
+
if key in directories:
|
|
62
|
+
raise OutputError(
|
|
63
|
+
f"artifact path {path!r} is also a directory of {directories[key]!r}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_destination(directory: Path) -> None:
|
|
68
|
+
"""Require an absent directory, or an empty real directory.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
OutputError: When the destination is a symbolic link, a file or a
|
|
72
|
+
nonempty directory.
|
|
73
|
+
"""
|
|
74
|
+
if directory.is_symlink():
|
|
75
|
+
raise OutputError(f"output directory {directory} is a symbolic link")
|
|
76
|
+
if directory.exists():
|
|
77
|
+
if not directory.is_dir():
|
|
78
|
+
raise OutputError(f"output path {directory} is not a directory")
|
|
79
|
+
if any(directory.iterdir()):
|
|
80
|
+
raise OutputError(f"output directory {directory} is not empty")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def publish_tree(directory: Path, files: Sequence[tuple[str, str]]) -> None:
|
|
84
|
+
"""Write ``files`` to a staging directory, then move it to ``directory``.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
directory: The absent or empty destination.
|
|
88
|
+
files: Relative POSIX paths and their text, already validated.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
OutputError: When the destination is unsuitable.
|
|
92
|
+
OSError: When writing or moving fails; the staging tree is removed and
|
|
93
|
+
the destination is unchanged.
|
|
94
|
+
"""
|
|
95
|
+
check_destination(directory)
|
|
96
|
+
parent = directory.parent
|
|
97
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
# mkdtemp creates 0700; publish with the existing directory's mode, or the
|
|
99
|
+
# mode an ordinary mkdir would give under the user's umask.
|
|
100
|
+
mode = directory.stat().st_mode & 0o7777 if directory.exists() else 0o777 & ~umask()
|
|
101
|
+
staging = Path(tempfile.mkdtemp(prefix=f".{directory.name}.", dir=parent))
|
|
102
|
+
try:
|
|
103
|
+
staging.chmod(mode)
|
|
104
|
+
for relative, text in files:
|
|
105
|
+
target = staging.joinpath(*relative.split("/"))
|
|
106
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
target.write_text(text, encoding="utf-8")
|
|
108
|
+
check_destination(directory)
|
|
109
|
+
if directory.exists():
|
|
110
|
+
directory.rmdir()
|
|
111
|
+
os.replace(staging, directory)
|
|
112
|
+
except BaseException:
|
|
113
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
114
|
+
raise
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Every IR operation goes through the `morphir` library: the shared format-version
|
|
4
4
|
reader, the v3 (3.0.x and 3.1.x) and draft v4 codecs, and `morphir.ir.migrate`.
|
|
5
|
-
|
|
5
|
+
``compile`` and ``generate`` go through the public `morphir_tools.python` API,
|
|
6
|
+
which loads the native compiler only when one of them runs. The CLI adds file
|
|
7
|
+
handling and reporting only.
|
|
6
8
|
|
|
7
9
|
Exit codes: 0 for success (including success with warnings), 1 when the library
|
|
8
10
|
reports a diagnostic, 2 for a usage error.
|
|
@@ -14,7 +16,7 @@ import os
|
|
|
14
16
|
import tempfile
|
|
15
17
|
from enum import Enum
|
|
16
18
|
from pathlib import Path
|
|
17
|
-
from typing import Annotated
|
|
19
|
+
from typing import TYPE_CHECKING, Annotated
|
|
18
20
|
|
|
19
21
|
import typer
|
|
20
22
|
|
|
@@ -30,6 +32,13 @@ from morphir.ir.v4 import distribution as v4_dist
|
|
|
30
32
|
from morphir.ir.v4.name import Name as Name4
|
|
31
33
|
from morphir.ir.v4.path import Path as Path4
|
|
32
34
|
|
|
35
|
+
from ._output import OutputError, publish_tree, umask, validate_artifact_paths
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from .python import CompilerDiagnostic
|
|
39
|
+
|
|
40
|
+
__all__ = ["OutputError", "app", "main", "validate_artifact_paths"]
|
|
41
|
+
|
|
33
42
|
app = typer.Typer(
|
|
34
43
|
name="morphir-python",
|
|
35
44
|
help="Command-line tools for Morphir IR.",
|
|
@@ -63,6 +72,9 @@ def _read(path: Path) -> str:
|
|
|
63
72
|
except OSError as error:
|
|
64
73
|
typer.echo(f"error: cannot read {path}: {error.strerror}", err=True)
|
|
65
74
|
raise typer.Exit(1) from None
|
|
75
|
+
except UnicodeDecodeError:
|
|
76
|
+
typer.echo(f"error: cannot read {path}: the file is not UTF-8", err=True)
|
|
77
|
+
raise typer.Exit(1) from None
|
|
66
78
|
|
|
67
79
|
|
|
68
80
|
def _write_atomically(target: Path, text: str) -> None:
|
|
@@ -71,6 +83,9 @@ def _write_atomically(target: Path, text: str) -> None:
|
|
|
71
83
|
prefix=f".{target.name}.", suffix=".tmp", dir=directory
|
|
72
84
|
)
|
|
73
85
|
try:
|
|
86
|
+
# mkstemp creates 0600; keep an existing file's mode, or use the umask.
|
|
87
|
+
mode = target.stat().st_mode & 0o7777 if target.exists() else 0o666 & ~umask()
|
|
88
|
+
os.chmod(fd, mode)
|
|
74
89
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
75
90
|
handle.write(text)
|
|
76
91
|
Path(temp_name).replace(target)
|
|
@@ -218,6 +233,165 @@ def convert(
|
|
|
218
233
|
typer.echo(f"wrote {output} (format {to.value})")
|
|
219
234
|
|
|
220
235
|
|
|
236
|
+
def _report_compiler(diagnostics: tuple[CompilerDiagnostic, ...]) -> None:
|
|
237
|
+
for diagnostic in diagnostics:
|
|
238
|
+
typer.echo(f"{diagnostic.severity.value}: {diagnostic.render()}", err=True)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _fail(message: str) -> typer.Exit:
|
|
242
|
+
typer.echo(f"error: {message}", err=True)
|
|
243
|
+
return typer.Exit(1)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _discover(source: Path) -> tuple[Path, list[Path]]:
|
|
247
|
+
"""Return the source root and its Python files, sorted, without following links."""
|
|
248
|
+
if source.is_dir() and not source.is_symlink():
|
|
249
|
+
files: list[Path] = []
|
|
250
|
+
for directory, names, filenames in os.walk(source, followlinks=False):
|
|
251
|
+
names[:] = sorted(
|
|
252
|
+
name
|
|
253
|
+
for name in names
|
|
254
|
+
if not name.startswith(".") and name != "__pycache__"
|
|
255
|
+
)
|
|
256
|
+
files.extend(
|
|
257
|
+
Path(directory) / name
|
|
258
|
+
for name in sorted(filenames)
|
|
259
|
+
if name.endswith(".py") and not (Path(directory) / name).is_symlink()
|
|
260
|
+
)
|
|
261
|
+
if not files:
|
|
262
|
+
raise _fail(f"no Python source files under {source}")
|
|
263
|
+
return source, files
|
|
264
|
+
return source.parent, [source]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@app.command("compile")
|
|
268
|
+
def compile_command(
|
|
269
|
+
source: Annotated[
|
|
270
|
+
Path, typer.Argument(help="A Python file, or a directory of Python files.")
|
|
271
|
+
],
|
|
272
|
+
package: Annotated[
|
|
273
|
+
str, typer.Option(help="Morphir package name, such as acme/example.")
|
|
274
|
+
],
|
|
275
|
+
output: Annotated[
|
|
276
|
+
Path, typer.Option("--output", "-o", help="Where to write the IR document.")
|
|
277
|
+
],
|
|
278
|
+
ir_version: Annotated[
|
|
279
|
+
Target, typer.Option("--ir-version", help="The IR version to write.")
|
|
280
|
+
] = Target.V3,
|
|
281
|
+
expose: Annotated[
|
|
282
|
+
list[str] | None,
|
|
283
|
+
typer.Option(
|
|
284
|
+
"--expose",
|
|
285
|
+
help="Module to make public, such as acme/models. Repeat for more. "
|
|
286
|
+
"Without it every module is public.",
|
|
287
|
+
),
|
|
288
|
+
] = None,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Compile supported Python source to a versioned Morphir IR Library.
|
|
291
|
+
|
|
292
|
+
A directory is compiled as one package; a single file uses its parent
|
|
293
|
+
directory as the source root. Sources are parsed, never imported or run.
|
|
294
|
+
The output is written to a temporary file and renamed only on success, so a
|
|
295
|
+
failed compilation leaves an existing output file unchanged.
|
|
296
|
+
"""
|
|
297
|
+
from .python import ( # Loads the compiler on demand.
|
|
298
|
+
CompiledV3,
|
|
299
|
+
CompileFailure,
|
|
300
|
+
IrVersion,
|
|
301
|
+
NativeCompilerError,
|
|
302
|
+
NativeCompilerUnavailable,
|
|
303
|
+
SourceDocument,
|
|
304
|
+
compile_sources,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
root, files = _discover(source)
|
|
308
|
+
documents = [
|
|
309
|
+
SourceDocument(path.relative_to(root).as_posix(), _read(path)) for path in files
|
|
310
|
+
]
|
|
311
|
+
version = IrVersion.V3 if ir_version is Target.V3 else IrVersion.V4
|
|
312
|
+
try:
|
|
313
|
+
result = compile_sources(
|
|
314
|
+
documents,
|
|
315
|
+
package=package,
|
|
316
|
+
exposed_modules=expose,
|
|
317
|
+
ir_version=version,
|
|
318
|
+
)
|
|
319
|
+
except (ValueError, NativeCompilerUnavailable) as error:
|
|
320
|
+
raise _fail(str(error)) from None
|
|
321
|
+
except NativeCompilerError as error:
|
|
322
|
+
raise _fail(f"internal compiler error: {error}") from None
|
|
323
|
+
if isinstance(result, CompileFailure):
|
|
324
|
+
_report_compiler(result.diagnostics)
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
if isinstance(result, CompiledV3):
|
|
327
|
+
document = v3_codec.encode_versioned_distribution(result.distribution)
|
|
328
|
+
else:
|
|
329
|
+
document = v4_codec.encode_versioned_distribution(result.distribution)
|
|
330
|
+
try:
|
|
331
|
+
_write_atomically(output, fv.dump_json(document))
|
|
332
|
+
except OSError as error:
|
|
333
|
+
raise _fail(f"cannot write {output}: {error.strerror}") from None
|
|
334
|
+
_report_compiler(result.warnings)
|
|
335
|
+
typer.echo(f"wrote {output} (format {ir_version.value})")
|
|
336
|
+
if ir_version is Target.V4:
|
|
337
|
+
typer.echo(f"note: {v4_codec.DRAFT_NOTICE}")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@app.command("generate")
|
|
341
|
+
def generate_command(
|
|
342
|
+
file: Annotated[Path, typer.Argument(help="A versioned morphir-ir.json file.")],
|
|
343
|
+
output_dir: Annotated[
|
|
344
|
+
Path,
|
|
345
|
+
typer.Option(
|
|
346
|
+
"--output-dir", help="A new or empty directory for the Python modules."
|
|
347
|
+
),
|
|
348
|
+
],
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Generate Python modules from a format 3.0 or draft v4 Library.
|
|
351
|
+
|
|
352
|
+
Module paths are preserved under the output directory. All files are written
|
|
353
|
+
to a staging directory first and published together, so a failure leaves no
|
|
354
|
+
partial output. Legacy formats 1 and 2 must be upgraded with ir convert first.
|
|
355
|
+
"""
|
|
356
|
+
from .python import ( # Loads the compiler on demand.
|
|
357
|
+
GenerationFailure,
|
|
358
|
+
NativeCompilerError,
|
|
359
|
+
NativeCompilerUnavailable,
|
|
360
|
+
generate_python,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
text = _read(file)
|
|
364
|
+
try:
|
|
365
|
+
version, _ = fv.read_document(text)
|
|
366
|
+
model: object = (
|
|
367
|
+
v4_codec.decode_versioned_distribution(text)
|
|
368
|
+
if _is_v4(version)
|
|
369
|
+
else v3_codec.decode_versioned_distribution(text)
|
|
370
|
+
)
|
|
371
|
+
except DiagnosticError as error:
|
|
372
|
+
_report(error.diagnostics)
|
|
373
|
+
raise typer.Exit(1) from None
|
|
374
|
+
try:
|
|
375
|
+
result = generate_python(model)
|
|
376
|
+
except NativeCompilerUnavailable as error:
|
|
377
|
+
raise _fail(str(error)) from None
|
|
378
|
+
except NativeCompilerError as error:
|
|
379
|
+
raise _fail(f"internal compiler error: {error}") from None
|
|
380
|
+
if isinstance(result, GenerationFailure):
|
|
381
|
+
_report_compiler(result.diagnostics)
|
|
382
|
+
raise typer.Exit(1)
|
|
383
|
+
files = [(artifact.path, artifact.text) for artifact in result.artifacts]
|
|
384
|
+
try:
|
|
385
|
+
validate_artifact_paths(path for path, _ in files)
|
|
386
|
+
publish_tree(output_dir, files)
|
|
387
|
+
except OutputError as error:
|
|
388
|
+
raise _fail(str(error)) from None
|
|
389
|
+
except OSError as error:
|
|
390
|
+
raise _fail(f"cannot write {output_dir}: {error.strerror}") from None
|
|
391
|
+
_report_compiler(result.warnings)
|
|
392
|
+
typer.echo(f"wrote {len(files)} modules to {output_dir}")
|
|
393
|
+
|
|
394
|
+
|
|
221
395
|
def main() -> None:
|
|
222
396
|
"""Run the ``morphir-python`` command line."""
|
|
223
397
|
app()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
r"""Compile Python to Morphir IR and generate Python from Morphir IR.
|
|
2
|
+
|
|
3
|
+
This is the typed, in-process API behind ``morphir-python compile`` and
|
|
4
|
+
``morphir-python generate``. A native compiler does the work; callers need
|
|
5
|
+
neither Rust nor a separate Morphir executable, and the native compiler loads
|
|
6
|
+
only when one of these functions is first called.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from morphir_tools.python import SourceDocument, compile_sources
|
|
10
|
+
>>> from morphir_tools.python import CompiledV3, generate_python
|
|
11
|
+
>>> source = SourceDocument(
|
|
12
|
+
... "rules.py",
|
|
13
|
+
... "def choose(flag: bool, a: int, b: int) -> int:\n"
|
|
14
|
+
... " return a if flag else b\n",
|
|
15
|
+
... )
|
|
16
|
+
>>> compiled = compile_sources([source], package="acme/example")
|
|
17
|
+
>>> isinstance(compiled, CompiledV3)
|
|
18
|
+
True
|
|
19
|
+
>>> generated = generate_python(compiled.distribution)
|
|
20
|
+
>>> [artifact.path for artifact in generated.artifacts]
|
|
21
|
+
['rules.py']
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from ._api import compile_sources, generate_python, native_compiler
|
|
25
|
+
from ._bridge import EXPECTED_BRIDGE_API, EXPECTED_NATIVE_VERSION
|
|
26
|
+
from ._types import (
|
|
27
|
+
CompiledV3,
|
|
28
|
+
CompiledV4,
|
|
29
|
+
CompileFailure,
|
|
30
|
+
CompilerDiagnostic,
|
|
31
|
+
CompileResult,
|
|
32
|
+
Generated,
|
|
33
|
+
GeneratedArtifact,
|
|
34
|
+
GenerateResult,
|
|
35
|
+
GenerationFailure,
|
|
36
|
+
IrVersion,
|
|
37
|
+
LibraryModel,
|
|
38
|
+
NativeCompilerError,
|
|
39
|
+
NativeCompilerInfo,
|
|
40
|
+
NativeCompilerUnavailable,
|
|
41
|
+
Severity,
|
|
42
|
+
SourceDocument,
|
|
43
|
+
SourceLocation,
|
|
44
|
+
SourcePosition,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"EXPECTED_BRIDGE_API",
|
|
49
|
+
"EXPECTED_NATIVE_VERSION",
|
|
50
|
+
"CompileFailure",
|
|
51
|
+
"CompileResult",
|
|
52
|
+
"CompiledV3",
|
|
53
|
+
"CompiledV4",
|
|
54
|
+
"CompilerDiagnostic",
|
|
55
|
+
"GenerateResult",
|
|
56
|
+
"Generated",
|
|
57
|
+
"GeneratedArtifact",
|
|
58
|
+
"GenerationFailure",
|
|
59
|
+
"IrVersion",
|
|
60
|
+
"LibraryModel",
|
|
61
|
+
"NativeCompilerError",
|
|
62
|
+
"NativeCompilerInfo",
|
|
63
|
+
"NativeCompilerUnavailable",
|
|
64
|
+
"Severity",
|
|
65
|
+
"SourceDocument",
|
|
66
|
+
"SourceLocation",
|
|
67
|
+
"SourcePosition",
|
|
68
|
+
"compile_sources",
|
|
69
|
+
"generate_python",
|
|
70
|
+
"native_compiler",
|
|
71
|
+
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Compile Python sources to Morphir IR and generate Python from IR."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
|
+
|
|
8
|
+
from morphir.ir import format_version as fv
|
|
9
|
+
from morphir.ir.diagnostics import DiagnosticError
|
|
10
|
+
from morphir.ir.v3 import codec as v3_codec
|
|
11
|
+
from morphir.ir.v3 import distribution as v3_dist
|
|
12
|
+
from morphir.ir.v4 import codec as v4_codec
|
|
13
|
+
from morphir.ir.v4 import distribution as v4_dist
|
|
14
|
+
|
|
15
|
+
from . import _bridge
|
|
16
|
+
from . import diagnostics as codes
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
20
|
+
|
|
21
|
+
from ._types import (
|
|
22
|
+
CompiledV3,
|
|
23
|
+
CompiledV4,
|
|
24
|
+
CompileFailure,
|
|
25
|
+
CompilerDiagnostic,
|
|
26
|
+
CompileResult,
|
|
27
|
+
Generated,
|
|
28
|
+
GeneratedArtifact,
|
|
29
|
+
GenerateResult,
|
|
30
|
+
GenerationFailure,
|
|
31
|
+
IrVersion,
|
|
32
|
+
LibraryModel,
|
|
33
|
+
NativeCompilerError,
|
|
34
|
+
NativeCompilerInfo,
|
|
35
|
+
Severity,
|
|
36
|
+
SourceDocument,
|
|
37
|
+
SourceLocation,
|
|
38
|
+
SourcePosition,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def native_compiler() -> NativeCompilerInfo:
|
|
43
|
+
"""Load the native compiler and report its identity.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
NativeCompilerUnavailable: When it is missing or incompatible.
|
|
47
|
+
"""
|
|
48
|
+
return _bridge.load()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def compile_sources(
|
|
52
|
+
sources: Iterable[SourceDocument],
|
|
53
|
+
*,
|
|
54
|
+
package: str,
|
|
55
|
+
exposed_modules: Sequence[str] | None = None,
|
|
56
|
+
ir_version: IrVersion = IrVersion.V3,
|
|
57
|
+
) -> CompileResult:
|
|
58
|
+
"""Compile Python source documents to a Morphir IR Library.
|
|
59
|
+
|
|
60
|
+
The sources are parsed statically: nothing is imported or executed. The
|
|
61
|
+
order of ``sources`` does not affect the result.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
sources: The documents of one package, with paths relative to its root.
|
|
65
|
+
package: The Morphir package name, such as ``"acme/example"``.
|
|
66
|
+
exposed_modules: Module paths to make public, such as
|
|
67
|
+
``("acme/models",)``. ``None`` exposes every module; an empty
|
|
68
|
+
sequence exposes none.
|
|
69
|
+
ir_version: The IR version to write. Defaults to stable v3.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A `CompiledV3` or `CompiledV4` holding the decoded Library, or a
|
|
73
|
+
`CompileFailure` with at least one diagnostic and no model.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: When ``package`` is empty or two documents share a path.
|
|
77
|
+
NativeCompilerUnavailable: When the native compiler is missing or
|
|
78
|
+
incompatible.
|
|
79
|
+
NativeCompilerError: When the native compiler fails internally.
|
|
80
|
+
"""
|
|
81
|
+
documents = sorted(sources, key=lambda document: document.path)
|
|
82
|
+
if not package:
|
|
83
|
+
raise ValueError("package must name a Morphir package")
|
|
84
|
+
paths = [document.path for document in documents]
|
|
85
|
+
if len(set(paths)) != len(paths):
|
|
86
|
+
raise ValueError("source documents contain a duplicate path")
|
|
87
|
+
request = {
|
|
88
|
+
"package": package,
|
|
89
|
+
"sources": [{"path": d.path, "text": d.text} for d in documents],
|
|
90
|
+
"exposedModules": None if exposed_modules is None else list(exposed_modules),
|
|
91
|
+
"irVersion": ir_version.value,
|
|
92
|
+
}
|
|
93
|
+
output = _call(_bridge.module().compile, json.dumps(request))
|
|
94
|
+
diagnostics = _diagnostics(output)
|
|
95
|
+
ir = output.get("ir")
|
|
96
|
+
if not output.get("success") or not isinstance(ir, str):
|
|
97
|
+
return CompileFailure(diagnostics or (_missing_diagnostic(),))
|
|
98
|
+
try:
|
|
99
|
+
if ir_version is IrVersion.V3:
|
|
100
|
+
classic = v3_codec.decode_versioned_distribution(ir)
|
|
101
|
+
if isinstance(classic, v3_dist.Library):
|
|
102
|
+
return CompiledV3(classic, diagnostics)
|
|
103
|
+
else:
|
|
104
|
+
draft = v4_codec.decode_versioned_distribution(ir)
|
|
105
|
+
if isinstance(draft, v4_dist.LibraryDistribution):
|
|
106
|
+
return CompiledV4(draft, diagnostics)
|
|
107
|
+
except DiagnosticError as error:
|
|
108
|
+
return CompileFailure(_ir_diagnostics(error))
|
|
109
|
+
raise NativeCompilerError("the native compiler returned a non-Library model")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def generate_python(distribution: LibraryModel | object) -> GenerateResult:
|
|
113
|
+
"""Generate Python modules from a v3 or draft v4 Library model.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
distribution: A `morphir.ir.v3.distribution.Library` or
|
|
117
|
+
`morphir.ir.v4.distribution.LibraryDistribution`. Specs and
|
|
118
|
+
Application distributions are rejected with a diagnostic.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
`Generated` with one artifact per module, in path order, or a
|
|
122
|
+
`GenerationFailure` with at least one diagnostic and no artifacts.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
NativeCompilerUnavailable: When the native compiler is missing or
|
|
126
|
+
incompatible.
|
|
127
|
+
NativeCompilerError: When the native compiler fails internally.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
match distribution:
|
|
131
|
+
case v3_dist.Library():
|
|
132
|
+
document = v3_codec.encode_versioned_distribution(distribution)
|
|
133
|
+
case v4_dist.LibraryDistribution():
|
|
134
|
+
document = v4_codec.encode_versioned_distribution(distribution)
|
|
135
|
+
case _:
|
|
136
|
+
kind = type(distribution).__name__
|
|
137
|
+
return GenerationFailure(
|
|
138
|
+
(
|
|
139
|
+
CompilerDiagnostic(
|
|
140
|
+
codes.UNSUPPORTED_DISTRIBUTION,
|
|
141
|
+
Severity.ERROR,
|
|
142
|
+
"Python generation needs a Library distribution, "
|
|
143
|
+
f"not {kind}",
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
except DiagnosticError as error:
|
|
148
|
+
return GenerationFailure(_ir_diagnostics(error))
|
|
149
|
+
output = _call(_bridge.module().generate, fv.dump_json(document))
|
|
150
|
+
diagnostics = _diagnostics(output)
|
|
151
|
+
raw = output.get("artifacts")
|
|
152
|
+
if not output.get("success") or not isinstance(raw, list):
|
|
153
|
+
return GenerationFailure(diagnostics or (_missing_diagnostic(),))
|
|
154
|
+
artifacts = tuple(
|
|
155
|
+
sorted(
|
|
156
|
+
(
|
|
157
|
+
GeneratedArtifact(str(item["path"]), str(item["content"]))
|
|
158
|
+
for item in cast("list[Mapping[str, object]]", raw)
|
|
159
|
+
),
|
|
160
|
+
key=lambda artifact: artifact.path,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
return Generated(artifacts, diagnostics)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _call(function: Callable[[str], str], argument: str) -> Mapping[str, object]:
|
|
167
|
+
output = cast("Mapping[str, object]", json.loads(function(argument)))
|
|
168
|
+
internal = output.get("internalError")
|
|
169
|
+
if internal is not None:
|
|
170
|
+
raise NativeCompilerError(str(internal))
|
|
171
|
+
return output
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _missing_diagnostic() -> CompilerDiagnostic:
|
|
175
|
+
return CompilerDiagnostic(
|
|
176
|
+
codes.PY_SERIALIZATION,
|
|
177
|
+
Severity.ERROR,
|
|
178
|
+
"the native compiler failed without a diagnostic",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _diagnostics(output: Mapping[str, object]) -> tuple[CompilerDiagnostic, ...]:
|
|
183
|
+
items = cast("list[Mapping[str, object]]", output.get("diagnostics") or [])
|
|
184
|
+
return tuple(_diagnostic(item) for item in items)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _diagnostic(item: Mapping[str, object]) -> CompilerDiagnostic:
|
|
188
|
+
location = cast("Mapping[str, object] | None", item.get("location"))
|
|
189
|
+
source = None
|
|
190
|
+
if location is not None:
|
|
191
|
+
start = cast("Mapping[str, int]", location["start"])
|
|
192
|
+
end = cast("Mapping[str, int]", location["end"])
|
|
193
|
+
source = SourceLocation(
|
|
194
|
+
str(location["path"]),
|
|
195
|
+
SourcePosition(start["line"], start["character"]),
|
|
196
|
+
SourcePosition(end["line"], end["character"]),
|
|
197
|
+
)
|
|
198
|
+
return CompilerDiagnostic(
|
|
199
|
+
code=str(item.get("code") or codes.PY_SERIALIZATION),
|
|
200
|
+
severity=Severity(str(item["severity"])),
|
|
201
|
+
message=str(item["message"]),
|
|
202
|
+
source=source,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _ir_diagnostics(error: DiagnosticError) -> tuple[CompilerDiagnostic, ...]:
|
|
207
|
+
return tuple(
|
|
208
|
+
CompilerDiagnostic(
|
|
209
|
+
code=item.code,
|
|
210
|
+
severity=Severity.ERROR,
|
|
211
|
+
message=item.message,
|
|
212
|
+
ir_path=item.location,
|
|
213
|
+
)
|
|
214
|
+
for item in error.diagnostics
|
|
215
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Lazy loading and compatibility checks for the private native compiler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
from importlib import import_module, metadata
|
|
7
|
+
from typing import Protocol, cast
|
|
8
|
+
|
|
9
|
+
from ._types import NativeCompilerInfo, NativeCompilerUnavailable
|
|
10
|
+
|
|
11
|
+
EXPECTED_NATIVE_VERSION = "0.1.0"
|
|
12
|
+
"""The exact ``morphir-python-native`` version this release was qualified with.
|
|
13
|
+
|
|
14
|
+
It must equal the pin in this distribution's ``pyproject.toml``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
EXPECTED_BRIDGE_API = 1
|
|
18
|
+
"""The bridge contract revision this release speaks."""
|
|
19
|
+
|
|
20
|
+
_DISTRIBUTION = "morphir-python-native"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NativeModule(Protocol):
|
|
24
|
+
"""The private ``morphir_python_native._native`` module."""
|
|
25
|
+
|
|
26
|
+
BRIDGE_API: int
|
|
27
|
+
COMPILER_VERSION: str
|
|
28
|
+
__version__: str
|
|
29
|
+
|
|
30
|
+
def compile(self, request: str) -> str:
|
|
31
|
+
"""Compile a JSON request."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def generate(self, ir: str) -> str:
|
|
35
|
+
"""Generate Python from IR JSON text."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _unavailable(detail: str) -> NativeCompilerUnavailable:
|
|
40
|
+
return NativeCompilerUnavailable(
|
|
41
|
+
f"the Morphir Python compiler is unavailable: {detail}. Reinstall "
|
|
42
|
+
f"morphir-python, which requires {_DISTRIBUTION}=={EXPECTED_NATIVE_VERSION}."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@functools.cache
|
|
47
|
+
def load() -> NativeCompilerInfo:
|
|
48
|
+
"""Import the native module once and check that this release can use it.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
NativeCompilerUnavailable: When the module is missing, or its
|
|
52
|
+
distribution version, module version or bridge revision differ from
|
|
53
|
+
what this release expects.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
installed = metadata.version(_DISTRIBUTION)
|
|
57
|
+
except metadata.PackageNotFoundError:
|
|
58
|
+
raise _unavailable(f"{_DISTRIBUTION} is not installed") from None
|
|
59
|
+
try:
|
|
60
|
+
native = cast("NativeModule", import_module("morphir_python_native._native"))
|
|
61
|
+
except ImportError as error:
|
|
62
|
+
raise _unavailable(f"its native module cannot be imported ({error})") from None
|
|
63
|
+
if installed != EXPECTED_NATIVE_VERSION:
|
|
64
|
+
raise _unavailable(
|
|
65
|
+
f"{_DISTRIBUTION} {installed} is installed but this release needs "
|
|
66
|
+
f"{EXPECTED_NATIVE_VERSION}"
|
|
67
|
+
)
|
|
68
|
+
if native.__version__ != installed:
|
|
69
|
+
raise _unavailable(
|
|
70
|
+
f"the native module reports {native.__version__} but its distribution "
|
|
71
|
+
f"is {installed}"
|
|
72
|
+
)
|
|
73
|
+
if native.BRIDGE_API != EXPECTED_BRIDGE_API:
|
|
74
|
+
raise _unavailable(
|
|
75
|
+
f"bridge revision {native.BRIDGE_API} differs from the expected "
|
|
76
|
+
f"{EXPECTED_BRIDGE_API}"
|
|
77
|
+
)
|
|
78
|
+
return NativeCompilerInfo(installed, native.BRIDGE_API, native.COMPILER_VERSION)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def module() -> NativeModule:
|
|
82
|
+
"""Return the checked native module, loading it on first use."""
|
|
83
|
+
load()
|
|
84
|
+
return cast("NativeModule", import_module("morphir_python_native._native"))
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Immutable inputs and results of the Python compiler API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from morphir.ir.v3 import distribution as v3_dist
|
|
9
|
+
from morphir.ir.v4 import distribution as v4_dist
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IrVersion(Enum):
|
|
13
|
+
"""The Morphir IR version a compilation writes."""
|
|
14
|
+
|
|
15
|
+
V3 = "3"
|
|
16
|
+
"""Stable format 3.0.0."""
|
|
17
|
+
V4 = "4"
|
|
18
|
+
"""Draft format 4.0.0."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Severity(Enum):
|
|
22
|
+
"""How serious a compiler diagnostic is."""
|
|
23
|
+
|
|
24
|
+
ERROR = "error"
|
|
25
|
+
WARNING = "warning"
|
|
26
|
+
INFO = "info"
|
|
27
|
+
HINT = "hint"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class SourceDocument:
|
|
32
|
+
"""One Python source file to compile.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
path: Path relative to the source root, with ``/`` separators, such as
|
|
36
|
+
``"acme/rules.py"``. It decides the Morphir module name.
|
|
37
|
+
text: The complete source text. It is parsed, never imported or run.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
path: str
|
|
41
|
+
text: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class SourcePosition:
|
|
46
|
+
"""A zero-based line and UTF-16 character offset in a source document."""
|
|
47
|
+
|
|
48
|
+
line: int
|
|
49
|
+
character: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class SourceLocation:
|
|
54
|
+
"""A half-open range in one source document."""
|
|
55
|
+
|
|
56
|
+
path: str
|
|
57
|
+
start: SourcePosition
|
|
58
|
+
end: SourcePosition
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class CompilerDiagnostic:
|
|
63
|
+
"""A problem reported by compilation or generation.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
code: Stable code from `morphir_tools.python.diagnostics` or
|
|
67
|
+
`morphir.ir.diagnostics`.
|
|
68
|
+
severity: How serious the problem is.
|
|
69
|
+
message: Human-readable explanation. Callers must not parse it.
|
|
70
|
+
source: The source range, when the problem is in a source document.
|
|
71
|
+
ir_path: Path from the IR document root, when the problem is in IR.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
code: str
|
|
75
|
+
severity: Severity
|
|
76
|
+
message: str
|
|
77
|
+
source: SourceLocation | None = None
|
|
78
|
+
ir_path: tuple[str | int, ...] = ()
|
|
79
|
+
|
|
80
|
+
def render(self) -> str:
|
|
81
|
+
"""Render as ``code at location: message``, like IR diagnostics.
|
|
82
|
+
|
|
83
|
+
Source locations are written ``path:line:column`` with one-based line and
|
|
84
|
+
column numbers.
|
|
85
|
+
"""
|
|
86
|
+
if self.source is not None:
|
|
87
|
+
start = self.source.start
|
|
88
|
+
where = f"{self.source.path}:{start.line + 1}:{start.character + 1}"
|
|
89
|
+
elif self.ir_path:
|
|
90
|
+
where = "/".join(str(part) for part in self.ir_path)
|
|
91
|
+
else:
|
|
92
|
+
where = "<request>"
|
|
93
|
+
return f"{self.code} at {where}: {self.message}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _warnings_only(diagnostics: tuple[CompilerDiagnostic, ...]) -> None:
|
|
97
|
+
if any(item.severity is Severity.ERROR for item in diagnostics):
|
|
98
|
+
raise ValueError("a successful result cannot carry error diagnostics")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _not_empty(diagnostics: tuple[CompilerDiagnostic, ...]) -> None:
|
|
102
|
+
if not diagnostics:
|
|
103
|
+
raise ValueError("a failure needs at least one diagnostic")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class CompiledV3:
|
|
108
|
+
"""A successful compilation to a format 3.0.0 Library."""
|
|
109
|
+
|
|
110
|
+
distribution: v3_dist.Library
|
|
111
|
+
warnings: tuple[CompilerDiagnostic, ...] = ()
|
|
112
|
+
|
|
113
|
+
def __post_init__(self) -> None:
|
|
114
|
+
_warnings_only(self.warnings)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class CompiledV4:
|
|
119
|
+
"""A successful compilation to a draft format 4.0.0 Library."""
|
|
120
|
+
|
|
121
|
+
distribution: v4_dist.LibraryDistribution
|
|
122
|
+
warnings: tuple[CompilerDiagnostic, ...] = ()
|
|
123
|
+
|
|
124
|
+
def __post_init__(self) -> None:
|
|
125
|
+
_warnings_only(self.warnings)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class CompileFailure:
|
|
130
|
+
"""A compilation that produced no model."""
|
|
131
|
+
|
|
132
|
+
diagnostics: tuple[CompilerDiagnostic, ...]
|
|
133
|
+
|
|
134
|
+
def __post_init__(self) -> None:
|
|
135
|
+
_not_empty(self.diagnostics)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
type CompileResult = CompiledV3 | CompiledV4 | CompileFailure
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class GeneratedArtifact:
|
|
143
|
+
"""One generated Python module.
|
|
144
|
+
|
|
145
|
+
Attributes:
|
|
146
|
+
path: Path relative to the output root, with ``/`` separators.
|
|
147
|
+
text: The module source.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
path: str
|
|
151
|
+
text: str
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class Generated:
|
|
156
|
+
"""Successful generation: every module of the input Library."""
|
|
157
|
+
|
|
158
|
+
artifacts: tuple[GeneratedArtifact, ...]
|
|
159
|
+
warnings: tuple[CompilerDiagnostic, ...] = ()
|
|
160
|
+
|
|
161
|
+
def __post_init__(self) -> None:
|
|
162
|
+
_warnings_only(self.warnings)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class GenerationFailure:
|
|
167
|
+
"""A generation that produced no artifacts."""
|
|
168
|
+
|
|
169
|
+
diagnostics: tuple[CompilerDiagnostic, ...]
|
|
170
|
+
|
|
171
|
+
def __post_init__(self) -> None:
|
|
172
|
+
_not_empty(self.diagnostics)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
type GenerateResult = Generated | GenerationFailure
|
|
176
|
+
|
|
177
|
+
type LibraryModel = v3_dist.Library | v4_dist.LibraryDistribution
|
|
178
|
+
"""A Library model that generation accepts."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class NativeCompilerInfo:
|
|
183
|
+
"""Identity of the loaded native compiler.
|
|
184
|
+
|
|
185
|
+
Attributes:
|
|
186
|
+
version: Version of the ``morphir-python-native`` distribution and module.
|
|
187
|
+
bridge_api: Revision of the private bridge contract.
|
|
188
|
+
compiler_version: Version of the linked ``morphir-python-binding`` crate.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
version: str
|
|
192
|
+
bridge_api: int
|
|
193
|
+
compiler_version: str
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class NativeCompilerUnavailable(ImportError): # noqa: N818 - reads as a state
|
|
197
|
+
"""The native compiler is missing or incompatible with this release."""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class NativeCompilerError(RuntimeError):
|
|
201
|
+
"""The native compiler failed internally; this is a bug, not a user error."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Diagnostic codes reported by the Python compiler API.
|
|
2
|
+
|
|
3
|
+
Every code the API can report lives in this module, as `morphir.ir.diagnostics`
|
|
4
|
+
does for the IR readers and codecs.
|
|
5
|
+
|
|
6
|
+
- ``PY001`` to ``PY006`` come from the native ``morphir-python-binding`` compiler.
|
|
7
|
+
They are the same codes its MEP extension reports, so Rust, WASM and Python
|
|
8
|
+
callers see one vocabulary.
|
|
9
|
+
- ``unsupported_distribution`` is local to this API: generation accepts only a
|
|
10
|
+
Library distribution.
|
|
11
|
+
- Decoding and encoding failures keep the `morphir.ir.diagnostics` code that the
|
|
12
|
+
codec reported, such as ``unknown_node`` or ``no_v4_spelling``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
# Native compiler (morphir-python-binding).
|
|
18
|
+
PY_REQUEST = "PY001"
|
|
19
|
+
"""Unsupported request or option."""
|
|
20
|
+
PY_SYNTAX = "PY002"
|
|
21
|
+
"""Python syntax error, with a source range."""
|
|
22
|
+
PY_NAME = "PY003"
|
|
23
|
+
"""Invalid name or naming collision."""
|
|
24
|
+
PY_UNSUPPORTED = "PY004"
|
|
25
|
+
"""Unsupported declaration, annotation or IR feature."""
|
|
26
|
+
PY_SERIALIZATION = "PY005"
|
|
27
|
+
"""IR decoding or serialization, or generated syntax failure."""
|
|
28
|
+
PY_PARSE_STAGE = "PY006"
|
|
29
|
+
"""Parse-stage output was requested but is unavailable."""
|
|
30
|
+
|
|
31
|
+
# Python API (morphir-python).
|
|
32
|
+
UNSUPPORTED_DISTRIBUTION = "unsupported_distribution"
|
|
33
|
+
"""Generation received a Specs or Application distribution."""
|
|
File without changes
|
|
File without changes
|