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