1import os 2import sys 3import inspect 4import textwrap 5from sphinx.util import logging 6 7logger = logging.getLogger(__name__) 8 9 10def is_cyfunction(obj): 11 return type(obj).__name__ == 'cython_function_or_method' 12 13 14def is_function(obj): 15 return inspect.isbuiltin(obj) or is_cyfunction(obj) or type(obj) is type(ord) 16 17 18def is_method(obj): 19 return ( 20 inspect.ismethoddescriptor(obj) 21 or inspect.ismethod(obj) 22 or is_cyfunction(obj) 23 or type(obj) 24 in ( 25 type(str.index), 26 type(str.__add__), 27 type(str.__new__), 28 ) 29 ) 30 31 32def is_classmethod(obj): 33 return inspect.isbuiltin(obj) or type(obj).__name__ in ( 34 'classmethod', 35 'classmethod_descriptor', 36 ) 37 38 39def is_staticmethod(obj): 40 return type(obj).__name__ in ('staticmethod',) 41 42 43def is_constant(obj): 44 return isinstance(obj, (int, float, str, dict)) 45 46 47def is_datadescr(obj): 48 return inspect.isdatadescriptor(obj) and not hasattr(obj, 'fget') 49 50 51def is_property(obj): 52 return inspect.isdatadescriptor(obj) and hasattr(obj, 'fget') 53 54 55def is_class(obj): 56 return inspect.isclass(obj) or type(obj) is type(int) 57 58 59def is_hidden(obj): 60 return obj.__qualname__.startswith('_') 61 62 63class Lines(list): 64 INDENT = ' ' * 4 65 level = 0 66 67 @property 68 def add(self): 69 return self 70 71 @add.setter 72 def add(self, lines): 73 if lines is None: 74 return 75 if isinstance(lines, str): 76 lines = textwrap.dedent(lines).strip().split('\n') 77 indent = self.INDENT * self.level 78 for line in lines: 79 self.append(indent + line) 80 81 82def signature(obj, fail=True): 83 doc = obj.__doc__ 84 if not doc: 85 if fail and not is_hidden(obj): 86 logger.warning(f'Missing signature for {obj}') 87 doc = f'{obj.__name__}: Any' 88 sig = doc.partition('\n')[0].split('.', 1)[-1] 89 return sig or None 90 91 92def docstring(obj, fail=True): 93 doc = obj.__doc__ 94 if not doc: 95 if fail and not is_hidden(obj): 96 logger.warning(f'Missing docstring for {obj}') 97 doc = '' 98 link = None 99 sig = None 100 cl = is_class(obj) 101 if cl: 102 doc = doc.strip() 103 else: 104 sig, _, doc = doc.partition('\n') 105 doc, _, link = doc.rpartition('\n') 106 107 summary, _, docbody = doc.partition('\n') 108 summary = summary.strip() 109 docbody = textwrap.dedent(docbody).strip() 110 111 # raise warning if docstring is not provided for a method 112 if not summary and not is_function(obj) and is_method(obj): 113 logger.warning(f'docstring: Missing summary for {obj}') 114 115 # warnings for docstrings that are not compliant 116 if len(summary) > 79: 117 logger.warning(f'Summary for {obj} too long.') 118 if docbody: 119 if not summary.endswith('.'): 120 logger.warning(f'Summary for {obj} does not end with period.') 121 # FIXME 122 lines = docbody.split('\n') 123 for i, line in enumerate(lines): 124 if len(line) > 79: 125 logger.warning(f'Line {i} for documentation of {obj} too long.') 126 if not cl: 127 init = ( 128 'Collective.', 129 'Not collective.', 130 'Logically collective.', 131 'Neighborwise collective.', 132 'Collective the first time it is called.', 133 ) 134 if lines[0] not in init: 135 logger.warning(f'Unexpected collectiveness for {sig}\nFound {lines[0]}') 136 137 if link: 138 linktxt, _, link = link.rpartition(' ') 139 linkloc = link.replace(':', '#L') 140 # FIXME do we want to use a special section? 141 # section = f'References\n----------`' 142 section = '\n' 143 linkbody = f':sources:`{linktxt} {link} <{linkloc}>`' 144 linkbody = f'{section}\n{linkbody}' 145 if docbody: 146 docbody = f'{docbody}\n\n{linkbody}' 147 else: 148 docbody = linkbody 149 150 if docbody: 151 doc = f'"""{summary}\n\n{docbody}\n\n"""' 152 else: 153 doc = f'"""{summary}"""' 154 return textwrap.indent(doc, Lines.INDENT) 155 156 157def visit_data(constant): 158 name, value = constant 159 typename = type(value).__name__ 160 kind = 'Constant' if isinstance(value, int) else 'Object' 161 init = f"_def({typename}, '{name}')" 162 doc = f'#: {kind} ``{name}`` of type :class:`{typename}`' 163 return f'{name}: {typename} = {init} {doc}\n' 164 165 166def visit_function(function): 167 sig = signature(function) 168 doc = docstring(function) 169 body = Lines.INDENT + '...' 170 return f'def {sig}:\n{doc}\n{body}\n' 171 172 173def visit_method(method): 174 sig = signature(method) 175 doc = docstring(method) 176 body = Lines.INDENT + '...' 177 return f'def {sig}:\n{doc}\n{body}\n' 178 179 180def visit_datadescr(datadescr, name=None): 181 sig = signature(datadescr) 182 doc = docstring(datadescr) 183 name = sig.partition(':')[0].strip() or datadescr.__name__ 184 rtype = sig.partition(':')[2].strip() or 'Any' 185 sig = f'{name}(self) -> {rtype}' 186 body = Lines.INDENT + '...' 187 return f'@property\ndef {sig}:\n{doc}\n{body}\n' 188 189 190def visit_property(prop, name=None): 191 sig = signature(prop.fget) 192 name = name or prop.fget.__name__ 193 rtype = sig.rsplit('->', 1)[-1].strip() 194 sig = f'{name}(self) -> {rtype}' 195 doc = f'"""{prop.__doc__}"""' 196 doc = textwrap.indent(doc, Lines.INDENT) 197 body = Lines.INDENT + '...' 198 return f'@property\ndef {sig}:\n{doc}\n{body}\n' 199 200 201def visit_constructor(cls, name='__init__', args=None): 202 init = name == '__init__' 203 argname = cls.__mro__[-2].__name__.lower() 204 argtype = cls.__name__ 205 initarg = args or f'{argname}: Optional[{argtype}] = None' 206 selfarg = 'self' if init else 'cls' 207 rettype = 'None' if init else argtype 208 arglist = f'{selfarg}, {initarg}' 209 sig = f'{name}({arglist}) -> {rettype}' 210 ret = '...' if init else 'return super().__new__(cls)' 211 body = Lines.INDENT + ret 212 return f'def {sig}:\n{body}' 213 214 215def visit_class(cls, outer=None, done=None): 216 skip = { 217 '__doc__', 218 '__dict__', 219 '__module__', 220 '__weakref__', 221 '__pyx_vtable__', 222 '__lt__', 223 '__le__', 224 '__ge__', 225 '__gt__', 226 '__enum2str', # FIXME refactor implementation 227 '_traceback_', # FIXME maybe refactor? 228 } 229 special = { 230 '__len__': '__len__(self) -> int', 231 '__bool__': '__bool__(self) -> bool', 232 '__hash__': '__hash__(self) -> int', 233 '__int__': '__int__(self) -> int', 234 '__index__': '__int__(self) -> int', 235 '__str__': '__str__(self) -> str', 236 '__repr__': '__repr__(self) -> str', 237 '__eq__': '__eq__(self, other: object) -> bool', 238 '__ne__': '__ne__(self, other: object) -> bool', 239 } 240 241 qualname = cls.__name__ 242 cls_name = cls.__name__ 243 if outer is not None and cls_name.startswith(outer): 244 cls_name = cls_name[len(outer) :] 245 qualname = f'{outer}.{cls_name}' 246 247 override = OVERRIDE.get(qualname, {}) 248 done = set() if done is None else done 249 lines = Lines() 250 251 base = cls.__base__ 252 if base is object: 253 lines.add = f'class {cls_name}:' 254 else: 255 lines.add = f'class {cls_name}({base.__name__}):' 256 lines.level += 1 257 258 lines.add = docstring(cls) 259 260 for name in ('__new__', '__init__', '__hash__'): 261 if name in cls.__dict__: 262 done.add(name) 263 264 dct = cls.__dict__ 265 keys = list(dct.keys()) 266 267 def dunder(name): 268 return name.startswith('__') and name.endswith('__') 269 270 def members(seq): 271 for name in seq: 272 if name in skip: 273 continue 274 if name in done: 275 continue 276 if dunder(name): 277 if name not in special and name not in override: 278 done.add(name) 279 continue 280 yield name 281 282 for name in members(keys): 283 attr = getattr(cls, name) 284 if is_class(attr): 285 done.add(name) 286 lines.add = visit_class(attr, outer=cls_name) 287 continue 288 289 for name in members(keys): 290 if name in override: 291 done.add(name) 292 lines.add = override[name] 293 continue 294 295 if name in special: 296 done.add(name) 297 sig = special[name] 298 lines.add = f'def {sig}: ...' 299 continue 300 301 attr = getattr(cls, name) 302 303 if is_method(attr): 304 done.add(name) 305 if name == attr.__name__: 306 obj = dct[name] 307 if is_classmethod(obj): 308 lines.add = '@classmethod' 309 elif is_staticmethod(obj): 310 lines.add = '@staticmethod' 311 lines.add = visit_method(attr) 312 continue 313 314 if is_datadescr(attr): 315 done.add(name) 316 lines.add = visit_datadescr(attr) 317 continue 318 319 if is_property(attr): 320 done.add(name) 321 lines.add = visit_property(attr, name) 322 continue 323 324 if is_constant(attr): 325 done.add(name) 326 lines.add = visit_data((name, attr)) 327 continue 328 329 leftovers = [name for name in keys if name not in done and name not in skip] 330 if leftovers: 331 raise RuntimeError(f'leftovers: {leftovers}') 332 333 lines.level -= 1 334 return lines 335 336 337def visit_module(module, done=None): 338 skip = { 339 '__doc__', 340 '__name__', 341 '__loader__', 342 '__spec__', 343 '__file__', 344 '__package__', 345 '__builtins__', 346 '__pyx_capi__', 347 '__pyx_unpickle_Enum', # FIXME review 348 } 349 350 done = set() if done is None else done 351 lines = Lines() 352 353 keys = list(module.__dict__.keys()) 354 keys.sort(key=lambda name: name.startswith('_')) 355 356 constants = [ 357 (name, getattr(module, name)) 358 for name in keys 359 if all( 360 ( 361 name not in done and name not in skip, 362 is_constant(getattr(module, name)), 363 ) 364 ) 365 ] 366 for _, value in constants: 367 cls = type(value) 368 name = cls.__name__ 369 if name in done or name in skip: 370 continue 371 if cls.__module__ == module.__name__: 372 done.add(name) 373 lines.add = visit_class(cls) 374 lines.add = '' 375 for attr in constants: 376 name, value = attr 377 done.add(name) 378 if name in OVERRIDE: 379 lines.add = OVERRIDE[name] 380 else: 381 lines.add = visit_data((name, value)) 382 if constants: 383 lines.add = '' 384 385 for name in keys: 386 if name in done or name in skip: 387 continue 388 value = getattr(module, name) 389 390 if is_class(value): 391 done.add(name) 392 if value.__name__ != name: 393 continue 394 if value.__module__ != module.__name__: 395 continue 396 lines.add = visit_class(value) 397 lines.add = '' 398 instances = [ 399 (k, getattr(module, k)) 400 for k in keys 401 if all( 402 ( 403 k not in done and k not in skip, 404 type(getattr(module, k)) is value, 405 ) 406 ) 407 ] 408 for attrname, attrvalue in instances: 409 done.add(attrname) 410 lines.add = visit_data((attrname, attrvalue)) 411 if instances: 412 lines.add = '' 413 continue 414 415 if is_function(value): 416 done.add(name) 417 if name == value.__name__: 418 lines.add = visit_function(value) 419 else: 420 lines.add = f'{name} = {value.__name__}' 421 continue 422 423 lines.add = '' 424 for name in keys: 425 if name in done or name in skip: 426 continue 427 value = getattr(module, name) 428 done.add(name) 429 if name in OVERRIDE: 430 lines.add = OVERRIDE[name] 431 else: 432 lines.add = visit_data((name, value)) 433 434 leftovers = [name for name in keys if name not in done and name not in skip] 435 if leftovers: 436 raise RuntimeError(f'leftovers: {leftovers}') 437 return lines 438 439 440IMPORTS = """ 441from __future__ import annotations 442import sys 443from typing import ( 444 Any, 445 Union, 446 Literal, 447 Optional, 448 NoReturn, 449 Final, 450) 451from typing import ( 452 Callable, 453 Hashable, 454 Iterable, 455 Iterator, 456 Sequence, 457 Mapping, 458) 459if sys.version_info >= (3, 11): 460 from typing import Self 461else: 462 from typing_extensions import Self 463 464import numpy 465from numpy import dtype, ndarray 466from mpi4py.MPI import ( 467 Intracomm, 468 Datatype, 469 Op, 470) 471 472class _dtype: 473 def __init__(self, name): 474 self.name = name 475 def __repr__(self): 476 return self.name 477 478IntType: dtype = _dtype('IntType') 479RealType: dtype = _dtype('RealType') 480ComplexType: dtype = _dtype('ComplexType') 481ScalarType: dtype = _dtype('ScalarType') 482""" 483 484HELPERS = """ 485class _Int(int): pass 486class _Str(str): pass 487class _Float(float): pass 488class _Dict(dict): pass 489 490def _repr(obj): 491 try: 492 return obj._name 493 except AttributeError: 494 return super(obj).__repr__() 495 496def _def(cls, name): 497 if cls is int: 498 cls = _Int 499 if cls is str: 500 cls = _Str 501 if cls is float: 502 cls = _Float 503 if cls is dict: 504 cls = _Dict 505 506 obj = cls() 507 obj._name = name 508 if '__repr__' not in cls.__dict__: 509 cls.__repr__ = _repr 510 return obj 511""" 512 513OVERRIDE = {} 514 515TYPING = """ 516from .typing import * 517""" 518 519 520def visit_petsc4py_PETSc(done=None): 521 from petsc4py import PETSc 522 523 lines = Lines() 524 lines.add = f'"""{PETSc.__doc__}"""' 525 lines.add = IMPORTS 526 lines.add = '' 527 lines.add = HELPERS 528 lines.add = '' 529 lines.add = visit_module(PETSc) 530 lines.add = '' 531 lines.add = TYPING 532 return lines 533 534 535def generate(filename): 536 dirname = os.path.dirname(filename) 537 os.makedirs(dirname, exist_ok=True) 538 with open(filename, 'w') as f: 539 for line in visit_petsc4py_PETSc(): 540 print(line, file=f) 541 542 543def load_module(filename, name=None): 544 if name is None: 545 name, _ = os.path.splitext(os.path.basename(filename)) 546 module = type(sys)(name) 547 module.__file__ = filename 548 module.__package__ = name.rsplit('.', 1)[0] 549 old = replace_module(module) 550 with open(filename) as f: 551 exec(f.read(), module.__dict__) # noqa: S102 552 restore_module(old) 553 return module 554 555 556_sys_modules = {} 557 558 559def replace_module(module): 560 name = module.__name__ 561 if name in _sys_modules: 562 raise RuntimeError(f'{name} in modules') 563 _sys_modules[name] = sys.modules[name] 564 sys.modules[name] = module 565 return _sys_modules[name] 566 567 568def restore_module(module): 569 name = module.__name__ 570 if name not in _sys_modules: 571 raise RuntimeError(f'{name} not in modules') 572 sys.modules[name] = _sys_modules[name] 573 del _sys_modules[name] 574 575 576def annotate(dest, source): 577 try: 578 dest.__annotations__ = source.__annotations__ 579 except AttributeError: 580 pass 581 if isinstance(dest, type): 582 for name in dest.__dict__.keys(): 583 if hasattr(source, name): 584 obj = getattr(dest, name) 585 annotate(obj, getattr(source, name)) 586 if isinstance(dest, type(sys)): 587 for name in dir(dest): 588 if hasattr(source, name): 589 obj = getattr(dest, name) 590 mod = getattr(obj, '__module__', None) 591 if dest.__name__ == mod: 592 annotate(obj, getattr(source, name)) 593 for name in dir(source): 594 if not hasattr(dest, name): 595 setattr(dest, name, getattr(source, name)) 596 597 598OUTDIR = 'reference' 599 600if __name__ == '__main__': 601 generate(os.path.join(OUTDIR, 'petsc4py.PETSc.py')) 602