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