xref: /petsc/src/binding/petsc4py/docs/source/conf.py (revision 822ddb90084969dca3f8c89ec247fcef7ec1ec76)
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, *p, **kw):
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, *p, **kw) for a in args)
224            return f'{qualname}[{args}]'
225        return stringify_annotation_orig(annotation, *p, **kw)
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    inspect.TypeAliasForwardRef.__repr__ = lambda self: self.name
240
241    #
242
243    class ClassDocumenterMixin:
244        def __init__(self, *args, **kwargs):
245            super().__init__(*args, **kwargs)
246            if self.config.autodoc_class_signature == 'separated':
247                members = self.options.members
248                special_members = self.options.special_members
249                if special_members is not None:
250                    for name in ('__new__', '__init__'):
251                        if name in members:
252                            members.remove(name)
253                        if name in special_members:
254                            special_members.remove(name)
255
256    class ClassDocumenter(
257        ClassDocumenterMixin,
258        autodoc.ClassDocumenter,
259    ):
260        pass
261
262    class ExceptionDocumenter(
263        ClassDocumenterMixin,
264        autodoc.ExceptionDocumenter,
265    ):
266        pass
267
268    app.add_autodocumenter(ClassDocumenter, override=True)
269    app.add_autodocumenter(ExceptionDocumenter, override=True)
270
271
272def _monkey_patch_returns():
273    """Rewrite the role of names in "Returns" sections.
274
275    This is needed because Napoleon uses ``:class:`` for the return types
276    and this does not work with type aliases like ``ArrayScalar``. To resolve
277    this we swap ``:class:`` for ``:any:``.
278
279    """
280    _parse_returns_section = NumpyDocstring._parse_returns_section
281
282    @functools.wraps(NumpyDocstring._parse_returns_section)
283    def wrapper(*args, **kwargs):
284        out = _parse_returns_section(*args, **kwargs)
285        for role in (':py:class:', ':class:'):
286            out = [line.replace(role, ':any:') for line in out]
287        return out
288
289    NumpyDocstring._parse_returns_section = wrapper
290
291
292def _monkey_patch_see_also():
293    """Rewrite the role of names in "see also" sections.
294
295    Napoleon uses :obj: for all names found in "see also" sections but we
296    need :all: so that references to labels work."""
297
298    _parse_numpydoc_see_also_section = NumpyDocstring._parse_numpydoc_see_also_section
299
300    @functools.wraps(NumpyDocstring._parse_numpydoc_see_also_section)
301    def wrapper(*args, **kwargs):
302        out = _parse_numpydoc_see_also_section(*args, **kwargs)
303        for role in (':py:obj:', ':obj:'):
304            out = [line.replace(role, ':any:') for line in out]
305        return out
306
307    NumpyDocstring._parse_numpydoc_see_also_section = wrapper
308
309
310def _apply_monkey_patches():
311    """Modify Napoleon types after parsing to make references work."""
312    _monkey_patch_returns()
313    _monkey_patch_see_also()
314
315
316_apply_monkey_patches()
317
318
319def _process_demos(*demos):
320    # Convert demo .py files to rst. Also copy the .py file so it can be
321    # linked from the demo rst file.
322    try:
323        os.mkdir('demo')
324    except FileExistsError:
325        pass
326    for demo in demos:
327        demo_dir = os.path.join('demo', os.path.dirname(demo))
328        demo_src = os.path.join(os.pardir, os.pardir, 'demo', demo)
329        try:
330            os.mkdir(demo_dir)
331        except FileExistsError:
332            pass
333        with open(demo_src, 'r') as infile:
334            with open(
335                os.path.join(os.path.join('demo', os.path.splitext(demo)[0] + '.rst')),
336                'w',
337            ) as outfile:
338                converter = pylit.Code2Text(infile)
339                outfile.write(str(converter))
340        demo_copy_name = os.path.join(demo_dir, os.path.basename(demo))
341        shutil.copyfile(demo_src, demo_copy_name)
342        html_static_path.append(demo_copy_name)
343    with open(os.path.join('demo', 'demo.rst'), 'w') as demofile:
344        demofile.write("""
345petsc4py demos
346==============
347
348.. toctree::
349
350""")
351        for demo in demos:
352            demofile.write('    ' + os.path.splitext(demo)[0] + '\n')
353        demofile.write('\n')
354
355
356html_static_path = []
357_process_demos('poisson2d/poisson2d.py')
358
359
360def setup(app):
361    _setup_mpi4py_typing()
362    _patch_domain_python()
363    _monkey_patch_returns()
364    _monkey_patch_see_also()
365    _setup_autodoc(app)
366
367    try:
368        from petsc4py import PETSc
369    except ImportError:
370        autodoc_mock_imports.append('PETSc')
371        return
372    del PETSc.DA  # FIXME
373
374    sys_dwb = sys.dont_write_bytecode
375    sys.dont_write_bytecode = True
376    import apidoc
377
378    sys.dont_write_bytecode = sys_dwb
379
380    name = PETSc.__name__
381    here = os.path.abspath(os.path.dirname(__file__))
382    outdir = os.path.join(here, apidoc.OUTDIR)
383    source = os.path.join(outdir, f'{name}.py')
384    getmtime = os.path.getmtime
385    generate = (
386        not os.path.exists(source)
387        or getmtime(source) < getmtime(PETSc.__file__)
388        or getmtime(source) < getmtime(apidoc.__file__)
389    )
390    if generate:
391        apidoc.generate(source)
392    module = apidoc.load_module(source)
393    apidoc.replace_module(module)
394
395    modules = [
396        'petsc4py',
397    ]
398    typing_overload = typing.overload
399    typing.overload = lambda arg: arg
400    for name in modules:
401        mod = importlib.import_module(name)
402        ann = apidoc.load_module(f'{mod.__file__}i', name)
403        apidoc.annotate(mod, ann)
404    typing.overload = typing_overload
405
406    from petsc4py import typing as tp
407
408    for attr in tp.__all__:
409        autodoc_type_aliases[attr] = f'~petsc4py.typing.{attr}'
410
411
412# -- Options for HTML output -------------------------------------------------
413# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
414
415# The theme to use for HTML and HTML Help pages.  See the documentation for
416# a list of builtin themes.
417html_theme = 'pydata_sphinx_theme'
418
419html_theme_options = {
420    'navigation_with_keys': True,
421    'footer_end': ['theme-version', 'last-updated'],
422}
423git_describe_version = (
424    subprocess.check_output(['git', 'describe', '--always']).strip().decode('utf-8')  # noqa: S603, S607
425)
426html_last_updated_fmt = r'%Y-%m-%dT%H:%M:%S%z (' + git_describe_version + ')'
427
428# -- Options for HTMLHelp output ------------------------------------------
429
430# Output file base name for HTML help builder.
431htmlhelp_basename = f'{package}-man'
432
433
434# -- Options for LaTeX output ---------------------------------------------
435
436# (source start file, target name, title,
437#  author, documentclass [howto, manual, or own class]).
438latex_documents = [
439    ('index', f'{package}.tex', __project__, __author__, 'howto'),
440]
441
442latex_elements = {
443    'papersize': 'a4',
444}
445
446
447# -- Options for manual page output ---------------------------------------
448
449# (source start file, name, description, authors, manual section).
450man_pages = [('index', package, __project__, [__author__], 3)]
451
452
453# -- Options for Texinfo output -------------------------------------------
454
455# (source start file, target name, title, author,
456#  dir menu entry, description, category)
457texinfo_documents = [
458    (
459        'index',
460        package,
461        __project__,
462        __author__,
463        package,
464        f'{__project__}.',
465        'Miscellaneous',
466    ),
467]
468
469
470# -- Options for Epub output ----------------------------------------------
471
472# Output file base name for ePub builder.
473epub_basename = package
474