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