xref: /petsc/src/binding/petsc4py/docs/source/apidoc.py (revision 03e5aca47aa9bd9d5a48c628e4311d43ab2119ff)
1import os
2import sys
3import inspect
4import textwrap
5
6
7def is_cyfunction(obj):
8    return type(obj).__name__ == 'cython_function_or_method'
9
10
11def is_function(obj):
12    return (
13        inspect.isbuiltin(obj)
14        or is_cyfunction(obj)
15        or type(obj) is type(ord)
16    )
17
18
19def is_method(obj):
20    return (
21        inspect.ismethoddescriptor(obj)
22        or inspect.ismethod(obj)
23        or is_cyfunction(obj)
24        or type(obj) in (
25            type(str.index),
26            type(str.__add__),
27            type(str.__new__),
28        )
29    )
30
31
32def is_classmethod(obj):
33    return (
34        inspect.isbuiltin(obj)
35        or type(obj).__name__ in (
36            'classmethod',
37            'classmethod_descriptor',
38        )
39    )
40
41
42def is_staticmethod(obj):
43    return (
44        type(obj).__name__ in (
45            'staticmethod',
46        )
47    )
48
49def is_constant(obj):
50    return isinstance(obj, (int, float, str, dict))
51
52def is_datadescr(obj):
53    return inspect.isdatadescriptor(obj) and not hasattr(obj, 'fget')
54
55
56def is_property(obj):
57    return inspect.isdatadescriptor(obj) and hasattr(obj, 'fget')
58
59
60def is_class(obj):
61    return inspect.isclass(obj) or type(obj) is type(int)
62
63
64class Lines(list):
65
66    INDENT = " " * 4
67    level = 0
68
69    @property
70    def add(self):
71        return self
72
73    @add.setter
74    def add(self, lines):
75        if lines is None:
76            return
77        if isinstance(lines, str):
78            lines = textwrap.dedent(lines).strip().split('\n')
79        indent = self.INDENT * self.level
80        for line in lines:
81            self.append(indent + line)
82
83
84def signature(obj):
85    doc = obj.__doc__
86    doc = doc or f"{obj.__name__}: Any"  # FIXME remove line
87    sig = doc.partition('\n')[0].split('.', 1)[-1]
88    return sig or None
89
90
91def docstring(obj):
92    doc = obj.__doc__
93    doc = doc or '' # FIXME
94    if is_class(obj):
95        doc = doc.strip()
96    else:
97        doc = doc.partition('\n')[2]
98    summary, _, docbody = doc.partition('\n')
99    summary = summary.strip()
100    docbody = textwrap.dedent(docbody).strip()
101    if docbody:
102        doc = f'"""{summary}\n\n{docbody}\n\n"""'
103    else:
104        doc = f'"""{summary}"""'
105    doc = textwrap.indent(doc, Lines.INDENT)
106    return doc
107
108
109def visit_data(constant):
110    name, value = constant
111    typename = type(value).__name__
112    kind = "Constant" if isinstance(value, int) else "Object"
113    init = f"_def({typename}, '{name}')"
114    doc = f"#: {kind} ``{name}`` of type :class:`{typename}`"
115    return f"{name}: {typename} = {init}  {doc}\n"
116
117
118def visit_function(function):
119    sig = signature(function)
120    doc = docstring(function)
121    body = Lines.INDENT + "..."
122    return f"def {sig}:\n{doc}\n{body}\n"
123
124
125def visit_method(method):
126    sig = signature(method)
127    doc = docstring(method)
128    body = Lines.INDENT + "..."
129    return f"def {sig}:\n{doc}\n{body}\n"
130
131
132def visit_datadescr(datadescr, name=None):
133    sig = signature(datadescr)
134    doc = docstring(datadescr)
135    name = sig.partition(':')[0].strip() or datadescr.__name__
136    type = sig.partition(':')[2].strip() or 'Any'
137    sig = f"{name}(self) -> {type}"
138    body = Lines.INDENT + "..."
139    return f"@property\ndef {sig}:\n{doc}\n{body}\n"
140
141
142def visit_property(prop, name=None):
143    sig = signature(prop.fget)
144    name = name or prop.fget.__name__
145    type = sig.rsplit('->', 1)[-1].strip()
146    sig = f"{name}(self) -> {type}"
147    doc = f'"""{prop.__doc__}"""'
148    doc = textwrap.indent(doc, Lines.INDENT)
149    body = Lines.INDENT + "..."
150    return f"@property\ndef {sig}:\n{doc}\n{body}\n"
151
152
153def visit_constructor(cls, name='__init__', args=None):
154    init = (name == '__init__')
155    argname = cls.__mro__[-2].__name__.lower()
156    argtype = cls.__name__
157    initarg = args or f"{argname}: Optional[{argtype}] = None"
158    selfarg = 'self' if init else 'cls'
159    rettype = 'None' if init else argtype
160    arglist = f"{selfarg}, {initarg}"
161    sig = f"{name}({arglist}) -> {rettype}"
162    ret = '...' if init else 'return super().__new__(cls)'
163    body = Lines.INDENT + ret
164    return f"def {sig}:\n{body}"
165
166
167def visit_class(cls, outer=None, done=None):
168    skip = {
169        '__doc__',
170        '__dict__',
171        '__module__',
172        '__weakref__',
173        '__pyx_vtable__',
174        '__lt__',
175        '__le__',
176        '__ge__',
177        '__gt__',
178        '__enum2str',  # FIXME refactor implemetation
179        '_traceback_', # FIXME maybe refactor?
180    }
181    special = {
182        '__len__': "__len__(self) -> int",
183        '__bool__': "__bool__(self) -> bool",
184        '__hash__': "__hash__(self) -> int",
185        '__int__': "__int__(self) -> int",
186        '__index__': "__int__(self) -> int",
187        '__str__': "__str__(self) -> str",
188        '__repr__': "__repr__(self) -> str",
189        '__eq__': "__eq__(self, other: object) -> bool",
190        '__ne__': "__ne__(self, other: object) -> bool",
191    }
192    constructor = (
193        '__new__',
194        '__init__',
195    )
196
197    qualname = cls.__name__
198    cls_name = cls.__name__
199    if outer is not None and cls_name.startswith(outer):
200        cls_name = cls_name[len(outer):]
201        qualname = f"{outer}.{cls_name}"
202
203    override = OVERRIDE.get(qualname, {})
204    done = set() if done is None else done
205    lines = Lines()
206
207    base = cls.__base__
208    if base is object:
209        lines.add = f"class {cls_name}:"
210    else:
211        lines.add = f"class {cls_name}({base.__name__}):"
212    lines.level += 1
213
214    lines.add = docstring(cls)
215
216    for name in ('__new__', '__init__', '__hash__'):
217        if name in cls.__dict__:
218            done.add(name)
219
220    dct = cls.__dict__
221    keys = list(dct.keys())
222
223    def dunder(name):
224        return name.startswith('__') and name.endswith('__')
225
226    def members(seq):
227        for name in seq:
228            if name in skip:
229                continue
230            if name in done:
231                continue
232            if dunder(name):
233                if name not in special and name not in override:
234                    done.add(name)
235                    continue
236            yield name
237
238    for name in members(keys):
239        attr = getattr(cls, name)
240        if is_class(attr):
241            done.add(name)
242            lines.add = visit_class(attr, outer=cls_name)
243            continue
244
245    for name in members(keys):
246
247        if name in override:
248            done.add(name)
249            lines.add = override[name]
250            continue
251
252        if name in special:
253            done.add(name)
254            sig = special[name]
255            lines.add = f"def {sig}: ..."
256            continue
257
258        attr = getattr(cls, name)
259
260        if is_method(attr):
261            done.add(name)
262            if name == attr.__name__:
263                obj = dct[name]
264                if is_classmethod(obj):
265                    lines.add = "@classmethod"
266                elif is_staticmethod(obj):
267                    lines.add = "@staticmethod"
268                lines.add = visit_method(attr)
269            elif False:
270                lines.add = f"{name} = {attr.__name__}"
271            continue
272
273        if is_datadescr(attr):
274            done.add(name)
275            lines.add = visit_datadescr(attr)
276            continue
277
278        if is_property(attr):
279            done.add(name)
280            lines.add = visit_property(attr, name)
281            continue
282
283        if is_constant(attr):
284            done.add(name)
285            lines.add = visit_data((name, attr))
286            continue
287
288    leftovers = [name for name in keys if
289                 name not in done and name not in skip]
290    if leftovers:
291        raise RuntimeError(f"leftovers: {leftovers}")
292
293    lines.level -= 1
294    return lines
295
296
297def visit_module(module, done=None):
298    skip = {
299        '__doc__',
300        '__name__',
301        '__loader__',
302        '__spec__',
303        '__file__',
304        '__package__',
305        '__builtins__',
306        '__pyx_capi__',
307        '__pyx_unpickle_Enum',  # FIXME review
308    }
309
310    done = set() if done is None else done
311    lines = Lines()
312
313    keys = list(module.__dict__.keys())
314    keys.sort(key=lambda name: name.startswith("_"))
315
316    constants = [
317        (name, getattr(module, name)) for name in keys
318        if all((
319            name not in done and name not in skip,
320            is_constant(getattr(module, name)),
321        ))
322    ]
323    for _, value in constants:
324        cls = type(value)
325        name = cls.__name__
326        if name in done or name in skip:
327            continue
328        if cls.__module__ == module.__name__:
329            done.add(name)
330            lines.add = visit_class(cls)
331            lines.add = ""
332    for attr in constants:
333        name, value = attr
334        done.add(name)
335        if name in OVERRIDE:
336            lines.add = OVERRIDE[name]
337        else:
338            lines.add = visit_data((name, value))
339    if constants:
340        lines.add = ""
341
342    for name in keys:
343        if name in done or name in skip:
344            continue
345        value = getattr(module, name)
346
347        if is_class(value):
348            done.add(name)
349            if value.__name__ != name:
350                continue
351            if value.__module__ != module.__name__:
352                continue
353            lines.add = visit_class(value)
354            lines.add = ""
355            instances = [
356                (k, getattr(module, k)) for k in keys
357                if all((
358                    k not in done and k not in skip,
359                    type(getattr(module, k)) is value,
360                ))
361            ]
362            for attrname, attrvalue in instances:
363                done.add(attrname)
364                lines.add = visit_data((attrname, attrvalue))
365            if instances:
366                lines.add = ""
367            continue
368
369        if is_function(value):
370            done.add(name)
371            if name == value.__name__:
372                lines.add = visit_function(value)
373            else:
374                lines.add = f"{name} = {value.__name__}"
375            continue
376
377    lines.add = ""
378    for name in keys:
379        if name in done or name in skip:
380            continue
381        value = getattr(module, name)
382        done.add(name)
383        if name in OVERRIDE:
384            lines.add = OVERRIDE[name]
385        else:
386            lines.add = visit_data((name, value))
387
388    leftovers = [name for name in keys if
389                 name not in done and name not in skip]
390    if leftovers:
391        raise RuntimeError(f"leftovers: {leftovers}")
392    return lines
393
394
395IMPORTS = """
396from __future__ import annotations
397import sys
398from typing import (
399    Any,
400    Union,
401    Literal,
402    Optional,
403    NoReturn,
404    Final,
405)
406from typing import (
407    Callable,
408    Hashable,
409    Iterable,
410    Iterator,
411    Sequence,
412    Mapping,
413)
414if sys.version_info >= (3, 11):
415    from typing import Self
416else:
417    from typing_extensions import Self
418
419import numpy
420from numpy import dtype, ndarray
421from mpi4py.MPI import (
422    Intracomm,
423    Datatype,
424    Op,
425)
426
427class _dtype:
428    def __init__(self, name):
429        self.name = name
430    def __repr__(self):
431        return self.name
432
433IntType: dtype = _dtype('IntType')
434RealType: dtype =  _dtype('RealType')
435ComplexType: dtype = _dtype('ComplexType')
436ScalarType: dtype = _dtype('ScalarType')
437"""
438
439HELPERS = """
440class _Int(int): pass
441class _Str(str): pass
442class _Float(float): pass
443class _Dict(dict): pass
444
445def _repr(obj):
446    try:
447        return obj._name
448    except AttributeError:
449        return super(obj).__repr__()
450
451def _def(cls, name):
452    if cls is int:
453       cls = _Int
454    if cls is str:
455       cls = _Str
456    if cls is float:
457       cls = _Float
458    if cls is dict:
459       cls = _Dict
460
461    obj = cls()
462    obj._name = name
463    if '__repr__' not in cls.__dict__:
464        cls.__repr__ = _repr
465    return obj
466"""
467
468OVERRIDE = {
469}
470
471TYPING = """
472from .typing import *
473"""
474
475
476def visit_petsc4py_PETSc(done=None):
477    from petsc4py import PETSc
478    lines = Lines()
479    lines.add = f'"""{PETSc.__doc__}"""'
480    lines.add = IMPORTS
481    lines.add = ""
482    lines.add = HELPERS
483    lines.add = ""
484    lines.add = visit_module(PETSc)
485    lines.add = ""
486    lines.add = TYPING
487    return lines
488
489
490def generate(filename):
491    dirname = os.path.dirname(filename)
492    os.makedirs(dirname, exist_ok=True)
493    with open(filename, 'w') as f:
494        for line in visit_petsc4py_PETSc():
495            print(line, file=f)
496
497
498def load_module(filename, name=None):
499    if name is None:
500        name, _ = os.path.splitext(
501            os.path.basename(filename))
502    module = type(sys)(name)
503    module.__file__ = filename
504    module.__package__ = name.rsplit('.', 1)[0]
505    old = replace_module(module)
506    with open(filename) as f:
507        exec(f.read(), module.__dict__)  # noqa: S102
508    restore_module(old)
509    return module
510
511
512_sys_modules = {}
513
514
515def replace_module(module):
516    name = module.__name__
517    assert name not in _sys_modules
518    _sys_modules[name] = sys.modules[name]
519    sys.modules[name] = module
520    return _sys_modules[name]
521
522
523def restore_module(module):
524    name = module.__name__
525    assert name in _sys_modules
526    sys.modules[name] = _sys_modules[name]
527    del _sys_modules[name]
528
529
530def annotate(dest, source):
531    try:
532        dest.__annotations__ = source.__annotations__
533    except AttributeError:
534        pass
535    if isinstance(dest, type):
536        for name in dest.__dict__.keys():
537            if hasattr(source, name):
538                obj = getattr(dest, name)
539                annotate(obj, getattr(source, name))
540    if isinstance(dest, type(sys)):
541        for name in dir(dest):
542            if hasattr(source, name):
543                obj = getattr(dest, name)
544                mod = getattr(obj, '__module__', None)
545                if dest.__name__ == mod:
546                    annotate(obj, getattr(source, name))
547        for name in dir(source):
548            if not hasattr(dest, name):
549                setattr(dest, name, getattr(source, name))
550
551
552OUTDIR = 'reference'
553
554if __name__ == '__main__':
555    generate(os.path.join(OUTDIR, 'petsc4py.PETSc.py'))
556