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