comparison env/lib/python3.7/site-packages/schema_salad/makedoc.py @ 0:26e78fe6e8c4 draft

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