Skip to content

Commit a310ec5

Browse files
committed
core, docutils: Revamp error reporting in the SerAPI module
* alectryon/core.py (SerAPI): Add `observer` field. (SerAPI._warn_on_exn): Call `observer.notify()`. (SerAPI.annotate): New function (uses `self.observer`). * alectryon/docutils.py (AlectryonTransform.annotate_cached): Call SerAPI with `observer` set to instance of new `DocutilsObserver` class.
1 parent c779def commit a310ec5

File tree

4 files changed

+73
-34
lines changed

4 files changed

+73
-34
lines changed

alectryon/cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ def rst_to_coq(coq, fpath, point, marker):
6262
return _catch_parsing_errors(fpath, rst2coq_marked, coq, point, marker)
6363

6464
def annotate_chunks(chunks, fpath, cache_directory, cache_compression, sertop_args):
65-
from .core import SerAPI, annotate
65+
from .core import SerAPI
6666
from .json import Cache
67+
api = SerAPI(sertop_args)
6768
metadata = {"sertop_args": sertop_args}
6869
cache = Cache(cache_directory, fpath, metadata, cache_compression)
69-
return cache.update(chunks, lambda c: annotate(c, sertop_args), SerAPI.version_info())
70+
return cache.update(chunks, api.annotate, SerAPI.version_info())
7071

7172
def register_docutils(v, ctx):
7273
from . import docutils

alectryon/core.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
2020

21-
from typing import Any, Iterator, List, Tuple, Union
21+
from typing import Any, Iterator, List, Tuple, Union, NamedTuple, TypeVar, Generic
2222

2323
from collections import namedtuple, defaultdict
2424
from shlex import quote
@@ -144,16 +144,24 @@ def __init__(self, _s, pos, col_offset):
144144
super().__init__()
145145
self.pos, self.col_offset = pos, col_offset
146146

147-
class PosView(bytes):
147+
class View(bytes):
148+
def __getitem__(self, key):
149+
return memoryview(self).__getitem__(key)
150+
151+
def __init__(self, s):
152+
super().__init__()
153+
self.s = s
154+
155+
class PosView(View):
148156
NL = b"\n"
149157

150158
def __new__(cls, s):
151159
bs = s.encode("utf-8")
152160
# https://stackoverflow.com/questions/20221858/
153-
return super().__new__(cls, bs) if isinstance(s, PosStr) else memoryview(bs)
161+
return super().__new__(cls, bs) if isinstance(s, PosStr) else View(bs)
154162

155163
def __init__(self, s):
156-
super().__init__()
164+
super().__init__(s)
157165
self.pos, self.col_offset = s.pos, s.col_offset
158166

159167
def __getitem__(self, key):
@@ -183,6 +191,27 @@ def translate_span(self, beg, end):
183191
return Range(self.translate_offset(beg),
184192
self.translate_offset(end))
185193

194+
_T = TypeVar("_T")
195+
class Notification(NamedTuple, Generic[_T]): # type: ignore
196+
obj: _T
197+
message: str
198+
location: Range
199+
level: int
200+
201+
class Observer:
202+
def _notify(self, n: Notification):
203+
raise NotImplementedError()
204+
205+
def notify(self, obj, message, location, level):
206+
self._notify(Notification(obj, message, location, level))
207+
208+
class StderrObserver(Observer):
209+
def _notify(self, n: Notification):
210+
header = n.location.as_header() if n.location else "!!"
211+
message = n.message.rstrip().replace("\n", "\n ")
212+
level_name = {2: "WARNING", 3: "ERROR"}.get(n.level, "??")
213+
stderr.write("{} ({}/{}) {}\n".format(header, level_name, n.level, message))
214+
186215
PrettyPrinted = namedtuple("PrettyPrinted", "sid pp")
187216

