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