File indexing completed on 2024-05-19 04:19:33
0001 #!/usr/bin/env python3 0002 # SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best> 0003 # SPDX-License-Identifier: GPL-3.0-or-later 0004 import re 0005 import sys 0006 import types 0007 import inspect 0008 import argparse 0009 0010 import glaxnimate 0011 0012 0013 class TocItem: 0014 def __init__(self, title="", id=""): 0015 self.title = title 0016 self.id = id 0017 self.children = [] 0018 0019 def to_yaml(self, file, depth=1): 0020 indent = " " * depth 0021 pre = "- " 0022 0023 def yaml_line(name, val): 0024 file.write("%s%s%s: %s\n" % (indent, pre, name, val)) 0025 0026 yaml_line("title", self.title) 0027 pre = " " 0028 yaml_line("url", repr("#" + self.id)) 0029 if self.children: 0030 yaml_line("children", "") 0031 for child in self.children: 0032 child.to_yaml(file, depth + 1) 0033 0034 def sub_item(self, title, id): 0035 sub = TocItem(title, id) 0036 self.children.append(sub) 0037 return sub 0038 0039 def write_toc(self, file): 0040 file.write("---\ntoc:\n") 0041 for toc in self.children: 0042 toc.to_yaml(file) 0043 file.write("---\n\n") 0044 0045 0046 class MdWriter: 0047 def __init__(self, file): 0048 self.out = file 0049 self.level = 1 0050 self._table_data = [] 0051 0052 def write(self, text): 0053 self.out.write(text) 0054 0055 def title(self, text, level=0): 0056 self.write("%s %s\n\n" % ("#"*(level+self.level), text)) 0057 #self.fancy_title(text, text, level) 0058 0059 def fancy_title(self, text, id, level=0): 0060 self.write("<h{level} id='{id}'><a href='#{id}'>{text}</a></h{level}>\n\n".format( 0061 text=text, 0062 id=id, 0063 level=level+self.level 0064 )) 0065 0066 def p(self, text): 0067 self.write(text + "\n\n") 0068 0069 def li(self, text): 0070 self.write("* %s\n" % text) 0071 0072 def a(self, text, href): 0073 return "[%s](%s)" % (text, href) 0074 0075 def nl(self): 0076 self.write("\n") 0077 0078 def sublevel(self, amount: int = 1): 0079 return MdLevel(self, amount) 0080 0081 def end_table(self): 0082 lengths = [ 0083 max(len(row[1][col]) for row in self._table_data) 0084 for col in range(len(self._table_data[0][1])) 0085 ] 0086 0087 for row in self._table_data: 0088 self.write("| ") 0089 for cell, length in zip(row[1], lengths): 0090 self.write(cell.ljust(length)) 0091 self.write(" | ") 0092 self.write("\n") 0093 0094 if row[0]: 0095 self.write("| ") 0096 for length in lengths: 0097 self.write("-" * length) 0098 self.write(" | ") 0099 self.write("\n") 0100 0101 self.write("\n") 0102 self._table_data = [] 0103 0104 def table_row(self, *cells): 0105 self._table_data.append( 0106 (False, list(map(str, cells))) 0107 ) 0108 0109 def table_header(self, *cells): 0110 self._table_data.append( 0111 (True, list(map(str, cells))) 0112 ) 0113 0114 def code(self, string): 0115 return "`%s`" % string 0116 0117 0118 class MdLevel: 0119 def __init__(self, mdw: MdWriter, amount: int): 0120 self.mdw = mdw 0121 self.amount = amount 0122 0123 def __enter__(self): 0124 self.mdw.level += self.amount 0125 0126 def __exit__(self, *a): 0127 self.mdw.level -= self.amount 0128 0129 0130 class ModuleDocs: 0131 def __init__(self, module): 0132 self.module = module 0133 self.props = [] 0134 self.const = [] 0135 self.classes = [] 0136 self.functions = [] 0137 self.file_props = [] 0138 self.docs = inspect.getdoc(module) 0139 if self.docs and "Members:" in self.docs: 0140 self.docs = "" 0141 0142 def toc(self, writer: TocItem): 0143 sub = writer.sub_item(self.toc_title(), self.name()) 0144 0145 if self.classes: 0146 for cls in self.classes: 0147 cls.toc(sub) 0148 0149 def name(self): 0150 return self.module.__name__ 0151 0152 def toc_title(self): 0153 return self.name() 0154 0155 def toc_pre_title(self): 0156 return "" 0157 0158 def print_intro(self, writer: MdWriter): 0159 pass 0160 0161 def print(self, writer: MdWriter): 0162 writer.fancy_title(self.toc_pre_title() + self.toc_title(), self.name()) 0163 0164 if self.docs: 0165 writer.p(self.docs) 0166 0167 self.print_intro(writer) 0168 0169 if self.props: 0170 writer.p("Properties:") 0171 writer.table_header("name", "type", "notes", "docs") 0172 for v in self.props: 0173 v.print(writer) 0174 writer.end_table() 0175 0176 if self.const: 0177 writer.p("Constants:") 0178 writer.table_header("name", "type", "value", "docs") 0179 for v in self.const: 0180 v.print(writer) 0181 writer.end_table() 0182 0183 if self.functions: 0184 with writer.sublevel(1): 0185 for func in self.functions: 0186 func.print(writer) 0187 0188 if self.classes: 0189 with writer.sublevel(1): 0190 for cls in self.classes: 0191 cls.print(writer) 0192 0193 def inspect(self, modules, classes): 0194 for name, val in vars(self.module).items(): 0195 if name.startswith("__") and name != "__version__": 0196 continue 0197 elif inspect.ismodule(val): 0198 submod = ModuleDocs(val) 0199 modules.append(submod) 0200 submod.inspect(modules, submod.classes) 0201 elif inspect.isfunction(val) or inspect.ismethod(val) or isinstance(val, types.BuiltinMethodType): 0202 self.functions.append(FunctionDoc(name, self.child_name(name), val)) 0203 elif inspect.isclass(val): 0204 cls = ClassDoc(self.child_name(name), val) 0205 cls.inspect([], classes) 0206 classes.append(cls) 0207 elif type(val).__name__ == "instancemethod": 0208 self.functions.append(FunctionDoc(name, self.child_name(name), val.__func__)) 0209 elif isinstance(val, property): 0210 classinfo = getattr(self.module, "__classinfo__", {}).get(name) 0211 prop = PropertyDoc(name, val, classinfo) 0212 self.props.append(prop) 0213 if classinfo: 0214 self.file_props.append(prop) 0215 elif hasattr(type(val), "__int__"): 0216 self.const.append(Constant.enum(val)) 0217 else: 0218 self.const.append(Constant(name, val if " at 0x" not in repr(val) else None, type(val))) 0219 0220 def child_name(self, child): 0221 return self.name() + "." + child 0222 0223 0224 class TypeFixer: 0225 basic = [ 0226 ("QString", "str"), 0227 ("QByteArray", "bytes"), 0228 ("QUuid", "uuid.UUID"), 0229 ("glaxnimate.__detail.__QObject", "object"), 0230 ("QColor", "glaxnimate.utils.Color"), 0231 ("QPointF", "glaxnimate.utils.Point"), 0232 ("QSizeF", "glaxnimate.utils.Size"), 0233 ("QSize", "glaxnimate.utils.IntSize"), 0234 ("QVector2D", "glaxnimate.utils.Vector2D"), 0235 ("QRectF", "glaxnimate.utils.Rect"), 0236 ("QObject", "object"), 0237 ("List[QVariant]", "list"), 0238 ("AnimatableBase", "glaxnimate.model.AnimatableBase"), 0239 ("QVariantMap", "dict"), 0240 ("QVariant", "<type>"), 0241 ("QGradientStops", "List[GradientStop]"), 0242 ("builtins.", "") 0243 ] 0244 wrong_ns = re.compile(r"\b([a-z]+)::([a-zA-Z0-9_]+)") 0245 link_re = re.compile(r"(glaxnimate\.[a-zA-Z0-9._]+\.([a-zA-Z0-9_]+))") 0246 types = {} 0247 unlinked = set() 0248 0249 @classmethod 0250 def add_class(cls, class_): 0251 cls.types[class_.__name__] = class_.__module__ + "." + class_.__qualname__ 0252 0253 @classmethod 0254 def fix(cls, text: str) -> str: 0255 for qt, py in cls.basic: 0256 text = text.replace(qt, py) 0257 text = cls.wrong_ns.sub(r"glaxnimate.\1.\2", text) 0258 return text 0259 0260 @classmethod 0261 def classhref(cls, full_name): 0262 return "#" + full_name.replace(".", "").lower() 0263 0264 @classmethod 0265 def classlink(cls, name, full_name, json): 0266 return r"[{txt}]({link})".format( 0267 txt=name, 0268 link=cls.classhref(full_name) if not json else "#" + name.lower() 0269 ) 0270 0271 @classmethod 0272 def format(cls, text: str, json=False) -> str: 0273 text = cls.fix(text) 0274 text = cls.link_re.sub( 0275 lambda m: cls.classlink(m.group(2), m.group(0), json), 0276 text 0277 ) 0278 0279 if json: 0280 text = text.replace("List[", "array of ") 0281 if text.endswith("]"): 0282 text = text[:-1] 0283 text = text.replace("GradientStop", "[Gradient Stop](#gradient-stop)") 0284 text = text.replace("uuid.UUID", "[UUID](#uuid)") 0285 text = text.replace("str", "string") 0286 text = text.replace("bytes", "Base64 string") 0287 else: 0288 text = text.replace("GradientStop", "Tuple[`float`, [Color](#glaxnimateutilscolor)]") 0289 0290 if text in cls.types: 0291 text = cls.classlink(text, cls.types[text], json) 0292 0293 if "glaxnimate" not in text and "](" not in text and " " not in text: 0294 TypeFixer.unlinked.add(text) 0295 text = "`%s`" % text 0296 return text 0297 0298 0299 class FunctionDoc: 0300 re_sig = re.compile(r"^(?:[0-9]+\. )?([a-zA-Z0-9_]+\(.*\)(?: -> .*)?)$", re.M) 0301 0302 def __init__(self, name, full_name, function): 0303 self.function = function 0304 self.name = name 0305 self.full_name = full_name 0306 self.docs = inspect.getdoc(function) 0307 if self.docs: 0308 if "Signature:" in self.docs: 0309 lines = self.docs.splitlines() 0310 self.docs = "Signature:\n\n```python\n" 0311 for i in range(len(lines)): 0312 if lines[i] == "Signature:": 0313 self.docs += TypeFixer.fix(lines[i+1]) + "\n" 0314 self.docs += "```" 0315 else: 0316 self.docs = self.docs.replace("(self: glaxnimate.__detail.__QObject", "(self") 0317 self.docs = TypeFixer.fix(self.docs) 0318 self.docs = self.re_sig.sub("```python\n\\1\n```", self.docs) 0319 0320 def print(self, writer: MdWriter): 0321 writer.fancy_title(self.toc_pre_title() + self.name + "()", self.full_name) 0322 0323 if self.docs: 0324 writer.p(self.docs) 0325 0326 def toc_pre_title(self): 0327 return "<small>%s.</small>" % self.full_name.rsplit(".", 1)[0] 0328 0329 0330 class PropertyDoc: 0331 extract_type = re.compile("-> ([^\n]+)") 0332 extract_type_qt = re.compile("Type: (.*)") 0333 extract_type_classinfo = re.compile("^property (?:([a-z]+) )?(.*)$") 0334 0335 def __init__(self, name, prop, classinfo=None): 0336 self.name = name 0337 self.prop = prop 0338 self.docs = inspect.getdoc(prop) or "" 0339 self.reference = False 0340 0341 if classinfo: 0342 match = self.extract_type_classinfo.match(classinfo) 0343 self.type = TypeFixer.fix(match.group(2)) 0344 if match.group(1) == "list": 0345 self.type = "List[%s]" % self.type 0346 elif match.group(1) == "ref": 0347 self.reference = True 0348 elif not prop.fget.__doc__: 0349 self.type = None 0350 elif "Type: " in prop.fget.__doc__: 0351 self.type = TypeFixer.fix(self.extract_type_qt.search(prop.fget.__doc__).group(1)) 0352 else: 0353 match = self.extract_type.search(prop.fget.__doc__) 0354 if match: 0355 self.type = TypeFixer.fix(match.group(1)) 0356 else: 0357 self.type = None 0358 0359 if "->" in self.docs: 0360 self.docs = "\n".join(self.docs.splitlines()[1:]) 0361 0362 self.readonly = prop.fset is None 0363 0364 def print(self, writer: MdWriter): 0365 notes = [] 0366 if self.readonly: 0367 notes.append("Read only") 0368 if self.reference: 0369 notes.append("Reference") 0370 0371 writer.table_row( 0372 writer.code(self.name), 0373 TypeFixer.format(self.type), 0374 ",".join(notes), 0375 self.docs 0376 ) 0377 0378 def print_json(self, writer: MdWriter): 0379 if self.reference: 0380 writer.table_row( 0381 writer.code(self.name), 0382 TypeFixer.format("uuid.UUID", True), 0383 "References " + TypeFixer.format(self.type, True) + ". " + self.docs 0384 ) 0385 else: 0386 writer.table_row( 0387 writer.code(self.name), 0388 TypeFixer.format(self.type, True), 0389 self.docs 0390 ) 0391 0392 0393 class ClassDoc(ModuleDocs): 0394 def __init__(self, full_name, cls): 0395 super().__init__(cls) 0396 TypeFixer.add_class(cls) 0397 self.full_name = full_name 0398 self.bases = [ 0399 base 0400 for base in cls.__bases__ 0401 if "__" not in base.__name__ and "pybind11" not in base.__name__ 0402 ] 0403 self.children = [ 0404 cls 0405 for cls in cls.__subclasses__() 0406 if "AnimatedProperty_" not in cls.__name__ 0407 ] 0408 0409 def name(self): 0410 return self.full_name 0411 0412 def toc_title(self): 0413 return self.module.__name__ 0414 0415 def toc_pre_title(self): 0416 return "<small>%s.</small>" % self.module.__module__ 0417 0418 def print_intro(self, writer: MdWriter): 0419 if self.bases: 0420 writer.p("Base classes:") 0421 for base in self.bases: 0422 writer.li(writer.a( 0423 base.__name__, 0424 TypeFixer.classhref(base.__module__ + '.' + base.__name__) 0425 )) 0426 writer.nl() 0427 0428 if self.children: 0429 writer.p("Sub classes:") 0430 for base in self.children: 0431 writer.li(writer.a( 0432 base.__name__, 0433 TypeFixer.classhref(base.__module__ + '.' + base.__name__) 0434 )) 0435 writer.nl() 0436 0437 def print_json(self, writer: MdWriter): 0438 writer.title(self.module.__name__, 1) 0439 if self.bases: 0440 writer.p("Base types:") 0441 for base in self.bases: 0442 writer.li(writer.a( 0443 base.__name__, 0444 "#" + base.__name__.lower() 0445 )) 0446 writer.nl() 0447 0448 if self.children: 0449 writer.p("Sub types:") 0450 for base in self.children: 0451 writer.li(writer.a( 0452 base.__name__, 0453 "#" + base.__name__.lower() 0454 )) 0455 writer.nl() 0456 0457 if self.file_props: 0458 writer.p("Properties:") 0459 0460 writer.table_header("name", "type", "docs") 0461 for v in self.file_props: 0462 v.print_json(writer) 0463 writer.end_table() 0464 0465 def print_json_enum(self, writer: MdWriter): 0466 writer.title(self.module.__name__, 1) 0467 0468 writer.table_header("value", "docs") 0469 for v in self.const: 0470 v.print_json(writer) 0471 writer.end_table() 0472 0473 0474 class Constant: 0475 def __init__(self, name, value, type): 0476 self.name = name 0477 self.value = value 0478 self.type = type.__module__ + "." + type.__qualname__ 0479 self.docs = "" 0480 0481 @classmethod 0482 def enum(cls, value): 0483 return cls(value.name, int(value), type(value)) 0484 0485 def print(self, writer: MdWriter): 0486 writer.table_row( 0487 writer.code(self.name), 0488 TypeFixer.format(self.type), 0489 writer.code(repr(self.value)) if self.value is not None else "", 0490 self.docs 0491 ) 0492 0493 def print_json(self, writer: MdWriter): 0494 writer.table_row( 0495 writer.code(self.name), 0496 self.docs 0497 ) 0498 0499 0500 class DocBuilder: 0501 def __init__(self): 0502 self.modules = [] 0503 0504 def module(self, module_obj): 0505 module = ModuleDocs(module_obj) 0506 self.modules.append(module) 0507 module.inspect(self.modules, None) 0508 0509 def print(self, py_doc_file): 0510 toc = TocItem() 0511 for module in self.modules: 0512 module.toc(toc) 0513 toc.write_toc(py_doc_file) 0514 0515 writer = MdWriter(py_doc_file) 0516 for module in self.modules: 0517 module.print(writer) 0518 0519 def print_json(self, json_doc_file): 0520 writer = MdWriter(json_doc_file) 0521 enums = [] 0522 writer.level = 2 0523 for module in self.modules: 0524 for cls in module.classes: 0525 if issubclass(cls.module, glaxnimate.model.Object): 0526 cls.print_json(writer) 0527 elif cls.const and not cls.functions and all(type(x.value) is int for x in cls.const): 0528 enums.append(cls) 0529 0530 writer.title("Enumerations") 0531 for enum in enums: 0532 enum.print_json_enum(writer) 0533 0534 0535 parser = argparse.ArgumentParser() 0536 parser.add_argument("py_doc_file") 0537 parser.add_argument("json_doc_file") 0538 ns = parser.parse_args() 0539 0540 doc = DocBuilder() 0541 doc.module(glaxnimate) 0542 0543 with open(ns.py_doc_file, "w") as py_doc_file: 0544 doc.print(py_doc_file) 0545 0546 with open(ns.json_doc_file, "w") as json_doc_file: 0547 doc.print_json(json_doc_file) 0548 0549 print(TypeFixer.unlinked)