xref: /petsc/src/binding/petsc4py/docs/source/conf.py (revision c2935fbe9ea1c87af9124628e1d99021d6bc8dae)
1# Configuration file for the Sphinx documentation builder.
2#
3# For the full list of built-in configuration values, see the documentation:
4# https://www.sphinx-doc.org/en/master/usage/configuration.html
5
6# -- Path setup --------------------------------------------------------------
7
8# If extensions (or modules to document with autodoc) are in another directory,
9# add these directories to sys.path here. If the directory is relative to the
10# documentation root, use os.path.abspath to make it absolute, like shown here.
11
12import re
13import os
14import shutil
15import sys
16import typing
17import datetime
18import importlib
19import sphobjinv
20import functools
21import pylit
22from sphinx.ext.napoleon.docstring import NumpyDocstring
23
24sys.path.insert(0, os.path.abspath('.'))
25_today = datetime.datetime.now()
26
27
28# -- Project information -----------------------------------------------------
29# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
30
31package = 'petsc4py'
32
33docdir = os.path.abspath(os.path.dirname(__file__))
34topdir = os.path.abspath(os.path.join(docdir, *[os.path.pardir]*2))
35
36def pkg_version():
37    with open(os.path.join(topdir, 'src', package, '__init__.py')) as f:
38        m = re.search(r"__version__\s*=\s*'(.*)'", f.read())
39        return m.groups()[0]
40
41
42def get_doc_branch():
43    release = 1
44    if topdir.endswith(os.path.join(os.path.sep, 'src', 'binding', package)):
45        rootdir = os.path.abspath(os.path.join(topdir, *[os.path.pardir]*3))
46        rootname = package.replace('4py', '')
47        version_h = os.path.join(rootdir, 'include', f'{rootname}version.h')
48        if os.path.exists(version_h) and os.path.isfile(version_h):
49            release_macro = f'{rootname.upper()}_VERSION_RELEASE'
50            version_re = re.compile(rf"#define\s+{release_macro}\s+([-]*\d+)")
51            with open(version_h, 'r') as f:
52                release = int(version_re.search(f.read()).groups()[0])
53    return 'release' if release else 'main'
54
55
56project = 'PETSc for Python'
57author = 'Lisandro Dalcin'
58copyright = f'{_today.year}, {author}'
59
60release = pkg_version()
61version = release.rsplit('.', 1)[0]
62
63
64# -- General configuration ---------------------------------------------------
65# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
66
67extensions = [
68    'sphinx.ext.autodoc',
69    'sphinx.ext.autosummary',
70    'sphinx.ext.intersphinx',
71    'sphinx.ext.napoleon',
72    'sphinx.ext.extlinks',
73]
74
75templates_path = ['_templates']
76exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
77
78needs_sphinx = '5.0.0'
79
80default_role = 'any'
81
82pygments_style = 'tango'
83
84nitpicky = True
85nitpick_ignore = [
86    ('envvar', 'NUMPY_INCLUDE'),
87    ('py:class', 'ndarray'),  # FIXME
88    ('py:class', 'typing_extensions.Self'),
89]
90nitpick_ignore_regex = [
91    (r'c:.*', r'MPI_.*'),
92    (r'c:.*', r'Petsc.*'),
93    (r'envvar', r'(LD_LIBRARY_)?PATH'),
94    (r'envvar', r'(MPICH|OMPI|MPIEXEC)_.*'),
95]
96
97toc_object_entries = False
98toc_object_entries_show_parents = 'hide'
99# python_use_unqualified_type_names = True
100
101autodoc_class_signature = 'separated'
102autodoc_typehints = 'description'
103autodoc_typehints_format = 'short'
104autodoc_mock_imports = []
105autodoc_type_aliases = {}
106
107autosummary_context = {
108    'synopsis': {},
109    'autotype': {},
110}
111
112# Links depends on the actual branch -> release or main
113www = f'https://gitlab.com/petsc/petsc/-/tree/{get_doc_branch()}'
114extlinks = {'sources': (f'{www}/src/binding/petsc4py/src/%s','')}
115
116napoleon_preprocess_types = True
117
118try:
119    import sphinx_rtd_theme
120    if 'sphinx_rtd_theme' not in extensions:
121        extensions.append('sphinx_rtd_theme')
122except ImportError:
123    sphinx_rtd_theme = None
124
125intersphinx_mapping = {
126    'python': ('https://docs.python.org/3/', None),
127    'numpy': ('https://numpy.org/doc/stable/', None),
128    'numpydoc': ('https://numpydoc.readthedocs.io/en/latest/', None),
129    'mpi4py': ('https://mpi4py.readthedocs.io/en/stable/', None),
130    'pyopencl': ('https://documen.tician.de/pyopencl/', None),
131    'dlpack': ('https://dmlc.github.io/dlpack/latest/', None),
132    'petsc': ('https://petsc.org/release/', None),
133}
134
135def _mangle_petsc_intersphinx():
136    """Preprocess the keys in PETSc's intersphinx inventory.
137
138    PETSc have intersphinx keys of the form:
139
140        manualpages/Vec/VecShift
141
142    instead of:
143
144        petsc.VecShift
145
146    This function downloads their object inventory and strips the leading path
147    elements so that references to PETSc names actually resolve."""
148
149    website = intersphinx_mapping['petsc'][0].partition('/release/')[0]
150    branch = get_doc_branch()
151    doc_url = f"{website}/{branch}/"
152    if 'LOC' in os.environ and os.path.isfile(os.path.join(os.environ['LOC'],'objects.inv')):
153      inventory_url=f"file://" + os.path.join(os.environ['LOC'],'objects.inv')
154    else:
155      inventory_url=f"{doc_url}objects.inv"
156    print("Using PETSC inventory from "+inventory_url)
157    inventory = sphobjinv.Inventory(url=inventory_url)
158    print(inventory)
159
160    for obj in inventory.objects:
161        if obj.name.startswith("manualpages"):
162            obj.name = "petsc." + "/".join(obj.name.split("/")[2:])
163            obj.role = "class"
164            obj.domain = "py"
165
166    new_inventory_filename = "petsc_objects.inv"
167    sphobjinv.writebytes(
168        new_inventory_filename,
169        sphobjinv.compress(inventory.data_file(contract=True))
170    )
171    intersphinx_mapping['petsc'] = (doc_url, new_inventory_filename)
172
173
174_mangle_petsc_intersphinx()
175
176
177def _setup_mpi4py_typing():
178    pkg = type(sys)('mpi4py')
179    mod = type(sys)('mpi4py.MPI')
180    mod.__package__ = pkg.__name__
181    sys.modules[pkg.__name__] = pkg
182    sys.modules[mod.__name__] = mod
183    for clsname in (
184        'Intracomm',
185        'Datatype',
186        'Op',
187    ):
188        cls = type(clsname, (), {})
189        cls.__module__ = mod.__name__
190        setattr(mod, clsname, cls)
191
192
193def _patch_domain_python():
194    from sphinx.domains.python import PythonDomain
195    PythonDomain.object_types['data'].roles += ('class',)
196
197
198def _setup_autodoc(app):
199    from sphinx.ext import autodoc
200    from sphinx.util import inspect
201    from sphinx.util import typing
202
203    #
204
205    def stringify_annotation(annotation, mode='fully-qualified-except-typing'):
206        qualname = getattr(annotation, '__qualname__', '')
207        module = getattr(annotation, '__module__', '')
208        args = getattr(annotation, '__args__', None)
209        if module == 'builtins' and qualname and args is not None:
210            args = ', '.join(stringify_annotation(a, mode) for a in args)
211            return f'{qualname}[{args}]'
212        return stringify_annotation_orig(annotation, mode)
213
214    try:
215        stringify_annotation_orig = typing.stringify_annotation
216        inspect.stringify_annotation = stringify_annotation
217        typing.stringify_annotation = stringify_annotation
218        autodoc.stringify_annotation = stringify_annotation
219        autodoc.typehints.stringify_annotation = stringify_annotation
220    except AttributeError:
221        stringify_annotation_orig = typing.stringify
222        inspect.stringify_annotation = stringify_annotation
223        typing.stringify = stringify_annotation
224        autodoc.stringify_typehint = stringify_annotation
225
226    #
227
228    class ClassDocumenterMixin:
229
230        def __init__(self, *args, **kwargs):
231            super().__init__(*args, **kwargs)
232            if self.config.autodoc_class_signature == 'separated':
233                members = self.options.members
234                special_members = self.options.special_members
235                if special_members is not None:
236                    for name in ('__new__', '__init__'):
237                        if name in members:
238                            members.remove(name)
239                        if name in special_members:
240                            special_members.remove(name)
241
242    class ClassDocumenter(
243        ClassDocumenterMixin,
244        autodoc.ClassDocumenter,
245    ):
246        pass
247
248    class ExceptionDocumenter(
249        ClassDocumenterMixin,
250        autodoc.ExceptionDocumenter,
251    ):
252        pass
253
254    app.add_autodocumenter(ClassDocumenter, override=True)
255    app.add_autodocumenter(ExceptionDocumenter, override=True)
256
257
258def _monkey_patch_returns():
259    """Rewrite the role of names in "Returns" sections.
260
261    This is needed because Napoleon uses ``:class:`` for the return types
262    and this does not work with type aliases like ``ArrayScalar``. To resolve
263    this we swap ``:class:`` for ``:any:``.
264
265    """
266    _parse_returns_section = \
267        NumpyDocstring._parse_returns_section
268
269    @functools.wraps(NumpyDocstring._parse_returns_section)
270    def wrapper(*args, **kwargs):
271        out = _parse_returns_section(*args, **kwargs)
272        return [line.replace(":class:", ":any:") for line in out]
273
274    NumpyDocstring._parse_returns_section = wrapper
275
276
277def _monkey_patch_see_also():
278    """Rewrite the role of names in "see also" sections.
279
280    Napoleon uses :obj: for all names found in "see also" sections but we
281    need :all: so that references to labels work."""
282
283    _parse_numpydoc_see_also_section = \
284        NumpyDocstring._parse_numpydoc_see_also_section
285
286    @functools.wraps(NumpyDocstring._parse_numpydoc_see_also_section)
287    def wrapper(*args, **kwargs):
288        out = _parse_numpydoc_see_also_section(*args, **kwargs)
289        return [line.replace(":obj:", ":any:") for line in out]
290
291    NumpyDocstring._parse_numpydoc_see_also_section = wrapper
292
293
294def _apply_monkey_patches():
295    """Modify Napoleon types after parsing to make references work."""
296    _monkey_patch_returns()
297    _monkey_patch_see_also()
298
299
300_apply_monkey_patches()
301
302
303def _process_demos(*demos):
304    # Convert demo .py files to rst. Also copy the .py file so it can be
305    # linked from the demo rst file.
306    try:
307        os.mkdir("demo")
308    except FileExistsError:
309        pass
310    for demo in demos:
311        demo_dir = os.path.join("demo", os.path.dirname(demo))
312        demo_src = os.path.join(os.pardir, os.pardir, "demo", demo)
313        try:
314            os.mkdir(demo_dir)
315        except FileExistsError:
316            pass
317        with open(demo_src, "r") as infile:
318            with open(os.path.join(
319                os.path.join("demo", os.path.splitext(demo)[0] + ".rst")), "w"
320            ) as outfile:
321                converter = pylit.Code2Text(infile)
322                outfile.write(str(converter))
323        demo_copy_name = os.path.join(demo_dir, os.path.basename(demo))
324        shutil.copyfile(demo_src, demo_copy_name)
325        html_static_path.append(demo_copy_name)
326    with open(os.path.join("demo", "demo.rst"), "w") as demofile:
327        demofile.write("""
328petsc4py demos
329==============
330
331.. toctree::
332
333""")
334        for demo in demos:
335            demofile.write("    " + os.path.splitext(demo)[0] + "\n")
336        demofile.write("\n")
337
338html_static_path=[]
339_process_demos(
340    "poisson2d/poisson2d.py"
341)
342
343
344def setup(app):
345    _setup_mpi4py_typing()
346    _patch_domain_python()
347    _monkey_patch_returns()
348    _monkey_patch_see_also()
349    _setup_autodoc(app)
350
351    try:
352        from petsc4py import PETSc
353    except ImportError:
354        autodoc_mock_imports.append('PETSc')
355        return
356    del PETSc.DA  # FIXME
357
358    sys_dwb = sys.dont_write_bytecode
359    sys.dont_write_bytecode = True
360    import apidoc
361    sys.dont_write_bytecode = sys_dwb
362
363    name = PETSc.__name__
364    here = os.path.abspath(os.path.dirname(__file__))
365    outdir = os.path.join(here, apidoc.OUTDIR)
366    source = os.path.join(outdir, f'{name}.py')
367    getmtime = os.path.getmtime
368    generate = (
369        not os.path.exists(source)
370        or getmtime(source) < getmtime(PETSc.__file__)
371        or getmtime(source) < getmtime(apidoc.__file__)
372    )
373    if generate:
374        apidoc.generate(source)
375    module = apidoc.load_module(source)
376    apidoc.replace_module(module)
377
378    modules = [
379        'petsc4py',
380    ]
381    typing_overload = typing.overload
382    typing.overload = lambda arg: arg
383    for name in modules:
384        mod = importlib.import_module(name)
385        ann = apidoc.load_module(f'{mod.__file__}i', name)
386        apidoc.annotate(mod, ann)
387    typing.overload = typing_overload
388
389    from petsc4py import typing as tp
390    for attr in tp.__all__:
391        autodoc_type_aliases[attr] = f'~petsc4py.typing.{attr}'
392
393# -- Options for HTML output -------------------------------------------------
394# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
395
396# The theme to use for HTML and HTML Help pages.  See the documentation for
397# a list of builtin themes.
398html_theme = 'pydata_sphinx_theme'
399
400html_theme_options = {
401    "navigation_with_keys":True
402}
403
404# -- Options for HTMLHelp output ------------------------------------------
405
406# Output file base name for HTML help builder.
407htmlhelp_basename = f'{package}-man'
408
409
410# -- Options for LaTeX output ---------------------------------------------
411
412# (source start file, target name, title,
413#  author, documentclass [howto, manual, or own class]).
414latex_documents = [
415    ('index', f'{package}.tex', project, author, 'howto'),
416]
417
418latex_elements = {
419    'papersize': 'a4',
420}
421
422
423# -- Options for manual page output ---------------------------------------
424
425# (source start file, name, description, authors, manual section).
426man_pages = [
427    ('index', package, project, [author], 3)
428]
429
430
431# -- Options for Texinfo output -------------------------------------------
432
433# (source start file, target name, title, author,
434#  dir menu entry, description, category)
435texinfo_documents = [
436    ('index', package, project, author,
437     package, f'{project}.', 'Miscellaneous'),
438]
439
440
441# -- Options for Epub output ----------------------------------------------
442
443# Output file base name for ePub builder.
444epub_basename = package
445