Source code for simvx.core.scene_io.syntax
"""How the scene editor refuses source it cannot read.
**There is no subset any more.** The editor reads and rewrites a scene through
CPython's own parser (:mod:`simvx.core.scene_io.source`), so the Python it
accepts is exactly the Python that runs: a ``match``, a parenthesized ``with``,
an ``except*``, a ``type`` alias, a t-string or a PEP 758 ``except A, B:`` loads,
opens, edits and saves like anything else. What this module used to hold -- eight
documented constructs, a classifier that named them, and the per-statement
refusal that stopped an edit from rewriting lines inside one -- is gone with the
parser that needed it, because a file the reader cannot parse now has no
statements to refuse an edit over: it does not open at all.
What is left is the one refusal that was always honest. Source that will not
compile is refused at load, in CPython's own words, with its line and its caret
column, because a file that does not compile does not run either and there is
nothing to show a reader of it.
:class:`UnsupportedSceneSyntaxError` keeps its name and its shape. Every editor
call site catches it, and the name is the channel rather than a claim about
which constructs are readable.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, NamedTuple
if TYPE_CHECKING:
from .source import SceneSyntaxError
__all__ = [
"SyntaxIssue",
"UnsupportedSceneSyntaxError",
"refusal",
]
[docs]
class UnsupportedSceneSyntaxError(ValueError):
"""The scene holds source the parser cannot read, so it will not open.
Raised by :meth:`~simvx.core.scene_io.SceneFile.load` and
:meth:`~simvx.core.scene_io.SceneFile.from_source` over source CPython
itself refuses. The message is the parser's own, so the author is told what
is wrong with their file rather than what this layer's limits are.
:attr:`issues` carries the parse issues behind the refusal, first one
first, for a caller that wants to place a marker rather than show a
message.
"""
def __init__(self, message: str, issues: list[SyntaxIssue]) -> None:
self.issues: list[SyntaxIssue] = list(issues)
super().__init__(message)
[docs]
class SyntaxIssue(NamedTuple):
"""One place the parser could not read, as it reported it."""
#: 1-indexed line, matching the editor's cursor convention.
line: int
#: 0-indexed column, which is where the caret was pointing.
column: int
#: Always ``None``. Kept so a caller reading a refusal's issues sees the
#: shape it has always had: nothing names a construct now, because the
#: parser accepts every construct the language has and a file that fails it
#: is malformed rather than merely modern.
construct: str | None
#: The parser's own message, which is what a reader needs told.
message: str
[docs]
def describe(self) -> str:
"""One line naming what is wrong and where it is, for a refusal."""
what = self.construct or "source that does not parse"
return f"{what} on line {self.line}"
[docs]
def refusal(error: SceneSyntaxError, *, subject: str) -> UnsupportedSceneSyntaxError:
"""Present a parse failure as the scene layer's refusal to open ``subject``.
``subject`` is what holds the source (a path, ``"this source"``). The
parser's message, line and caret column cross unedited: they are what the
author needs in order to fix the file, and nothing here knows better.
"""
line = 1 if error.line is None else error.line
issue = SyntaxIssue(line=line, column=error.column or 0, construct=None, message=error.message)
return UnsupportedSceneSyntaxError(
f"{subject} does not parse: {issue.message} at line {issue.line}, column {issue.column}",
[issue],
)