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