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