File indexing completed on 2024-05-19 15:24:58

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)