comparison env/lib/python3.9/site-packages/schema_salad/makedoc.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 import argparse
2 import copy
3 import logging
4 import os
5 import re
6 import sys
7 from codecs import StreamWriter
8 from io import StringIO, TextIOWrapper
9 from typing import (
10 IO,
11 Any,
12 Dict,
13 List,
14 MutableMapping,
15 MutableSequence,
16 Optional,
17 Set,
18 Tuple,
19 Union,
20 cast,
21 )
22 from urllib.parse import urldefrag
23
24 import mistune
25
26 from . import schema
27 from .exceptions import SchemaSaladException, ValidationException
28 from .utils import add_dictlist, aslist
29
30 _logger = logging.getLogger("salad")
31
32
33 def has_types(items: Any) -> List[str]:
34 r = [] # type: List[str]
35 if isinstance(items, MutableMapping):
36 if items["type"] == "https://w3id.org/cwl/salad#record":
37 return [items["name"]]
38 for n in ("type", "items", "values"):
39 if n in items:
40 r.extend(has_types(items[n]))
41 return r
42 if isinstance(items, MutableSequence):
43 for i in items:
44 r.extend(has_types(i))
45 return r
46 if isinstance(items, str):
47 return [items]
48 return []
49
50
51 def linkto(item: str) -> str:
52 frg = urldefrag(item)[1]
53 return "[{}](#{})".format(frg, to_id(frg))
54
55
56 class MyRenderer(mistune.Renderer):
57 def __init__(self) -> None:
58 super().__init__()
59 self.options = {}
60
61 def header(self, text: str, level: int, raw: Optional[Any] = None) -> str:
62 return (
63 """<h{} id="{}" class="section">{} <a href="#{}">&sect;</a></h{}>""".format(
64 level, to_id(text), text, to_id(text), level
65 )
66 )
67
68 def table(self, header: str, body: str) -> str:
69 return (
70 '<table class="table table-striped">\n<thead>{}</thead>\n'
71 "<tbody>\n{}</tbody>\n</table>\n"
72 ).format(header, body)
73
74
75 def to_id(text: str) -> str:
76 textid = text
77 if text[0] in ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"):
78 try:
79 textid = text[text.index(" ") + 1 :]
80 except ValueError:
81 pass
82 return textid.replace(" ", "_")
83
84
85 class ToC:
86 def __init__(self) -> None:
87 self.first_toc_entry = True
88 self.numbering = [0]
89 self.toc = ""
90 self.start_numbering = True
91
92 def add_entry(self, thisdepth, title): # type: (int, str) -> str
93 depth = len(self.numbering)
94 if thisdepth < depth:
95 self.toc += "</ol>"
96 for _ in range(0, depth - thisdepth):
97 self.numbering.pop()
98 self.toc += "</li></ol>"
99 self.numbering[-1] += 1
100 elif thisdepth == depth:
101 if not self.first_toc_entry:
102 self.toc += "</ol>"
103 else:
104 self.first_toc_entry = False
105 self.numbering[-1] += 1
106 elif thisdepth > depth:
107 self.numbering.append(1)
108
109 num = (
110 "{}.{}".format(
111 self.numbering[0], ".".join([str(n) for n in self.numbering[1:]])
112 )
113 if self.start_numbering
114 else ""
115 )
116 self.toc += """<li><a href="#{}">{} {}</a><ol>\n""".format(
117 to_id(title), num, title
118 )
119 return num
120
121 def contents(self, idn: str) -> str:
122 toc = """<h1 id="{}">Table of contents</h1>
123 <nav class="tocnav"><ol>{}""".format(
124 idn, self.toc
125 )
126 toc += "</ol>"
127 for _ in range(0, len(self.numbering)):
128 toc += "</li></ol>"
129 toc += """</nav>"""
130 return toc
131
132
133 basicTypes = (
134 "https://w3id.org/cwl/salad#null",
135 "http://www.w3.org/2001/XMLSchema#boolean",
136 "http://www.w3.org/2001/XMLSchema#int",
137 "http://www.w3.org/2001/XMLSchema#long",
138 "http://www.w3.org/2001/XMLSchema#float",
139 "http://www.w3.org/2001/XMLSchema#double",
140 "http://www.w3.org/2001/XMLSchema#string",
141 "https://w3id.org/cwl/salad#record",
142 "https://w3id.org/cwl/salad#enum",
143 "https://w3id.org/cwl/salad#array",
144 )
145
146
147 def number_headings(toc: ToC, maindoc: str) -> str:
148 mdlines = []
149 skip = False
150 for line in maindoc.splitlines():
151 if line.strip() == "# Introduction":
152 toc.start_numbering = True
153 toc.numbering.clear()
154 toc.numbering.append(0)
155
156 if "```" in line:
157 skip = not skip
158
159 if not skip:
160 m = re.match(r"^(#+) (.*)", line)
161 if m is not None:
162 group1 = m.group(1)
163 assert group1 is not None # nosec
164 group2 = m.group(2)
165 assert group2 is not None # nosec
166 num = toc.add_entry(len(group1), group2)
167 line = f"{group1} {num} {group2}"
168 line = re.sub(r"^(https?://\S+)", r"[\1](\1)", line)
169 mdlines.append(line)
170
171 maindoc = "\n".join(mdlines)
172 return maindoc
173
174
175 def fix_doc(doc: Union[List[str], str]) -> str:
176 docstr = "".join(doc) if isinstance(doc, MutableSequence) else doc
177 return "\n".join(
178 [
179 re.sub(r"<([^>@]+@[^>]+)>", r"[\1](mailto:\1)", d)
180 for d in docstr.splitlines()
181 ]
182 )
183
184
185 class RenderType:
186 def __init__(
187 self,
188 toc: ToC,
189 j: List[Dict[str, str]],
190 renderlist: str,
191 redirects: Dict[str, str],
192 primitiveType: str,
193 ) -> None:
194 self.typedoc = StringIO()
195 self.toc = toc
196 self.subs = {} # type: Dict[str, str]
197 self.docParent = {} # type: Dict[str, List[str]]
198 self.docAfter = {} # type: Dict[str, List[str]]
199 self.rendered = set() # type: Set[str]
200 self.redirects = redirects
201 self.title = None # type: Optional[str]
202 self.primitiveType = primitiveType
203
204 for t in j:
205 if "extends" in t:
206 for e in aslist(t["extends"]):
207 add_dictlist(self.subs, e, t["name"])
208 # if "docParent" not in t and "docAfter" not in t:
209 # add_dictlist(self.docParent, e, t["name"])
210
211 if t.get("docParent"):
212 add_dictlist(self.docParent, t["docParent"], t["name"])
213
214 if t.get("docChild"):
215 for c in aslist(t["docChild"]):
216 add_dictlist(self.docParent, t["name"], c)
217
218 if t.get("docAfter"):
219 add_dictlist(self.docAfter, t["docAfter"], t["name"])
220
221 metaschema_loader = schema.get_metaschema()[2]
222 alltypes = schema.extend_and_specialize(j, metaschema_loader)
223
224 self.typemap = {} # type: Dict[str, Dict[str, str]]
225 self.uses = {} # type: Dict[str, List[Tuple[str, str]]]
226 self.record_refs = {} # type: Dict[str, List[str]]
227 for entry in alltypes:
228 self.typemap[entry["name"]] = entry
229 try:
230 if entry["type"] == "record":
231 self.record_refs[entry["name"]] = []
232 fields = entry.get(
233 "fields", []
234 ) # type: Union[str, List[Dict[str, str]]]
235 if isinstance(fields, str):
236 raise KeyError("record fields must be a list of mappings")
237 for f in fields: # type: Dict[str, str]
238 p = has_types(f)
239 for tp in p:
240 if tp not in self.uses:
241 self.uses[tp] = []
242 if (entry["name"], f["name"]) not in self.uses[tp]:
243 _, frg1 = urldefrag(t["name"])
244 _, frg2 = urldefrag(f["name"])
245 self.uses[tp].append((frg1, frg2))
246 if (
247 tp not in basicTypes
248 and tp not in self.record_refs[entry["name"]]
249 ):
250 self.record_refs[entry["name"]].append(tp)
251 except KeyError:
252 _logger.error("Did not find 'type' in %s", t)
253 _logger.error("record refs is %s", self.record_refs)
254 raise
255
256 for entry in alltypes:
257 if entry["name"] in renderlist or (
258 (not renderlist)
259 and ("extends" not in entry)
260 and ("docParent" not in entry)
261 and ("docAfter" not in entry)
262 ):
263 self.render_type(entry, 1)
264
265 def typefmt(
266 self,
267 tp: Any,
268 redirects: Dict[str, str],
269 nbsp: bool = False,
270 jsonldPredicate: Optional[Dict[str, str]] = None,
271 ) -> str:
272 if isinstance(tp, MutableSequence):
273 if nbsp and len(tp) <= 3:
274 return "&nbsp;|&nbsp;".join(
275 [
276 self.typefmt(n, redirects, jsonldPredicate=jsonldPredicate)
277 for n in tp
278 ]
279 )
280 return " | ".join(
281 [
282 self.typefmt(n, redirects, jsonldPredicate=jsonldPredicate)
283 for n in tp
284 ]
285 )
286 if isinstance(tp, MutableMapping):
287 if tp["type"] == "https://w3id.org/cwl/salad#array":
288 ar = "array&lt;{}&gt;".format(
289 self.typefmt(tp["items"], redirects, nbsp=True)
290 )
291 if jsonldPredicate is not None and "mapSubject" in jsonldPredicate:
292 if "mapPredicate" in jsonldPredicate:
293 ar += " | "
294 if len(ar) > 40:
295 ar += "<br>"
296
297 ar += (
298 "<a href='#map'>map</a>&lt;<code>{}</code>"
299 ",&nbsp;<code>{}</code> | {}&gt".format(
300 jsonldPredicate["mapSubject"],
301 jsonldPredicate["mapPredicate"],
302 self.typefmt(tp["items"], redirects),
303 )
304 )
305 else:
306 ar += " | "
307 if len(ar) > 40:
308 ar += "<br>"
309 ar += "<a href='#map'>map</a>&lt;<code>{}</code>,&nbsp;{}&gt".format(
310 jsonldPredicate["mapSubject"],
311 self.typefmt(tp["items"], redirects),
312 )
313 return ar
314 if tp["type"] in (
315 "https://w3id.org/cwl/salad#record",
316 "https://w3id.org/cwl/salad#enum",
317 ):
318 frg = schema.avro_name(tp["name"])
319 if tp["name"] in redirects:
320 return """<a href="{}">{}</a>""".format(redirects[tp["name"]], frg)
321 if tp["name"] in self.typemap:
322 return """<a href="#{}">{}</a>""".format(to_id(frg), frg)
323 if (
324 tp["type"] == "https://w3id.org/cwl/salad#enum"
325 and len(tp["symbols"]) == 1
326 ):
327 return "constant value <code>{}</code>".format(
328 schema.avro_name(tp["symbols"][0])
329 )
330 return frg
331 if isinstance(tp["type"], MutableMapping):
332 return self.typefmt(tp["type"], redirects)
333 else:
334 if str(tp) in redirects:
335 return """<a href="{}">{}</a>""".format(redirects[tp], redirects[tp])
336 if str(tp) in basicTypes:
337 return """<a href="{}">{}</a>""".format(
338 self.primitiveType, schema.avro_name(str(tp))
339 )
340 frg2 = urldefrag(tp)[1]
341 if frg2 != "":
342 tp = frg2
343 return """<a href="#{}">{}</a>""".format(to_id(tp), tp)
344 raise SchemaSaladException("We should not be here!")
345
346 def render_type(self, f: Dict[str, Any], depth: int) -> None:
347 if f["name"] in self.rendered or f["name"] in self.redirects:
348 return
349 self.rendered.add(f["name"])
350
351 if f.get("abstract"):
352 return
353
354 if "doc" not in f:
355 f["doc"] = ""
356
357 f["type"] = copy.deepcopy(f)
358 f["doc"] = ""
359 f = f["type"]
360
361 if "doc" not in f:
362 f["doc"] = ""
363
364 def extendsfrom(item: Dict[str, Any], ex: List[Dict[str, Any]]) -> None:
365 if "extends" in item:
366 for e in aslist(item["extends"]):
367 ex.insert(0, self.typemap[e])
368 extendsfrom(self.typemap[e], ex)
369
370 ex = [f]
371 extendsfrom(f, ex)
372
373 enumDesc = {}
374 if f["type"] == "enum" and isinstance(f["doc"], MutableSequence):
375 for e in ex:
376 for i in e["doc"]:
377 idx = i.find(":")
378 if idx > -1:
379 enumDesc[i[:idx]] = i[idx + 1 :]
380 e["doc"] = [
381 i
382 for i in e["doc"]
383 if i.find(":") == -1 or i.find(" ") < i.find(":")
384 ]
385
386 f["doc"] = fix_doc(f["doc"])
387
388 if f["type"] == "record":
389 for field in f.get("fields", []):
390 if "doc" not in field:
391 field["doc"] = ""
392
393 if f["type"] != "documentation":
394 lines = []
395 for line in f["doc"].splitlines():
396 if len(line) > 0 and line[0] == "#":
397 line = ("#" * depth) + line
398 lines.append(line)
399 f["doc"] = "\n".join(lines)
400
401 frg = urldefrag(f["name"])[1]
402 num = self.toc.add_entry(depth, frg)
403 doc = "{} {} {}\n".format(("#" * depth), num, frg)
404 else:
405 doc = ""
406
407 if self.title is None and f["doc"]:
408 title = f["doc"][0 : f["doc"].index("\n")]
409 if title.startswith("# "):
410 self.title = title[2:]
411 else:
412 self.title = title
413
414 if f["type"] == "documentation":
415 f["doc"] = number_headings(self.toc, f["doc"])
416
417 doc = doc + "\n\n" + f["doc"]
418
419 doc = mistune.markdown(doc, renderer=MyRenderer())
420
421 if f["type"] == "record":
422 doc += "<h3>Fields</h3>"
423 doc += """
424 <div class="responsive-table">
425 <div class="row responsive-table-header">
426 <div class="col-xs-3 col-lg-2">field</div>
427 <div class="col-xs-2 col-lg-1">required</div>
428 <div class="col-xs-7 col-lg-3">type</div>
429 <div class="col-xs-12 col-lg-6 description-header">description</div>
430 </div>"""
431 required = []
432 optional = []
433 for i in f.get("fields", []):
434 tp = i["type"]
435 if (
436 isinstance(tp, MutableSequence)
437 and tp[0] == "https://w3id.org/cwl/salad#null"
438 ):
439 opt = False
440 tp = tp[1:]
441 else:
442 opt = True
443
444 desc = i["doc"]
445
446 rfrg = schema.avro_name(i["name"])
447 tr = """
448 <div class="row responsive-table-row">
449 <div class="col-xs-3 col-lg-2"><code>{}</code></div>
450 <div class="col-xs-2 col-lg-1">{}</div>
451 <div class="col-xs-7 col-lg-3">{}</div>
452 <div class="col-xs-12 col-lg-6 description-col">{}</div>
453 </div>""".format(
454 rfrg,
455 "required" if opt else "optional",
456 self.typefmt(
457 tp, self.redirects, jsonldPredicate=i.get("jsonldPredicate")
458 ),
459 mistune.markdown(desc),
460 )
461 if opt:
462 required.append(tr)
463 else:
464 optional.append(tr)
465 for i in required + optional:
466 doc += i
467 doc += """</div>"""
468 elif f["type"] == "enum":
469 doc += "<h3>Symbols</h3>"
470 doc += """<table class="table table-striped">"""
471 doc += "<tr><th>symbol</th><th>description</th></tr>"
472 for e in ex:
473 for i in e.get("symbols", []):
474 doc += "<tr>"
475 efrg = schema.avro_name(i)
476 doc += "<td><code>{}</code></td><td>{}</td>".format(
477 efrg, enumDesc.get(efrg, "")
478 )
479 doc += "</tr>"
480 doc += """</table>"""
481 f["doc"] = doc
482
483 self.typedoc.write(f["doc"])
484
485 subs = self.docParent.get(f["name"], []) + self.record_refs.get(f["name"], [])
486 if len(subs) == 1:
487 self.render_type(self.typemap[subs[0]], depth)
488 else:
489 for s in subs:
490 self.render_type(self.typemap[s], depth + 1)
491
492 for s in self.docAfter.get(f["name"], []):
493 self.render_type(self.typemap[s], depth)
494
495
496 def avrold_doc(
497 j: List[Dict[str, Any]],
498 outdoc: Union[IO[Any], StreamWriter],
499 renderlist: str,
500 redirects: Dict[str, str],
501 brand: str,
502 brandlink: str,
503 primtype: str,
504 brandstyle: Optional[str] = None,
505 brandinverse: Optional[bool] = False,
506 ) -> None:
507 toc = ToC()
508 toc.start_numbering = False
509
510 rt = RenderType(toc, j, renderlist, redirects, primtype)
511 content = rt.typedoc.getvalue()
512
513 if brandstyle is None:
514 bootstrap_url = (
515 "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"
516 )
517 bootstrap_integrity = (
518 "sha384-604wwakM23pEysLJAhja8Lm42IIwYrJ0dEAqzFsj9pJ/P5buiujjywArgPCi8eoz"
519 )
520 brandstyle_template = (
521 '<link rel="stylesheet" href={} integrity={} crossorigin="anonymous">'
522 )
523 brandstyle = brandstyle_template.format(bootstrap_url, bootstrap_integrity)
524
525 picturefill_url = (
526 "https://cdn.rawgit.com/scottjehl/picturefill/3.0.2/dist/picturefill.min.js"
527 )
528 picturefill_integrity = (
529 "sha384-ZJsVW8YHHxQHJ+SJDncpN90d0EfAhPP+yA94n+EhSRzhcxfo84yMnNk+v37RGlWR"
530 )
531 outdoc.write(
532 """
533 <!DOCTYPE html>
534 <html>
535 <head>
536 <meta charset="UTF-8">
537 <meta name="viewport" content="width=device-width, initial-scale=1.0">
538 {}
539 <script>
540 // Picture element HTML5 shiv
541 document.createElement( "picture" );
542 </script>
543 <script src="{}"
544 integrity="{}"
545 crossorigin="anonymous" async></script>
546 """.format(
547 brandstyle, picturefill_url, picturefill_integrity
548 )
549 )
550
551 outdoc.write(f"<title>{rt.title}</title>")
552
553 outdoc.write(
554 """
555 <style>
556 :target {
557 padding-top: 61px;
558 margin-top: -61px;
559 }
560 body {
561 padding-top: 61px;
562 }
563 .tocnav ol {
564 list-style: none
565 }
566 pre {
567 margin-left: 2em;
568 margin-right: 2em;
569 }
570 .section a {
571 visibility: hidden;
572 }
573 .section:hover a {
574 visibility: visible;
575 color: rgb(201, 201, 201);
576 }
577 .responsive-table-header {
578 text-align: left;
579 padding: 8px;
580 vertical-align: top;
581 font-weight: bold;
582 border-top-color: rgb(221, 221, 221);
583 border-top-style: solid;
584 border-top-width: 1px;
585 background-color: #f9f9f9
586 }
587 .responsive-table > .responsive-table-row {
588 text-align: left;
589 padding: 8px;
590 vertical-align: top;
591 border-top-color: rgb(221, 221, 221);
592 border-top-style: solid;
593 border-top-width: 1px;
594 }
595 @media (min-width: 0px), print {
596 .description-header {
597 display: none;
598 }
599 .description-col {
600 margin-top: 1em;
601 margin-left: 1.5em;
602 }
603 }
604 @media (min-width: 1170px) {
605 .description-header {
606 display: inline;
607 }
608 .description-col {
609 margin-top: 0px;
610 margin-left: 0px;
611 }
612 }
613 .responsive-table-row:nth-of-type(odd) {
614 background-color: #f9f9f9
615 }
616 </style>
617 </head>
618 <body>
619 """
620 )
621
622 navbar_extraclass = "navbar-inverse" if brandinverse else ""
623 outdoc.write(
624 """
625 <nav class="navbar navbar-default navbar-fixed-top {}">
626 <div class="container">
627 <div class="navbar-header">
628 <a class="navbar-brand" href="{}">{}</a>
629 """.format(
630 navbar_extraclass, brandlink, brand
631 )
632 )
633
634 if "<!--ToC-->" in content:
635 content = content.replace("<!--ToC-->", toc.contents("toc"))
636 outdoc.write(
637 """
638 <ul class="nav navbar-nav">
639 <li><a href="#toc">Table of contents</a></li>
640 </ul>
641 """
642 )
643
644 outdoc.write(
645 """
646 </div>
647 </div>
648 </nav>
649 """
650 )
651
652 outdoc.write(
653 """
654 <div class="container">
655 """
656 )
657
658 outdoc.write(
659 """
660 <div class="row">
661 """
662 )
663
664 outdoc.write(
665 """
666 <div class="col-md-12" role="main" id="main">"""
667 )
668
669 outdoc.write(content)
670
671 outdoc.write("""</div>""")
672
673 outdoc.write(
674 """
675 </div>
676 </div>
677 </body>
678 </html>"""
679 )
680
681
682 def main() -> None:
683 parser = argparse.ArgumentParser()
684 parser.add_argument("schema")
685 parser.add_argument("--only", action="append")
686 parser.add_argument("--redirect", action="append")
687 parser.add_argument("--brand")
688 parser.add_argument("--brandlink")
689 parser.add_argument("--brandstyle")
690 parser.add_argument("--brandinverse", default=False, action="store_true")
691 parser.add_argument("--primtype", default="#PrimitiveType")
692
693 args = parser.parse_args()
694
695 makedoc(args)
696
697
698 def makedoc(args: argparse.Namespace) -> None:
699
700 s = [] # type: List[Dict[str, Any]]
701 a = args.schema
702 with open(a, encoding="utf-8") as f:
703 if a.endswith("md"):
704 s.append(
705 {
706 "name": os.path.splitext(os.path.basename(a))[0],
707 "type": "documentation",
708 "doc": f.read(),
709 }
710 )
711 else:
712 uri = "file://" + os.path.abspath(a)
713 metaschema_loader = schema.get_metaschema()[2]
714 j = metaschema_loader.resolve_ref(uri, "")[0]
715 if isinstance(j, MutableSequence):
716 s.extend(j)
717 elif isinstance(j, MutableMapping):
718 s.append(j)
719 else:
720 raise ValidationException("Schema must resolve to a list or a dict")
721 redirect = {}
722 for r in args.redirect or []:
723 redirect[r.split("=")[0]] = r.split("=")[1]
724 renderlist = args.only if args.only else []
725 stdout = (
726 TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
727 if sys.stdout.encoding != "UTF-8"
728 else cast(TextIOWrapper, sys.stdout)
729 ) # type: Union[TextIOWrapper, StreamWriter]
730 avrold_doc(
731 s,
732 stdout,
733 renderlist,
734 redirect,
735 args.brand,
736 args.brandlink,
737 args.primtype,
738 brandstyle=args.brandstyle,
739 brandinverse=args.brandinverse,
740 )
741
742
743 if __name__ == "__main__":
744 main()