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