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