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