188217
def sexp_hd(sexp):
@@ -221,12 +250,13 @@ def version_info(sertop_bin=SERTOP_BIN):
221250
def __init__(self, args=(), # pylint: disable=dangerous-default-value
222251
sertop_bin=SERTOP_BIN,
223252
pp_args=DEFAULT_PP_ARGS):
224-
"""Configure and start a ``sertop`` instance."""
253+
"""Configure a ``sertop`` instance."""
225254
self.args, self.sertop_bin = [*args, *SerAPI.DEFAULT_ARGS], sertop_bin
226255
self.sertop = None
227256
self.next_qid = 0
228257
self.pp_args = {**SerAPI.DEFAULT_PP_ARGS, **pp_args}
229258
self.last_response = None
259+
self.observer : Observer = StderrObserver()
230260

231261
def __enter__(self):
232262
self.reset()
@@ -375,15 +405,7 @@ def _clip_span(loc, chunk):
375405
def _range_of_span(span, chunk):
376406
return chunk.translate_span(*span) if isinstance(chunk, PosView) else None
377407

378-
@staticmethod
379-
def _print_warning(msg, loc):
380-
header = loc.as_header() if loc else "!!"
381-
msg = msg.rstrip().replace("\n", "\n ")
382-
stderr.write("{} (WARNING/2) {}\n".format(header, msg))
383-
# FIXME report this error to calling context
384-
385-
@staticmethod
386-
def _warn_on_exn(response, chunk):
408+
def _warn_on_exn(self, response, chunk):
387409
QUOTE = ' > '
388410
ERR_FMT = "Coq raised an exception:\n{}"
389411
msg = sx.tostr(response.exn)
@@ -392,7 +414,7 @@ def _warn_on_exn(response, chunk):
392414
if chunk:
393415
err += "\n" + SerAPI._highlight_exn(span, chunk, prefix=QUOTE)
394416
err += "\n" + "Results past this point may be unreliable."
395-
SerAPI._print_warning(err, SerAPI._range_of_span(span, chunk))
417+
self.observer.notify(chunk.s, err, SerAPI._range_of_span(span, chunk), level=3)
396418

397419
def _collect_messages(self, typs: Tuple[type, ...], chunk, sid) -> Iterator[Any]:
398420
warn_on_exn = ApiExn not in typs
@@ -404,7 +426,7 @@ def _collect_messages(self, typs: Tuple[type, ...], chunk, sid) -> Iterator[Any]
404426
return
405427
if warn_on_exn and isinstance(response, ApiExn):
406428
if sid is None or response.sids is None or sid in response.sids:
407-
SerAPI._warn_on_exn(response, chunk)
429+
self._warn_on_exn(response, chunk)
408430
if (not typs) or isinstance(response, typs): # type: ignore
409431
yield response
410432

@@ -498,11 +520,16 @@ def run(self, chunk):
498520
if fragment is None:
499521
err = "Orphaned message for sid {}:".format(message.sid)
500522
err += "\n" + indent(message.pp, " > ")
501-
SerAPI._print_warning(err, SerAPI._range_of_span((0, len(chunk)), chunk))
523+
err_range = SerAPI._range_of_span((0, len(chunk)), chunk)
524+
self.observer.notify(chunk.s, err, err_range, level=2)
502525
else:
503526
fragment.messages.append(Message(message.pp))
504527
return fragments
505528

529+
def annotate(self, chunks):
530+
with self as api:
531+
return [api.run(chunk) for chunk in chunks]
532+
506533
def annotate(chunks, sertop_args=()):
507534
r"""Annotate multiple `chunks` of Coq code.
508535
@@ -514,5 +541,4 @@ def annotate(chunks, sertop_args=()):
514541
>>> annotate(["Check 1."])
515542
[[Sentence(contents='Check 1.', messages=[Message(contents='1\n : nat')], goals=[])]]
516543
"""
517-
with SerAPI(args=sertop_args) as api:
518-
return [api.run(chunk) for chunk in chunks]
544+
return SerAPI(args=sertop_args).annotate(chunks)

alectryon/docutils.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
from docutils.writers import html4css1, html5_polyglot, latex2e, xetex
8888

