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