Skip to content

Commit dffde22

Browse files
committed
cli, docutils: Unify docutils pipelines and propagate error code on exit
Closes GH-57. Suggested-by: @gares
1 parent a310ec5 commit dffde22

File tree

8 files changed

+87
-88
lines changed

8 files changed

+87
-88
lines changed

alectryon/cli.py

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def register_docutils(v, ctx):
9292
'stylesheet_path': None,
9393
'input_encoding': 'utf-8',
9494
'output_encoding': 'utf-8',
95+
'exit_status_level': 3,
9596
'alectryon_banner': ctx["include_banner"],
9697
'alectryon_vernums': ctx["include_vernums"],
9798
'alectryon_webpage_style': ctx["webpage_style"],
@@ -102,17 +103,26 @@ def register_docutils(v, ctx):
102103
def _gen_docutils(source, fpath,
103104
Parser, Reader, Writer,
104105
settings_overrides):
105-
from docutils.core import publish_string
106+
from docutils.core import publish_programmatically
107+
from docutils.io import StringInput, StringOutput
106108

107109
parser = Parser()
108-
return publish_string(
109-
source=source.encode("utf-8"),
110+
output, pub = publish_programmatically(
111+
source_class=StringInput, destination_class=StringOutput,
112+
source=source.encode("utf-8"), destination=None,
110113
source_path=fpath, destination_path=None,
114+
111115
reader=Reader(parser), reader_name=None,
112116
parser=parser, parser_name=None,
113117
writer=Writer(), writer_name=None,
114-
settings_overrides=settings_overrides,
115-
enable_exit_status=True).decode("utf-8")
118+
119+
settings=None, settings_spec=None,
120+
settings_overrides=settings_overrides, config_section=None,
121+
enable_exit_status=False)
122+
123+
max_level = pub.document.reporter.max_level
124+
exit_code = max_level + 10 if max_level >= pub.settings.exit_status_level else 0
125+
return output.decode("utf-8"), exit_code
116126

117127
def _resolve_dialect(backend, html_dialect, latex_dialect):
118128
return {"webpage": html_dialect, "latex": latex_dialect}.get(backend, None)
@@ -122,15 +132,17 @@ def _record_assets(assets, path, names):
122132
assets.append((path, name))
123133

124134
def gen_docutils(src, frontend, backend, fpath, dialect,
125-
docutils_settings_overrides, assets):
135+
docutils_settings_overrides, assets, exit_code):
126136
from .docutils import get_pipeline
127137

128138
pipeline = get_pipeline(frontend, backend, dialect)
129139
_record_assets(assets, pipeline.translator.ASSETS_PATH, pipeline.translator.ASSETS)
130140

131-
return _gen_docutils(src, fpath,
132-
pipeline.parser, pipeline.reader, pipeline.writer,
133-
docutils_settings_overrides)
141+
output, exit_code.val = \
142+
_gen_docutils(src, fpath,
143+
pipeline.parser, pipeline.reader, pipeline.writer,
144+
docutils_settings_overrides)
145+
return output
134146

135147
def _docutils_cmdline(description, frontend, backend):
136148
import locale
@@ -146,24 +158,7 @@ def _docutils_cmdline(description, frontend, backend):
146158
publish_cmdline(
147159
parser=pipeline.parser(), writer=pipeline.writer(),
148160
settings_overrides={'stylesheet_path': None},
149-
description="{} {}".format(description, default_description)
150-
)
151-
152-
def lint_docutils(source, fpath, frontend, docutils_settings_overrides):
153-
from docutils.core import publish_doctree
154-
from .docutils import get_parser, LintingReader
155-
156-
parser = get_parser(frontend)()
157-
reader = LintingReader(parser)
158-
159-
publish_doctree(
160-
source=source.encode("utf-8"), source_path=fpath,
161-
reader=reader, reader_name=None,
162-
parser=parser, parser_name=None,
163-
settings_overrides=docutils_settings_overrides,
164-
enable_exit_status=True)
165-
166-
return reader.error_stream.getvalue() # FIXME exit code
161+
description="{} {}".format(description, default_description))
167162