8989
from . import core, transforms, html, latex, markers
90-
from .core import Gensym, annotate, SerAPI, Position, PosStr
90+
from .core import Gensym, SerAPI, Position, PosStr
9191
from .pygments import highlight_html, highlight_latex, added_tokens, \
9292
resolve_token, replace_builtin_coq_lexer
9393

@@ -288,6 +288,16 @@ def apply(self, **kwargs):
288288
for node in self.document.traverse(self.is_math):
289289
node.attributes.setdefault("classes", []).append("mathjax_process")
290290

291+
class DocutilsObserver(core.Observer):
292+
def __init__(self, document):
293+
self.document = document
294+
295+
def _notify(self, n: core.Notification):
296+
beg, end = n.location.beg, n.location.end
297+
self.document.reporter.system_message(
298+
n.level, n.message, line=beg.line, column=beg.col,
299+
end_line=end.line, end_column=end.col)
300+
291301
class AlectryonTransform(OneTimeTransform):
292302
default_priority = 800
293303
auto_toggle = True
@@ -316,9 +326,11 @@ def check_for_long_lines(self, node, fragments):
316326

317327
def annotate_cached(self, chunks, sertop_args):
318328
from .json import Cache
329+
prover = SerAPI(sertop_args)
330+
prover.observer = DocutilsObserver(self.document)
319331
metadata = {"sertop_args": sertop_args}
320332
cache = Cache(CACHE_DIRECTORY, self.document['source'], metadata, CACHE_COMPRESSION)
321-
annotated = cache.update(chunks, lambda c: annotate(c, sertop_args), SerAPI.version_info())
333+
annotated = cache.update(chunks, prover.annotate, SerAPI.version_info())
322334
return cache.generator, annotated
323335

324336
def annotate(self, pending_nodes):

recipes/_output/tests/errors.txt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,17 @@ Missing search pattern for key ``.s`` in expression ``.s.g``. (maybe an invalid
4545
Check nat.
4646

4747
tests/errors.rst:5: (ERROR/3) In `:alectryon/pygments/xyz:`: Unknown token kind: xyz
48-
tests/errors.rst:(109:62)-(109:64): (WARNING/2) Coq raised an exception:
49-
> Syntax error: '.' expected after [vernac:syntax_command] (in [vernac_aux]).
50-
The offending chunk is delimited by >>>…<<< below:
51-
> Notation "'🆄🄽🅘ⓒ⒪𝖉∈' α ⊕ ⤋ β" := α >>>++<<< β. (* Bytes vs. str *)
52-
Results past this point may be unreliable.
53-
tests/errors.rst:(114:8)-(114:12): (WARNING/2) Coq raised an exception:
54-
> The term "true" has type "bool" which should be Set, Prop or Type.
55-
The offending chunk is delimited by >>>…<<< below:
56-
> Definition a
57-
> : >>>true<<< := 1. (* Next line *)
58-
Results past this point may be unreliable.
48+
tests/errors.rst:109: (ERROR/3) Coq raised an exception:
49+
> Syntax error: '.' expected after [vernac:syntax_command] (in [vernac_aux]).
50+
The offending chunk is delimited by >>>…<<< below:
51+
> Notation "'🆄🄽🅘ⓒ⒪𝖉∈' α ⊕ ⤋ β" := α >>>++<<< β. (* Bytes vs. str *)
52+
Results past this point may be unreliable.
53+
tests/errors.rst:114: (ERROR/3) Coq raised an exception:
54+
> The term "true" has type "bool" which should be Set, Prop or Type.
55+
The offending chunk is delimited by >>>…<<< below:
56+
> Definition a
57+
> : >>>true<<< := 1. (* Next line *)
58+
Results past this point may be unreliable.
5959
tests/errors.rst:15: (WARNING/2) Long line (107 characters)
6060
exact I. (* A very long line *) (* whose contents are split *) (* across *) (* multiple *) (* comments *)
6161
tests/errors.rst:87: (ERROR/3) In `.. coq::`:

0 commit comments

Comments
 (0)