xref: /petsc/src/binding/petsc4py/docs/source/conf.py (revision cb5db2414029547a5ccf00a1710ee072432a08af)
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    if 'LOC' in os.environ and os.path.isfile(os.path.join(os.environ['LOC'],'objects.inv')):
150      base_doc_url = os.environ['LOC']
151      url=f"file://" + os.path.join(base_doc_url,'objects.inv')
152    else:
153      website = intersphinx_mapping['petsc'][0].partition('/release/')[0]
154      branch = get_doc_branch()
155      base_doc_url = f"{website}/{branch}/"
156      url=f"{base_doc_url}objects.inv"
157    print("Using PETSC inventory from "+url)
158    inventory = sphobjinv.Inventory(url=url)
159    print(inventory)
160
161    for obj in inventory.objects:
162        if obj.name.startswith("manualpages"):
163            obj.name = "petsc." + "/".join(obj.name.split("/")[2:])
164            obj.role = "class"
165            obj.domain = "py"
166
167    new_inventory_filename = "petsc_objects.inv"
168    sphobjinv.writebytes(
169        new_inventory_filename,
170        sphobjinv.compress(inventory.data_file(contract=True))
171    )
172    intersphinx_mapping['petsc'] = (base_doc_url, new_inventory_filename)
173
174
175_mangle_petsc_intersphinx()
176
177
178def _setup_mpi4py_typing():
179    pkg = type(sys)('mpi4py')
180    mod = type(sys)('mpi4py.MPI')
181    mod.__package__ = pkg.__name__
182    sys.modules[pkg.__name__] = pkg
183    sys.modules[mod.__name__] = mod
184    for clsname in (
185        'Intracomm',
186        'Datatype',
187        'Op',
188    ):
189        cls = type(clsname, (), {})
190        cls.__module__ = mod.__name__
191        setattr(mod, clsname, cls)
192
193
194def _patch_domain_python():
195    from sphinx.domains.python import PythonDomain
196    PythonDomain.object_types['data'].roles += ('class',)
197
198
199def _setup_autodoc(app):
200    from sphinx.ext import autodoc
201    from sphinx.util import inspect
202    from sphinx.util import typing
203
204    #
205
206    def stringify_annotation(annotation, mode='fully-qualified-except-typing'):
207        qualname = getattr(annotation, '__qualname__', '')
208        module = getattr(annotation, '__module__', '')
209        args = getattr(annotation, '__args__', None)
210        if module == 'builtins' and qualname and args is not None:
211            args = ', '.join(stringify_annotation(a, mode) for a in args)
212            return f'{qualname}[{args}]'
213        return stringify_annotation_orig(annotation, mode)
214
215    try:
216        stringify_annotation_orig = typing.stringify_annotation
217        inspect.stringify_annotation = stringify_annotation
218        typing.stringify_annotation = stringify_annotation
219        autodoc.stringify_annotation = stringify_annotation
220        autodoc.typehints.stringify_annotation = stringify_annotation
221    except AttributeError:
222        stringify_annotation_orig = typing.stringify
223        inspect.stringify_annotation = stringify_annotation
224        typing.stringify = stringify_annotation
225        autodoc.stringify_typehint = stringify_annotation
226
227    #
228
229    class ClassDocumenterMixin:
230
231        def __init__(self, *args, **kwargs):
232            super().__init__(*args, **kwargs)
233            if self.config.autodoc_class_signature == 'separated':
234                members = self.options.members
235                special_members = self.options.special_members
236                if special_members is not None:
237                    for name in ('__new__', '__init__'):
238                        if name in members:
239                            members.remove(name)
240                        if name in special_members:
241                            special_members.remove(name)
242
243    class ClassDocumenter(
244        ClassDocumenterMixin,
245        autodoc.ClassDocumenter,
246    ):
247        pass
248
249    class ExceptionDocumenter(
250        ClassDocumenterMixin,
251        autodoc.ExceptionDocumenter,
252    ):
253        pass
254
255    app.add_autodocumenter(ClassDocumenter, override=True)
256    app.add_autodocumenter(ExceptionDocumenter, override=True)
257
258
259def _monkey_patch_returns():
260    """Rewrite the role of names in "Returns" sections.
261
262    This is needed because Napoleon uses ``:class:`` for the return types
263    and this does not work with type aliases like ``ArrayScalar``. To resolve
264    this we swap ``:class:`` for ``:any:``.
265
266    """
267    _parse_returns_section = \
268        NumpyDocstring._parse_returns_section
269
270    @functools.wraps(NumpyDocstring._parse_returns_section)
271    def wrapper(*args, **kwargs):
272        out = _parse_returns_section(*args, **kwargs)
273        return [line.replace(":class:", ":any:") for line in out]
274
275    NumpyDocstring._parse_returns_section = wrapper
276
277
278def _monkey_patch_see_also():
279    """Rewrite the role of names in "see also" sections.
280
281    Napoleon uses :obj: for all names found in "see also" sections but we
282    need :all: so that references to labels work."""
283
284    _parse_numpydoc_see_also_section = \
285        NumpyDocstring._parse_numpydoc_see_also_section
286
287    @functools.wraps(NumpyDocstring._parse_numpydoc_see_also_section)
288    def wrapper(*args, **kwargs):
289        out = _parse_numpydoc_see_also_section(*args, **kwargs)
290        return [line.replace(":obj:", ":any:") for line in out]
291
292    NumpyDocstring._parse_numpydoc_see_also_section = wrapper
293
294
295def _apply_monkey_patches():
296    """Modify Napoleon types after parsing to make references work."""
297    _monkey_patch_returns()
298    _monkey_patch_see_also()
299
300
301_apply_monkey_patches()
302
303
304def _process_demos(*demos):
305    # Convert demo .py files to rst. Also copy the .py file so it can be
306    # linked from the demo rst file.
307    try:
308        os.mkdir("demo")
309    except FileExistsError:
310        pass
311    for demo in demos:
312        demo_dir = os.path.join("demo", os.path.dirname(demo))
313        demo_src = os.path.join(os.pardir, os.pardir, "demo", demo)
314        try:
315            os.mkdir(demo_dir)
316        except FileExistsError:
317            pass
318        with open(demo_src, "r") as infile:
319            with open(os.path.join(
320                os.path.join("demo", os.path.splitext(demo)[0] + ".rst")), "w"
321            ) as outfile:
322                converter = pylit.Code2Text(infile)
323                outfile.write(str(converter))
324        demo_copy_name = os.path.join(demo_dir, os.path.basename(demo))
325        shutil.copyfile(demo_src, demo_copy_name)
326        html_static_path.append(demo_copy_name)
327    with open(os.path.join("demo", "demo.rst"), "w") as demofile:
328        demofile.write("""
329petsc4py demos
330==============
331
332.. toctree::
333
334""")
335        for demo in demos:
336            demofile.write("    " + os.path.splitext(demo)[0] + "\n")
337        demofile.write("\n")
338
339html_static_path=[]
340_process_demos(
341    "poisson2d/poisson2d.py"
342)
343
344
345def setup(app):
346    _setup_mpi4py_typing()
347    _patch_domain_python()
348    _monkey_patch_returns()
349    _monkey_patch_see_also()
350    _setup_autodoc(app)
351
352    try:
353        from petsc4py import PETSc
354    except ImportError:
355        autodoc_mock_imports.append('PETSc')
356        return
357    del PETSc.DA  # FIXME
358
359    sys_dwb = sys.dont_write_bytecode
360    sys.dont_write_bytecode = True
361    import apidoc
362    sys.dont_write_bytecode = sys_dwb
363
364    name = PETSc.__name__
365    here = os.path.abspath(os.path.dirname(__file__))
366    outdir = os.path.join(here, apidoc.OUTDIR)
367    source = os.path.join(outdir, f'{name}.py')
368    getmtime = os.path.getmtime
369    generate = (
370        not os.path.exists(source)
371        or getmtime(source) < getmtime(PETSc.__file__)
372        or getmtime(source) < getmtime(apidoc.__file__)
373    )
374    if generate:
375        apidoc.generate(source)
376    module = apidoc.load_module(source)
377    apidoc.replace_module(module)
378
379    modules = [
380        'petsc4py',
381    ]
382    typing_overload = typing.overload
383    typing.overload = lambda arg: arg
384    for name in modules:
385        mod = importlib.import_module(name)
386        ann = apidoc.load_module(f'{mod.__file__}i', name)
387        apidoc.annotate(mod, ann)
388    typing.overload = typing_overload
389
390    from petsc4py import typing as tp
391    for attr in tp.__all__:
392        autodoc_type_aliases[attr] = f'~petsc4py.typing.{attr}'
393
394# -- Options for HTML output -------------------------------------------------
395# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
396
397# The theme to use for HTML and HTML Help pages.  See the documentation for
398# a list of builtin themes.
399html_theme = 'pydata_sphinx_theme'
400
401html_theme_options = {
402    "navigation_with_keys":True
403}
404
405# -- Options for HTMLHelp output ------------------------------------------
406
407# Output file base name for HTML help builder.
408htmlhelp_basename = f'{package}-man'
409
410
411# -- Options for LaTeX output ---------------------------------------------
412
413# (source start file, target name, title,
414#  author, documentclass [howto, manual, or own class]).
415latex_documents = [
416    ('index', f'{package}.tex', project, author, 'howto'),
417]
418
419latex_elements = {
420    'papersize': 'a4',
421}
422
423
424# -- Options for manual page output ---------------------------------------
425
426# (source start file, name, description, authors, manual section).
427man_pages = [
428    ('index', package, project, [author], 3)
429]
430
431
432# -- Options for Texinfo output -------------------------------------------
433
434# (source start file, target name, title, author,
435#  dir menu entry, description, category)
436texinfo_documents = [
437    ('index', package, project, author,
438     package, f'{project}.', 'Miscellaneous'),
439]
440
441
442# -- Options for Epub output ----------------------------------------------
443
444# Output file base name for ePub builder.
445epub_basename = package
446