168163
def _scrub_fname(fname):
169164
import re
@@ -380,7 +375,7 @@ def write_file(ext):
380375
(read_plain, parse_coq_plain, annotate_chunks, apply_transforms,
381376
gen_latex_snippets, dump_latex_snippets, write_file(".snippets.tex")),
382377
'lint':
383-
(read_plain, register_docutils, lint_docutils,
378+
(read_plain, register_docutils, gen_docutils,
384379
write_file(".lint.json")),
385380
'rst':
386381
(read_plain, coq_to_rst, write_file(".v.rst")),
@@ -396,7 +391,7 @@ def write_file(ext):
396391
(read_plain, register_docutils, gen_docutils, copy_assets,
397392
write_file(".tex")),
398393
'lint':
399-
(read_plain, register_docutils, lint_docutils,
394+
(read_plain, register_docutils, gen_docutils,
400395
write_file(".lint.json")),
401396
'rst':
402397
(read_plain, coq_to_rst, write_file(".v.rst"))
@@ -415,7 +410,7 @@ def write_file(ext):
415410
(read_plain, register_docutils, gen_docutils, copy_assets,
416411
write_file(".tex")),
417412
'lint':
418-
(read_plain, register_docutils, lint_docutils,
413+
(read_plain, register_docutils, gen_docutils,
419414
write_file(".lint.json")),
420415
'coq':
421416
(read_plain, rst_to_coq, write_file(".v")),
@@ -430,7 +425,7 @@ def write_file(ext):
430425
(read_plain, register_docutils, gen_docutils, copy_assets,
431426
write_file(".tex")),
432427
'lint':
433-
(read_plain, register_docutils, lint_docutils,
428+
(read_plain, register_docutils, gen_docutils,
434429
write_file(".lint.json"))
435430
}
436431
}
@@ -694,6 +689,10 @@ def parse_arguments():
694689
# Entry point
695690
# ===========
696691

692+
class ExitCode:
693+
def __init__(self, n):
694+
self.val = n
695+
697696
def call_pipeline_step(step, state, ctx):
698697
params = list(inspect.signature(step).parameters.keys())[1:]
699698
return step(state, **{p: ctx[p] for p in params})
@@ -708,7 +707,7 @@ def build_context(fpath, args, frontend, backend):
708707
ctx = {**vars(args),
709708
"fpath": fpath, "fname": fname,
710709
"frontend": frontend, "backend": backend, "dialect": dialect,
711-
"assets": [], "html_classes": []}
710+
"assets": [], "html_classes": [], "exit_code": ExitCode(0)}
712711
ctx["ctx"] = ctx
713712

714713
if args.output_directory is None:
@@ -742,11 +741,12 @@ def process_pipelines(args):
742741
state, ctx = None, build_context(fpath, args, frontend, backend)
743742
for step in pipeline:
744743
state = call_pipeline_step(step, state, ctx)
744+
yield ctx["exit_code"].val
745745

746746
def main():
747747
try:
748748
args = parse_arguments()
749-
process_pipelines(args)
749+
sys.exit(max(process_pipelines(args), default=0))
750750
except (ValueError, FileNotFoundError, ImportError, argparse.ArgumentTypeError) as e:
751751
if core.TRACEBACK:
752752
raise e

alectryon/docutils.py

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
import docutils.frontend
7979
import docutils.transforms
8080
import docutils.utils
81+
import docutils.writers
8182
from docutils import nodes
8283

8384
from docutils.parsers.rst import directives, roles, Directive # type: ignore
@@ -904,7 +905,7 @@ def marker_quote_role(role, rawtext, text, lineno, inliner,
904905
# Error printer
905906
# -------------
906907

907-
class JsErrorPrinter:
908+
class JsErrorObserver:
908909
@staticmethod
909910
def json_of_message(msg):
910911
message = msg.children[0].astext() if msg.children else "Unknown error"
@@ -919,12 +920,14 @@ def json_of_message(msg):
919920
return js
920921

921922
def __init__(self, stream, settings):
923+
self.errors = []
922924
self.stream = stream
923925
self.report_level = settings.report_level
924926

925927
def __call__(self, msg):
926928
import json
927-
if msg['level'] >= self.report_level:
929+
self.errors.append(msg)
930+
if self.stream and msg['level'] >= self.report_level:
928931
js = self.json_of_message(msg)
929932
json.dump(js, self.stream)
930933
self.stream.write('\n')
@@ -1119,29 +1122,7 @@ def __init__(self, *args, **kwargs):
11191122

11201123
class DummyTranslator:
11211124
ASSETS: List[str] = []
1122-
1123-
Pipeline = namedtuple("Pipeline", "parser reader translator writer")
1124-
1125-
PARSERS = {
1126-
"coq+rst": (__name__, "RSTCoqParser"),
1127-
"rst": ("docutils.parsers.rst", "Parser"),
1128-
"md": ("alectryon.myst", "Parser"),
1129-
}
1130-
1131-
BACKENDS = {
1132-
'webpage': {
1133-
'html4': (HtmlTranslator, HtmlWriter),
1134-
'html5': (Html5Translator, Html5Writer),
1135-
},
1136-
'latex': {
1137-
'pdflatex': (LatexTranslator, LatexWriter),
1138-
'xelatex': (XeLatexTranslator, XeLatexWriter),
1139-
'lualatex': (LuaLatexTranslator, LuaLatexWriter),
1140-
},
1141-
'pseudoxml': {
1142-
None: (DummyTranslator, ("docutils.writers.pseudoxml", "Writer")),
1143-
}
1144-
}
1125+
ASSETS_PATH = ""
11451126

11461127
# Linter
11471128
# ======
@@ -1158,7 +1139,7 @@ class LintingReader(StandaloneReader):
11581139
def __init__(self, *args, **kwargs):
11591140
super().__init__(*args, **kwargs)
11601141
from io import StringIO
1161-
self.error_stream = StringIO()
1142+
self.error_stream = kwargs.get("error_stream", StringIO())
11621143

11631144
def get_transforms(self):
11641145
return super().get_transforms() + [LoadConfigTransform]
@@ -1167,20 +1148,54 @@ def new_document(self):
11671148
doc = super().new_document()
11681149
doc.transformer = EarlyTransformer(doc)
11691150

1170-
observer = JsErrorPrinter(self.error_stream, self.settings)
1151+
js_observer = JsErrorObserver(self.error_stream, self.settings)
11711152
doc.reporter.report_level = 0 # Report all messages
11721153
doc.reporter.halt_level = docutils.utils.Reporter.SEVERE_LEVEL + 1 # Do not exit early
11731154
doc.reporter.stream = False # Disable textual reporting
1174-
doc.reporter.attach_observer(observer)
1155+
doc.reporter.attach_observer(js_observer)
1156+
doc["js_observer"] = js_observer
11751157

11761158
return doc
11771159

1160+
class LintingWriter(docutils.writers.UnfilteredWriter):
1161+
def translate(self):
1162+
self.output = self.document["js_observer"].stream.getvalue()
1163+
11781164
# API
11791165
# ===
11801166

1167+
Pipeline = namedtuple("Pipeline", "reader parser translator writer")
1168+
1169+
PARSERS = {
1170+
"coq+rst": (__name__, "RSTCoqParser"),
1171+
"rst": ("docutils.parsers.rst", "Parser"),
1172+
"md": ("alectryon.myst", "Parser"),
1173+
}
1174+
1175+
BACKENDS = {
1176+
'webpage': {
1177+
'html4': (HtmlTranslator, HtmlWriter),
1178+
'html5': (Html5Translator, Html5Writer),
1179+
},
1180+
'latex': {
1181+
'pdflatex': (LatexTranslator, LatexWriter),
1182+
'xelatex': (XeLatexTranslator, XeLatexWriter),
1183+
'lualatex': (LuaLatexTranslator, LuaLatexWriter),
1184+
},
1185+
'lint': {
1186+
None: (DummyTranslator, LintingWriter),
1187+
},
1188+
'pseudoxml': {
1189+
None: (DummyTranslator, ("docutils.writers.pseudoxml", "Writer")),
1190+
}
1191+
}
1192+
11811193
def _maybe_import(tp):
11821194
return getattr(import_module(tp[0]), tp[1]) if isinstance(tp, tuple) else tp
11831195

1196+
def get_reader(_frontend, backend):
1197+
return LintingReader if backend == 'lint' else StandaloneReader
1198+
11841199
def get_parser(frontend):
11851200
if frontend not in PARSERS:
11861201
raise ValueError("Unsupported docutils frontend: {}".format(frontend))
@@ -1195,9 +1210,10 @@ def get_writer(backend, dialect):
11951210
return _maybe_import(translator), _maybe_import(writer)
11961211

11971212
def get_pipeline(frontend, backend, dialect):
1213+
reader = get_reader(frontend, backend)
11981214
parser = get_parser(frontend)
11991215
translator, writer = get_writer(backend, dialect)
1200-
return Pipeline(parser, StandaloneReader, translator, writer)
1216+
return Pipeline(reader, parser, translator, writer)
12011217

12021218
# Entry points
12031219
# ============

recipes/_output/tests/errors.lint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
{"level": "error", "message": "Error in \"coq\" directive:\nUnknown flag `unknown`.", "source": "tests/errors.rst", "line": 77, "column": null, "end_line": null, "end_column": null}
2020
{"level": "error", "message": "Error in \"coq\" directive:\nMissing search pattern for key ``.s`` in expression ``.s.g``. (maybe an invalid pattern?)", "source": "tests/errors.rst", "line": 82, "column": null, "end_line": null, "end_column": null}
2121
{"level": "error", "message": "In `:alectryon/pygments/xyz:`: Unknown token kind: xyz", "source": "tests/errors.rst", "line": 5, "column": null, "end_line": null, "end_column": null}
22+
exit: 13

recipes/_output/tests/errors.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,4 @@ tests/errors.rst:93: (ERROR/3) In `.. coq::`:
6868
tests/errors.rst:100: (ERROR/3) In `.. coq:: unfold out`: Cannot show output of 'Check nat.' without .in or .unfold.
6969
tests/errors.rst:70: (ERROR/3) In :mref:`.io#nope.s(123)`: Reference to unknown Alectryon block.
7070
tests/errors.rst:71: (ERROR/3) In :mref:`.s(Goal).g#25`: No goal matches '25'.
71+
exit: 13

recipes/_output/tests/linter.lint.json

Lines changed: 0 additions & 2 deletions
This file was deleted.

recipes/tests.mk

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ _output/tests/doctests.out: tests/doctests.py | _output/tests/
3838

3939
# reST → JSON errors
4040
_output/tests/errors.lint.json: tests/errors.rst
41-
$(alectryon) $< --backend lint
41+
$(alectryon) $< --backend lint; echo "exit: $$?" >> $@
4242
# reST → HTML + errors
4343
_output/tests/errors.txt: tests/errors.rst
44-
$(alectryon) $< --copy-assets none --backend webpage -o /dev/null 2> $@
44+
$(alectryon) $< --copy-assets none --backend webpage -o /dev/null 2> $@; echo "exit: $$?" >> $@
4545

4646
# Coq → HTML
4747
_output/tests/goal-name.html: tests/goal-name.v
@@ -54,10 +54,6 @@ _output/tests/goal-name.xe.tex: tests/goal-name.v
5454
_output/tests/latex_formatting.tex: tests/latex_formatting.v
5555
$(alectryon) $< --backend latex
5656

57-
# Coq+reST → JSON
58-
_output/tests/linter.lint.json: tests/linter.v
59-
$(alectryon) $< --backend lint
60-
6157
# reST → Coq
6258
_output/tests/literate.v: tests/literate.rst
6359
$(alectryon) $< --backend coq
@@ -74,6 +70,6 @@ _output/tests/screenshot.html: tests/screenshot.v
7470
_output/tests/syntax_highlighting.html: tests/syntax_highlighting.v
7571
$(alectryon) $<
7672

77-
_output/tests/dialects.4.html _output/tests/dialects.5.html _output/tests/dialects.tex _output/tests/dialects.xe.tex _output/tests/dialects.lua.tex _output/tests/directive-options.html _output/tests/directive-options.xe.tex _output/tests/display-flags.html _output/tests/doctests.out _output/tests/errors.lint.json _output/tests/errors.txt _output/tests/goal-name.html _output/tests/goal-name.xe.tex _output/tests/latex_formatting.tex _output/tests/linter.lint.json _output/tests/literate.v _output/tests/literate.v.rst _output/tests/screenshot.html _output/tests/syntax_highlighting.html: out_dir := _output/tests
73+
_output/tests/dialects.4.html _output/tests/dialects.5.html _output/tests/dialects.tex _output/tests/dialects.xe.tex _output/tests/dialects.lua.tex _output/tests/directive-options.html _output/tests/directive-options.xe.tex _output/tests/display-flags.html _output/tests/doctests.out _output/tests/errors.lint.json _output/tests/errors.txt _output/tests/goal-name.html _output/tests/goal-name.xe.tex _output/tests/latex_formatting.tex _output/tests/literate.v _output/tests/literate.v.rst _output/tests/screenshot.html _output/tests/syntax_highlighting.html: out_dir := _output/tests
7874

79-
targets += _output/tests/dialects.4.html _output/tests/dialects.5.html _output/tests/dialects.tex _output/tests/dialects.xe.tex _output/tests/dialects.lua.tex _output/tests/directive-options.html _output/tests/directive-options.xe.tex _output/tests/display-flags.html _output/tests/doctests.out _output/tests/errors.lint.json _output/tests/errors.txt _output/tests/goal-name.html _output/tests/goal-name.xe.tex _output/tests/latex_formatting.tex _output/tests/linter.lint.json _output/tests/literate.v _output/tests/literate.v.rst _output/tests/screenshot.html _output/tests/syntax_highlighting.html
75+
targets += _output/tests/dialects.4.html _output/tests/dialects.5.html _output/tests/dialects.tex _output/tests/dialects.xe.tex _output/tests/dialects.lua.tex _output/tests/directive-options.html _output/tests/directive-options.xe.tex _output/tests/display-flags.html _output/tests/doctests.out _output/tests/errors.lint.json _output/tests/errors.txt _output/tests/goal-name.html _output/tests/goal-name.xe.tex _output/tests/latex_formatting.tex _output/tests/literate.v _output/tests/literate.v.rst _output/tests/screenshot.html _output/tests/syntax_highlighting.html

recipes/tests/errors.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
:alectryon/pygments/xyz: test
66

7-
To compile::
7+
The ``lint`` backend in Alectryon runs the compiler and reports errors on ``stderr``::
88

9-
alectryon errors.rst --backend lint
9+
alectryon errors.rst --backend lint; echo "exit: $?" >> errors.lint.json
1010
# reST → JSON errors; produces ‘errors.lint.json’
11-
alectryon errors.rst --copy-assets none --backend webpage -o /dev/null 2> errors.txt
11+
alectryon errors.rst --copy-assets none --backend webpage -o /dev/null 2> errors.txt; echo "exit: $?" >> errors.txt
1212
# reST → HTML + errors; produces ‘errors.txt’
1313

1414
.. coq::

recipes/tests/linter.v

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)