Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
polakowo
GitHub Repository: polakowo/vectorbt
Path: blob/master/docs/generate_api.py
1147 views
1
# This file is a modified derivative of pdoc3.
2
# Copyright (c) the pdoc3 authors.
3
# Modifications Copyright (c) 2021 Oleg Polakow. All rights reserved.
4
#
5
# Licensed under the GNU Affero General Public License v3.0 or later.
6
# See docs/LICENSE.md for the full license text.
7
8
"""Auto-generate API documentation in Markdown format."""
9
10
import ast
11
import enum
12
import importlib
13
import inspect
14
import os
15
import os.path as path
16
import re
17
import shutil
18
import subprocess
19
import sys
20
import textwrap
21
import traceback
22
import typing
23
from contextlib import contextmanager
24
from copy import copy
25
from functools import lru_cache, partial, reduce, wraps
26
from itertools import tee
27
from warnings import warn
28
29
from mako.exceptions import TopLevelLookupException
30
from mako.lookup import TemplateLookup
31
from numba.core.registry import CPUDispatcher
32
33
_get_type_hints = lru_cache()(typing.get_type_hints)
34
_UNKNOWN_MODULE = "?"
35
T = typing.TypeVar("T", "Module", "Class", "Function", "Variable")
36
37
__pdoc__ = {}
38
39
tpl_lookup = TemplateLookup(
40
cache_args=dict(cached=True, cache_type="memory"),
41
input_encoding="utf-8",
42
directories=[path.join(path.dirname(__file__), "templates")],
43
)
44
45
46
class Context(dict):
47
__pdoc__["Context.__init__"] = False
48
49
def __init__(self, *args, **kwargs):
50
super().__init__(*args, **kwargs)
51
self.blacklisted = getattr(args[0], "blacklisted", set()) if args else set()
52
53
54
global_context = Context()
55
56
57
def import_module(module, *, reload=False):
58
@contextmanager
59
def _module_path(module):
60
from os.path import abspath, dirname, isdir, isfile, split
61
62
pth = "_dummy_nonexistent"
63
module_name = inspect.getmodulename(module)
64
if isdir(module):
65
pth, module = split(abspath(module))
66
elif isfile(module) and module_name:
67
pth, module = dirname(abspath(module)), module_name
68
try:
69
sys.path.insert(0, pth)
70
yield module
71
finally:
72
sys.path.remove(pth)
73
74
if isinstance(module, Module):
75
module = module.obj
76
if isinstance(module, str):
77
with _module_path(module) as module_path:
78
try:
79
module = importlib.import_module(module_path)
80
except Exception as e:
81
raise ImportError(f"Error importing {module!r}: {e.__class__.__name__}: {e}")
82
83
assert inspect.ismodule(module)
84
if reload and not module.__name__.startswith(__name__):
85
module = importlib.reload(module)
86
for mod_key, mod in list(sys.modules.items()):
87
if mod_key.startswith(module.__name__):
88
importlib.reload(mod)
89
return module
90
91
92
def pairwise(iterable):
93
a, b = tee(iterable)
94
next(b, None)
95
return zip(a, b)
96
97
98
def pep224_docstrings(doc_obj, *, _init_tree=None):
99
if isinstance(doc_obj, Module) and doc_obj.is_namespace:
100
return {}, {}
101
102
vars = {}
103
instance_vars = {}
104
105
if _init_tree:
106
tree = _init_tree
107
else:
108
try:
109
_ = inspect.findsource(doc_obj.obj)
110
tree = ast.parse(doc_obj.source)
111
except (OSError, TypeError, SyntaxError) as exc:
112
is_builtin = getattr(doc_obj.obj, "__module__", None) == "builtins"
113
if not is_builtin:
114
warn(
115
f"Couldn't read PEP-224 variable docstrings from {doc_obj!r}: {exc}",
116
stacklevel=3 + int(isinstance(doc_obj, Class)),
117
)
118
return {}, {}
119
120
if isinstance(doc_obj, Class):
121
tree = tree.body[0]
122
for node in reversed(tree.body):
123
if isinstance(node, ast.FunctionDef) and node.name == "__init__":
124
instance_vars, _ = pep224_docstrings(doc_obj, _init_tree=node)
125
break
126
127
def get_name(assign_node):
128
if isinstance(assign_node, ast.Assign) and len(assign_node.targets) == 1:
129
target = assign_node.targets[0]
130
elif isinstance(assign_node, ast.AnnAssign):
131
target = assign_node.target
132
else:
133
return None
134
135
if not _init_tree and isinstance(target, ast.Name):
136
name = target.id
137
elif (
138
_init_tree
139
and isinstance(target, ast.Attribute)
140
and isinstance(target.value, ast.Name)
141
and target.value.id == "self"
142
):
143
name = target.attr
144
else:
145
return None
146
147
if not is_public(name) and not is_whitelisted(name, doc_obj):
148
return None
149
return name
150
151
for assign_node, str_node in pairwise(ast.iter_child_nodes(tree)):
152
if not (
153
isinstance(assign_node, (ast.Assign, ast.AnnAssign))
154
and isinstance(str_node, ast.Expr)
155
and isinstance(str_node.value, ast.Str)
156
):
157
continue
158
name = get_name(assign_node)
159
if not name:
160
continue
161
docstring = inspect.cleandoc(str_node.value.s).strip()
162
if docstring:
163
vars[name] = docstring
164
165
for assign_node in ast.iter_child_nodes(tree):
166
if not isinstance(assign_node, (ast.Assign, ast.AnnAssign)):
167
continue
168
name = get_name(assign_node)
169
if not name or name in vars:
170
continue
171
172
def _get_indent(line):
173
return len(line) - len(line.lstrip())
174
175
source_lines = doc_obj.source.splitlines()
176
assign_line = source_lines[assign_node.lineno - 1]
177
assign_indent = _get_indent(assign_line)
178
comment_lines = []
179
MARKER = "#: "
180
for line in reversed(source_lines[: assign_node.lineno - 1]):
181
if _get_indent(line) == assign_indent and line.lstrip().startswith(MARKER):
182
comment_lines.append(line.split(MARKER, maxsplit=1)[1])
183
else:
184
break
185
comment_lines = comment_lines[::-1]
186
if MARKER in assign_line:
187
comment_lines.append(assign_line.rsplit(MARKER, maxsplit=1)[1])
188
if comment_lines:
189
vars[name] = "\n".join(comment_lines)
190
191
return vars, instance_vars
192
193
194
@lru_cache()
195
def is_whitelisted(name, doc_obj):
196
refname = f"{doc_obj.refname}.{name}"
197
module = doc_obj.module
198
while module:
199
qualname = refname[len(module.refname) + 1 :]
200
if module.__pdoc__.get(qualname) or module.__pdoc__.get(refname):
201
return True
202
module = module.supermodule
203
return False
204
205
206
@lru_cache()
207
def is_blacklisted(name, doc_obj):
208
refname = f"{doc_obj.refname}.{name}"
209
module = doc_obj.module
210
while module:
211
qualname = refname[len(module.refname) + 1 :]
212
if module.__pdoc__.get(qualname) is False or module.__pdoc__.get(refname) is False:
213
return True
214
module = module.supermodule
215
return False
216
217
218
def is_public(ident_name):
219
return not ident_name.startswith("_")
220
221
222
def is_function(obj):
223
return inspect.isroutine(obj) and callable(obj)
224
225
226
def is_descriptor(obj):
227
return (
228
inspect.isdatadescriptor(obj)
229
or inspect.ismethoddescriptor(obj)
230
or inspect.isgetsetdescriptor(obj)
231
or inspect.ismemberdescriptor(obj)
232
)
233
234
235
def filter_type(type_, values):
236
if isinstance(values, dict):
237
values = values.values()
238
return [i for i in values if isinstance(i, type_)]
239
240
241
def toposort(graph):
242
items_without_deps = reduce(set.union, graph.values(), set()) - set(graph.keys())
243
yield from items_without_deps
244
ordered = items_without_deps
245
while True:
246
graph = {item: (deps - ordered) for item, deps in graph.items() if item not in ordered}
247
ordered = {item for item, deps in graph.items() if not deps}
248
yield from ordered
249
if not ordered:
250
break
251
assert not graph, f"A cyclic dependency exists amongst {graph!r}"
252
253
254
def link_inheritance(context=None):
255
if context is None:
256
context = global_context
257
graph = {cls: set(cls.mro(only_documented=True)) for cls in filter_type(Class, context)}
258
for cls in toposort(graph):
259
cls.fill_inheritance()
260
for module in filter_type(Module, context):
261
module.link_inheritance()
262
263
264
class Doc:
265
__slots__ = ("module", "name", "obj", "docstring", "inherits")
266
267
def __init__(self, name, module, obj, docstring=None):
268
self.module = module
269
self.name = name
270
self.obj = obj
271
self.docstring = (docstring or inspect.getdoc(obj) or "").strip()
272
self.inherits = None
273
274
def __repr__(self):
275
return f"<{self.__class__.__name__} {self.refname!r}>"
276
277
@property
278
@lru_cache()
279
def source(self):
280
try:
281
lines, _ = inspect.getsourcelines(self.obj)
282
except (ValueError, TypeError, OSError):
283
return ""
284
return inspect.cleandoc("".join(["\n"] + lines))
285
286
@property
287
def refname(self):
288
return self.name
289
290
@property
291
def qualname(self):
292
return getattr(self.obj, "__qualname__", self.name)
293
294
@property
295
def url_base(self):
296
return f"{self.module.url_base}#{self.refname}"
297
298
@property
299
@lru_cache()
300
def inherits_top(self):
301
top = self
302
while top.inherits:
303
top = top.inherits
304
return top
305
306
@property
307
def link(self):
308
return f'[{self.qualname}]({self.inherits_top.url_base} "{self.refname}")'
309
310
@property
311
def type_name(self):
312
return "?"
313
314
def __lt__(self, other):
315
return self.qualname < other.qualname
316
317
318
class Module(Doc):
319
__slots__ = ("supermodule", "doc", "_context", "_is_inheritance_linked", "_skipped_submodules", "_curr_dir")
320
321
def __init__(
322
self,
323
module,
324
*,
325
docfilter=None,
326
supermodule=None,
327
context=None,
328
skip_errors=False,
329
curr_dir="api",
330
):
331
if isinstance(module, str):
332
module = import_module(module)
333
super().__init__(module.__name__, self, module)
334
if self.name.endswith(".__init__") and not self.is_package:
335
self.name = self.name[: -len(".__init__")]
336
337
self._context = global_context if context is None else context
338
assert isinstance(self._context, Context)
339
340
self.supermodule = supermodule
341
self.doc = {}
342
self._is_inheritance_linked = False
343
self._skipped_submodules = set()
344
self._curr_dir = curr_dir
345
346
var_docstrings, _ = pep224_docstrings(self)
347
public_objs = []
348
349
if hasattr(self.obj, "__pdoc__all__"):
350
for name in self.obj.__pdoc__all__:
351
try:
352
obj = getattr(self.obj, name)
353
except AttributeError:
354
warn(f"Module {self.module!r} doesn't contain identifier `{name}` exported in `__pdoc__all__`")
355
continue
356
if not is_blacklisted(name, self):
357
obj = inspect.unwrap(obj)
358
public_objs.append((name, obj))
359
else:
360
361
def is_from_this_module(obj):
362
mod = inspect.getmodule(inspect.unwrap(obj))
363
return mod is None or mod.__name__ == self.obj.__name__
364
365
for name, obj in inspect.getmembers(self.obj):
366
if (is_public(name) or is_whitelisted(name, self)) and (
367
is_blacklisted(name, self) or is_from_this_module(obj) or name in var_docstrings
368
):
369
if is_blacklisted(name, self):
370
self._context.blacklisted.add(f"{self.refname}.{name}")
371
continue
372
obj = inspect.unwrap(obj)
373
public_objs.append((name, obj))
374
375
index = list(self.obj.__dict__).index
376
public_objs.sort(key=lambda i: index(i[0]))
377
378
for name, obj in public_objs:
379
if is_function(obj):
380
self.doc[name] = Function(name, self, obj)
381
elif inspect.isclass(obj):
382
self.doc[name] = Class(name, self, obj)
383
elif name in var_docstrings:
384
self.doc[name] = Variable(name, self, var_docstrings[name], obj=obj)
385
386
if self.is_package:
387
388
def iter_modules(paths_):
389
from os.path import isdir, join
390
391
for pth in paths_:
392
for file in os.listdir(pth):
393
if file.startswith((".", "__pycache__", "__init__.py")):
394
continue
395
module_name = inspect.getmodulename(file)
396
if module_name:
397
yield module_name
398
if isdir(join(pth, file)) and "." not in file:
399
yield file
400
401
for root in iter_modules(self.obj.__path__):
402
if root in self.doc:
403
continue
404
if not is_public(root) and not is_whitelisted(root, self):
405
continue
406
if is_blacklisted(root, self):
407
self._skipped_submodules.add(root)
408
continue
409
assert self.refname == self.name
410
fullname = f"{self.name}.{root}"
411
try:
412
m = Module(
413
import_module(fullname),
414
docfilter=docfilter,
415
supermodule=self,
416
context=self._context,
417
skip_errors=skip_errors,
418
curr_dir=curr_dir,
419
)
420
except Exception as ex:
421
if skip_errors:
422
warn(str(ex), Module.ImportWarning)
423
continue
424
raise
425
self.doc[root] = m
426
if m.is_namespace and not m.doc:
427
del self.doc[root]
428
self._context.pop(m.refname, None)
429
430
if docfilter:
431
for name, dobj in list(self.doc.items()):
432
if not docfilter(dobj):
433
self.doc.pop(name, None)
434
self._context.pop(dobj.refname, None)
435
436
self._context[self.refname] = self
437
for docobj in self.doc.values():
438
self._context[docobj.refname] = docobj
439
if isinstance(docobj, Class):
440
self._context.update((obj.refname, obj) for obj in docobj.doc.values())
441
442
class ImportWarning(UserWarning):
443
pass
444
445
__pdoc__["Module.ImportWarning"] = False
446
447
@property
448
def __pdoc__(self):
449
return getattr(self.obj, "__pdoc__", {})
450
451
def link_inheritance(self):
452
if self._is_inheritance_linked:
453
return
454
455
for name, docstring in self.__pdoc__.items():
456
if docstring is True:
457
continue
458
459
refname = f"{self.refname}.{name}"
460
if docstring in (False, None):
461
if docstring is None:
462
warn(
463
"Setting `__pdoc__[key] = None` is deprecated; "
464
"use `__pdoc__[key] = False` "
465
f"(key: {name!r}, module: {self.name!r})."
466
)
467
if name in self._skipped_submodules:
468
continue
469
if (
470
not name.endswith(".__init__")
471
and name not in self.doc
472
and refname not in self._context
473
and refname not in self._context.blacklisted
474
):
475
warn(f"__pdoc__-overriden key {name!r} does not exist in module {self.name!r}")
476
477
obj = self.find_ident(name)
478
cls = getattr(obj, "cls", None)
479
if cls:
480
del cls.doc[obj.name]
481
self.doc.pop(name, None)
482
self._context.pop(refname, None)
483
for key in list(self._context.keys()):
484
if key.startswith(refname + "."):
485
del self._context[key]
486
continue
487
488
dobj = self.find_ident(refname)
489
if isinstance(dobj, External):
490
continue
491
if not isinstance(docstring, str):
492
raise ValueError(
493
f"__pdoc__ dict values must be strings; __pdoc__[{name!r}] is of type {type(docstring)}"
494
)
495
dobj.docstring = inspect.cleandoc(docstring)
496
497
for c in filter_type(Class, self.doc):
498
c.link_inheritance()
499
500
self._is_inheritance_linked = True
501
502
def to_markdown(self, **kwargs):
503
return render_template("/markdown.mako", module=self, **kwargs)
504
505
@property
506
def is_package(self):
507
return hasattr(self.obj, "__path__")
508
509
@property
510
def is_namespace(self):
511
try:
512
return self.obj.__spec__.origin in (None, "namespace")
513
except AttributeError:
514
return False
515
516
def find_class(self, cls):
517
return self.find_ident(f"{cls.__module__ or _UNKNOWN_MODULE}.{cls.__qualname__}")
518
519
def find_ident(self, name):
520
_name = name.rstrip("()")
521
if _name.endswith(".__init__"):
522
_name = _name[: -len(".__init__")]
523
return (
524
self.doc.get(_name)
525
or self._context.get(_name)
526
or self._context.get(f"{self.name}.{_name}")
527
or External(name)
528
)
529
530
def filter_doc_objs(self, type_, sort=True):
531
result = filter_type(type_, self.doc)
532
return sorted(result) if sort else result
533
534
@property
535
def variables(self):
536
return self.filter_doc_objs(Variable)
537
538
@property
539
def classes(self):
540
return self.filter_doc_objs(Class)
541
542
@property
543
def functions(self):
544
return self.filter_doc_objs(Function)
545
546
@property
547
def submodules(self):
548
return self.filter_doc_objs(Module)
549
550
@property
551
def url_base(self):
552
return "/" + self._curr_dir + "/" + "/".join(self.name.split(".")[1:]) + "/"
553
554
@property
555
def type_name(self):
556
if self.is_namespace:
557
return "namespace"
558
if self.is_package:
559
return "package"
560
return "module"
561
562
@property
563
def fname(self):
564
if len(self.module.name.split(".")) == 1:
565
return self._curr_dir
566
return self.name.split(".")[-1]
567
568
569
def _getmembers_all(obj):
570
mro = obj.__mro__[:-1]
571
names = set(dir(obj))
572
for base in mro:
573
names.update(base.__dict__.keys())
574
names.update(getattr(obj, "__annotations__", {}).keys())
575
576
results = []
577
for name in names:
578
try:
579
value = getattr(obj, name)
580
except AttributeError:
581
for base in mro:
582
if name in base.__dict__:
583
value = base.__dict__[name]
584
break
585
else:
586
value = None
587
results.append((name, value))
588
return results
589
590
591
class Class(Doc):
592
__slots__ = ("doc", "_super_members")
593
594
def __init__(self, name, module, obj, *, docstring=None):
595
assert inspect.isclass(obj)
596
if docstring is None:
597
init_doc = inspect.getdoc(obj.__init__) or ""
598
if init_doc == object.__init__.__doc__:
599
init_doc = ""
600
docstring = f'{inspect.getdoc(obj) or ""}\n\n{init_doc}'.strip()
601
602
super().__init__(name, module, obj, docstring=docstring)
603
self.doc = {}
604
605
annotations = getattr(self.obj, "__annotations__", {})
606
public_objs = []
607
for _name, obj_ in _getmembers_all(self.obj):
608
if (_name in self.obj.__dict__ or _name in annotations) and (
609
is_public(_name) or is_whitelisted(_name, self)
610
):
611
if is_blacklisted(_name, self):
612
self.module._context.blacklisted.add(f"{self.refname}.{_name}")
613
continue
614
obj_ = inspect.unwrap(obj_)
615
public_objs.append((_name, obj_))
616
617
def definition_order_index(
618
name, _annot_index=list(annotations).index, _dict_index=list(self.obj.__dict__).index
619
):
620
try:
621
return _dict_index(name)
622
except ValueError:
623
pass
624
try:
625
return _annot_index(name) - len(annotations)
626
except ValueError:
627
return 9e9
628
629
public_objs.sort(key=lambda i: definition_order_index(i[0]))
630
var_docstrings, instance_var_docstrings = pep224_docstrings(self)
631
632
for name, obj_ in public_objs:
633
if is_function(obj_):
634
self.doc[name] = Function(name, self.module, obj_, cls=self)
635
else:
636
self.doc[name] = Variable(
637
name,
638
self.module,
639
docstring=(
640
var_docstrings.get(name)
641
or ((inspect.isclass(obj_) or is_descriptor(obj_)) and inspect.getdoc(obj_))
642
),
643
cls=self,
644
obj=getattr(obj_, "fget", getattr(obj_, "__get__", None)),
645
instance_var=(is_descriptor(obj_) or name in getattr(self.obj, "__slots__", ())),
646
)
647
648
for name, docstring_ in instance_var_docstrings.items():
649
self.doc[name] = Variable(
650
name, self.module, docstring_, cls=self, obj=getattr(self.obj, name, None), instance_var=True
651
)
652
653
@property
654
def refname(self):
655
return f"{self.module.name}.{self.qualname}"
656
657
def mro(self, only_documented=False):
658
classes = [self.module.find_class(c) for c in inspect.getmro(self.obj) if c not in (self.obj, object)]
659
if self in classes:
660
classes.remove(self)
661
if only_documented:
662
classes = filter_type(Class, classes)
663
return classes
664
665
@property
666
def superclasses(self):
667
return sorted(self.mro())
668
669
@property
670
def subclasses(self):
671
return sorted([self.module.find_class(c) for c in type.__subclasses__(self.obj)])
672
673
@lru_cache()
674
def params(self):
675
name = self.name + ".__init__"
676
qualname = self.qualname + ".__init__"
677
refname = self.refname + ".__init__"
678
exclusions = self.module.__pdoc__
679
if name in exclusions or qualname in exclusions or refname in exclusions:
680
return []
681
return Function.get_params(self, module=self.module)
682
683
def filter_doc_objs(self, type_, filter_func=lambda x: True, sort=True):
684
result = [obj for obj in filter_type(type_, self.doc) if not obj.inherits and filter_func(obj)]
685
return sorted(result) if sort else result
686
687
@property
688
def class_variables(self):
689
return self.filter_doc_objs(Variable, filter_func=lambda dobj: not dobj.instance_var)
690
691
@property
692
def instance_variables(self):
693
return self.filter_doc_objs(Variable, filter_func=lambda dobj: dobj.instance_var)
694
695
@property
696
def functions(self):
697
return self.filter_doc_objs(Function, filter_func=lambda dobj: not dobj.is_method)
698
699
@property
700
def methods(self):
701
return self.filter_doc_objs(Function, filter_func=lambda dobj: dobj.is_method)
702
703
@property
704
def inherited_members(self):
705
return sorted([i.inherits for i in self.doc.values() if i.inherits])
706
707
def fill_inheritance(self):
708
super_members = self._super_members = {}
709
for cls in self.mro(only_documented=True):
710
for name, dobj in cls.doc.items():
711
if name not in super_members and dobj.docstring:
712
super_members[name] = dobj
713
if name not in self.doc:
714
dobj2 = copy(dobj)
715
dobj2.cls = self
716
self.doc[name] = dobj2
717
self.module._context[dobj2.refname] = dobj2
718
719
def link_inheritance(self):
720
if not hasattr(self, "_super_members"):
721
return
722
for name, parent_dobj in self._super_members.items():
723
try:
724
dobj = self.doc[name]
725
except KeyError:
726
continue
727
if dobj.obj is parent_dobj.obj or (dobj.docstring or parent_dobj.docstring) == parent_dobj.docstring:
728
dobj.inherits = parent_dobj
729
dobj.docstring = parent_dobj.docstring
730
del self._super_members
731
732
@property
733
def type_name(self):
734
return "class"
735
736
737
def maybe_lru_cache(func):
738
cached_func = lru_cache()(func)
739
740
@wraps(func)
741
def wrapper(*args):
742
try:
743
return cached_func(*args)
744
except TypeError:
745
return func(*args)
746
747
return wrapper
748
749
750
class Function(Doc):
751
__slots__ = ("cls",)
752
753
def __init__(self, name, module, obj, *, cls=None):
754
assert callable(obj), (name, module, obj)
755
super().__init__(name, module, obj)
756
self.cls = cls
757
758
@staticmethod
759
def method_type(cls, name):
760
func = getattr(cls, name, None)
761
if inspect.ismethod(func):
762
return classmethod
763
for c in inspect.getmro(cls):
764
if name in c.__dict__:
765
if isinstance(c.__dict__[name], staticmethod):
766
return staticmethod
767
return None
768
raise RuntimeError(f"{cls}.{name} not found")
769
770
@property
771
def is_method(self):
772
assert self.cls
773
return self.method_type(self.cls.obj, self.name) is None
774
775
@property
776
def method(self):
777
warn("`Function.method` is deprecated. Use: `Function.is_method`", DeprecationWarning, stacklevel=2)
778
return self.is_method
779
780
__pdoc__["Function.method"] = False
781
782
def funcdef(self):
783
return "async def" if self.is_async else "def"
784
785
@property
786
def is_async(self):
787
try:
788
obj = inspect.unwrap(self.obj)
789
return inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj)
790
except AttributeError:
791
return False
792
793
@lru_cache()
794
def params(self):
795
return self.get_params(self, module=self.module)
796
797
@staticmethod
798
def get_params(doc_obj, module=None):
799
try:
800
if inspect.isclass(doc_obj.obj) and doc_obj.obj.__init__ is not object.__init__:
801
init_sig = inspect.signature(doc_obj.obj.__init__)
802
init_params = list(init_sig.parameters.values())
803
signature = init_sig.replace(parameters=init_params[1:])
804
else:
805
signature = inspect.signature(doc_obj.obj)
806
except ValueError:
807
signature = Function.signature_from_string(doc_obj)
808
if not signature:
809
return ["..."]
810
811
def safe_default_value(p):
812
value = p.default
813
if value is inspect.Parameter.empty:
814
return p
815
816
replacement = next(
817
(i for i in ("os.environ", "sys.stdin", "sys.stdout", "sys.stderr") if value is eval(i)),
818
None,
819
)
820
if not replacement:
821
if isinstance(value, CPUDispatcher):
822
replacement = value.py_func.__name__
823
elif isinstance(value, enum.Enum):
824
replacement = str(value)
825
elif inspect.isclass(value):
826
replacement = f"{value.__module__ or _UNKNOWN_MODULE}.{value.__qualname__}"
827
elif " at 0x" in repr(value):
828
replacement = re.sub(r" at 0x\w+", "", repr(value))
829
830
if replacement:
831
832
class mock:
833
def __repr__(self):
834
return replacement
835
836
return p.replace(default=mock())
837
return p
838
839
params = []
840
kw_only = False
841
pos_only = False
842
EMPTY = inspect.Parameter.empty
843
844
for p in signature.parameters.values():
845
if not is_public(p.name) and p.default is not EMPTY:
846
continue
847
if p.name in {"self", "cls_self"}:
848
continue
849
850
if p.kind == p.POSITIONAL_ONLY:
851
pos_only = True
852
elif pos_only:
853
params.append("/")
854
pos_only = False
855
856
if p.kind == p.VAR_POSITIONAL:
857
kw_only = True
858
if p.kind == p.KEYWORD_ONLY and not kw_only:
859
kw_only = True
860
params.append("*")
861
862
p = safe_default_value(p)
863
864
formatted = p.name
865
if p.default is not EMPTY:
866
formatted += f"={repr(p.default)}"
867
if p.kind == p.VAR_POSITIONAL:
868
formatted = f"*{formatted}"
869
elif p.kind == p.VAR_KEYWORD:
870
formatted = f"**{formatted}"
871
872
params.append(formatted)
873
874
if pos_only:
875
params.append("/")
876
877
return params
878
879
@staticmethod
880
@lru_cache()
881
def signature_from_string(self):
882
signature = None
883
for expr, cleanup_docstring, filt in (
884
(r"^{}\(.*\)(?: -> .*)?$", True, lambda s: s),
885
(r"^{}\(.*\)(?= -|$)", False, lambda s: s.replace("[", "").replace("]", "")),
886
):
887
strings = sorted(re.findall(expr.format(self.name), self.docstring, re.MULTILINE), key=len, reverse=True)
888
if strings:
889
string = filt(strings[0])
890
_locals, _globals = {}, {}
891
_globals.update({"capsule": None})
892
_globals.update(typing.__dict__)
893
_globals.update(self.module.obj.__dict__)
894
module_basename = self.module.name.rsplit(".", maxsplit=1)[-1]
895
if module_basename in string and module_basename not in _globals:
896
string = re.sub(rf"(?<!\.)\b{module_basename}\.\b", "", string)
897
try:
898
exec(f"def {string}: pass", _globals, _locals)
899
except SyntaxError:
900
continue
901
signature = inspect.signature(_locals[self.name])
902
if cleanup_docstring and len(strings) == 1:
903
self.docstring = self.docstring.replace(strings[0], "")
904
break
905
return signature
906
907
@property
908
def refname(self):
909
return f"{self.cls.refname if self.cls else self.module.refname}.{self.name}"
910
911
@property
912
def qualname(self):
913
qualname = getattr(self.obj, "__qualname__", self.name)
914
if self.cls and len(qualname.split(".")) == 1:
915
return f"{self.cls.qualname}.{self.name}"
916
return qualname
917
918
@property
919
def link(self):
920
return f'[{self.qualname}()]({self.inherits_top.url_base} "{self.refname}")'
921
922
@property
923
def type_name(self):
924
if self.cls:
925
if self.method_type(self.cls.obj, self.name) is classmethod:
926
return "class method"
927
if self.method_type(self.cls.obj, self.name) is staticmethod:
928
return "static method"
929
return "method"
930
return "function"
931
932
933
class Variable(Doc):
934
__slots__ = ("cls", "instance_var")
935
936
def __init__(self, name, module, docstring, *, obj=None, cls=None, instance_var=False):
937
super().__init__(name, module, obj, docstring)
938
self.cls = cls
939
self.instance_var = instance_var
940
941
@property
942
def refname(self):
943
return f"{self.cls.refname if self.cls else self.module.refname}.{self.name}"
944
945
@property
946
def qualname(self):
947
if self.cls:
948
return f"{self.cls.qualname}.{self.name}"
949
return self.name
950
951
@property
952
def type_name(self):
953
if self.cls:
954
if hasattr(self.cls.obj, self.name) and isinstance(getattr(self.cls.obj, self.name), property):
955
return "property"
956
if self.obj is not None:
957
return type(self.obj).__name__
958
if not self.instance_var:
959
return "class variable"
960
if self.obj is not None:
961
return type(self.obj).__name__
962
return "variable"
963
964
965
class External(Doc):
966
def __init__(self, name):
967
super().__init__(name, None, None)
968
969
@property
970
def link(self):
971
return f"`{self.name}`"
972
973
974
@contextmanager
975
def fenced_code_blocks_hidden(text):
976
def _hide(text_):
977
def _replace(match):
978
orig = match.group()
979
new = f"@{hash(orig)}@"
980
hidden[new] = orig
981
return new
982
983
return re.compile(r"^(?P<fence>```+|~~~+).*\n" r"(?:.*\n)*?" r"^(?P=fence)[ ]*(?!.)", re.MULTILINE).sub(
984
_replace, text_
985
)
986
987
def _unhide(text_):
988
for k, v in hidden.items():
989
text_ = text_.replace(k, v)
990
return text_
991
992
hidden = {}
993
result = [_hide(text)]
994
yield result
995
result[0] = _unhide(result[0])
996
997
998
class ToMarkdown:
999
@staticmethod
1000
def deflist(name, type_, desc):
1001
type_parts = re.split(r"( *(?:, | of | or |, *default(?:=|\b)|, *optional\b) *)", type_ or "")
1002
type_parts[::2] = [f"`{s}`" if s else s for s in type_parts[::2]]
1003
type_ = "".join(type_parts)
1004
1005
desc = desc or "&nbsp;"
1006
assert ToMarkdown.is_indented_4_spaces(desc)
1007
assert name or type_
1008
ret = ""
1009
if name:
1010
ret += f"**```{name.replace(', ', '```**, **```')}```**"
1011
if type_:
1012
ret += f" :&ensp;{type_}" if ret else type_
1013
ret += f"\n: {desc}\n"
1014
return ret
1015
1016
@staticmethod
1017
def is_indented_4_spaces(txt, _3_spaces_or_less=re.compile(r"\n\s{0,3}\S").search):
1018
return "\n" not in txt or not _3_spaces_or_less(txt)
1019
1020
@staticmethod
1021
def fix_indent(name, type_, desc):
1022
if not ToMarkdown.is_indented_4_spaces(desc):
1023
desc = desc.replace("\n", "\n ")
1024
return name, type_, desc
1025
1026
@staticmethod
1027
def indent(indent, text, *, clean_first=False):
1028
if clean_first:
1029
text = inspect.cleandoc(text)
1030
return re.sub(r"\n", f"\n{indent}", indent + text.rstrip())
1031
1032
@staticmethod
1033
def google(text):
1034
def _googledoc_sections(match):
1035
section, body = match.groups("")
1036
if not body:
1037
return match.group()
1038
body = textwrap.dedent(body)
1039
if section in ("Args", "Attributes"):
1040
body = re.compile(
1041
r"^([\w*]+)(?:\s*\(([^)]*)\))?\s*:\s*([^\n]*(?:\n(?: {2,}.*|$))*)",
1042
re.MULTILINE,
1043
).sub(
1044
lambda m: ToMarkdown.deflist(*ToMarkdown.fix_indent(*m.groups())),
1045
inspect.cleandoc(f"\n{body}"),
1046
)
1047
elif section in ("Returns", "Yields", "Raises", "Warns"):
1048
body = re.compile(
1049
r"^()([A-Za-z0-9_.,\s\[\](){}\-+=|*/\\]+?)\s*:\s*([^\n]*(?:\n(?: {2,}.*|$))*)",
1050
re.MULTILINE,
1051
).sub(
1052
lambda m: ToMarkdown.deflist(*ToMarkdown.fix_indent(*m.groups())),
1053
inspect.cleandoc(f"\n{body}"),
1054
)
1055
return f"__{section}__\n\n{body}"
1056
1057
return re.compile(
1058
r"^([a-zA-Z0-9_ \-]+):$\n{1}" r"( {2,}.*(?:\n?(?: {2,}.*|$))+)",
1059
re.MULTILINE,
1060
).sub(_googledoc_sections, text)
1061
1062
@staticmethod
1063
def raw_urls(text):
1064
pattern = re.compile(
1065
r"""
1066
(?P<code_span>(?<!`)(?P<fence>`+)(?!`).*?(?<!`)(?P=fence)(?!`))
1067
|
1068
(?P<markdown_link>\[.*?\]\(.*\))
1069
|
1070
(?<![<\"\'])(?P<url>(?:http|ftp)s?://[^>\s()]+(?:\([^>\s)]*\))*[^>\s)]*)
1071
""",
1072
re.VERBOSE,
1073
)
1074
return pattern.sub(lambda m: f'<{m.group("url")}>' if m.group("url") else m.group(), text)
1075
1076
@staticmethod
1077
def convert(text, *, module=None):
1078
with fenced_code_blocks_hidden(text) as result:
1079
text = result[0]
1080
text = ToMarkdown.google(text)
1081
text = ToMarkdown.raw_urls(text)
1082
if module:
1083
_linkify = partial(linkify, module=module)
1084
text = re.sub(
1085
r"(?P<inside_link>\[[^\]]*?)?"
1086
r"(?:(?<!\\)(?:\\{2})+(?=`)|(?<!\\)(?P<fence>`+)"
1087
r"(?P<code>.+?)(?<!`)"
1088
r"(?P=fence)(?!`))",
1089
lambda m: (m.group() if m.group("inside_link") or len(m.group("fence")) > 2 else _linkify(m)),
1090
text,
1091
)
1092
result[0] = text
1093
return result[0]
1094
1095
1096
class ReferenceWarning(UserWarning):
1097
pass
1098
1099
1100
def linkify(match, *, module):
1101
try:
1102
refname = match.group("code")
1103
except IndexError:
1104
refname = match.group()
1105
1106
if not re.match(r"^[\w.]+$", refname):
1107
return match.group()
1108
1109
dobj = module.find_ident(refname)
1110
if isinstance(dobj, External):
1111
if "." not in refname:
1112
return match.group()
1113
module_part = module.find_ident(refname.split(".")[0])
1114
if not isinstance(module_part, External):
1115
warn(
1116
f'Code reference `{refname}` in module "{module.refname}" does not match any documented object.',
1117
ReferenceWarning,
1118
stacklevel=3,
1119
)
1120
return dobj.link
1121
1122
1123
def format_github_link(dobj, user, repo, select_lines=True):
1124
try:
1125
commit = git_head_commit()
1126
abs_path = inspect.getfile(inspect.unwrap(dobj.obj))
1127
relpath = project_relative_path(abs_path)
1128
if os.name == "nt":
1129
relpath = relpath.replace("\\", "/")
1130
lines, start_line = inspect.getsourcelines(dobj.obj)
1131
if start_line and select_lines:
1132
start_line = start_line or 1
1133
end_line = start_line + len(lines) - 1
1134
template = "https://github.com/{user}/{repo}/blob/{commit}/{relpath}#L{start_line}-L{end_line}"
1135
else:
1136
template = "https://github.com/{user}/{repo}/blob/{commit}/{relpath}"
1137
return template.format(**locals())
1138
except Exception:
1139
if isinstance(dobj, (Variable, Function)) and getattr(dobj, "cls", None):
1140
return format_github_link(dobj.cls, user, repo, select_lines=False)
1141
if not isinstance(dobj, Module):
1142
return format_github_link(dobj.module, user, repo, select_lines=False)
1143
warn(f"format_github_link for {dobj.refname} failed:\n{traceback.format_exc()}")
1144
return None
1145
1146
1147
@lru_cache()
1148
def git_head_commit():
1149
process_args = ["git", "rev-parse", "HEAD"]
1150
try:
1151
return subprocess.check_output(process_args, universal_newlines=True).strip()
1152
except OSError as error:
1153
warn(f"git executable not found on system:\n{error}")
1154
except subprocess.CalledProcessError as error:
1155
warn(
1156
"Ensure that generator is run within a git repository.\n"
1157
f"`{' '.join(process_args)}` failed with output:\n{error.output}"
1158
)
1159
return None
1160
1161
1162
@lru_cache()
1163
def git_project_root():
1164
for cmd in (["git", "rev-parse", "--show-superproject-working-tree"], ["git", "rev-parse", "--show-toplevel"]):
1165
try:
1166
p = subprocess.check_output(cmd, universal_newlines=True).rstrip("\r\n")
1167
if p:
1168
return os.path.normpath(p)
1169
except (subprocess.CalledProcessError, OSError):
1170
pass
1171
return None
1172
1173
1174
@lru_cache()
1175
def project_relative_path(absolute_path):
1176
from distutils.sysconfig import get_python_lib
1177
1178
for prefix_path in (git_project_root() or os.getcwd(), get_python_lib()):
1179
common_path = os.path.commonpath([prefix_path, absolute_path])
1180
if os.path.samefile(common_path, prefix_path):
1181
return os.path.relpath(absolute_path, prefix_path)
1182
raise RuntimeError(
1183
f"absolute path {absolute_path!r} is not a descendant of the current working directory "
1184
"or of the system's python library."
1185
)
1186
1187
1188
@lru_cache()
1189
def str_template_fields(template):
1190
from string import Formatter
1191
1192
return [field_name for _, field_name, _, _ in Formatter().parse(template) if field_name is not None]
1193
1194
1195
def render_template(template_name, **kwargs):
1196
try:
1197
t = tpl_lookup.get_template(template_name)
1198
except TopLevelLookupException:
1199
paths = [path.join(p, template_name.lstrip("/")) for p in tpl_lookup.directories]
1200
raise OSError(f"No template found at any of: {', '.join(paths)}")
1201
return re.sub("\n\n\n+", "\n\n", t.render(**kwargs).strip())
1202
1203
1204
@contextmanager
1205
def open_write_file(filename):
1206
try:
1207
with open(filename, "w", encoding="utf-8") as f:
1208
yield f
1209
except Exception:
1210
try:
1211
os.unlink(filename)
1212
except Exception:
1213
pass
1214
raise
1215
1216
1217
def recursive_write_files(m, root_dir=".", clear=True, **kwargs):
1218
root_dir = path.join(root_dir, m.fname)
1219
if clear and os.path.exists(root_dir) and os.path.isdir(root_dir):
1220
shutil.rmtree(root_dir)
1221
1222
if m.is_package:
1223
filepath = path.join(root_dir, "index.md")
1224
else:
1225
filepath = root_dir + ".md"
1226
1227
dirpath = path.dirname(filepath)
1228
if not os.access(dirpath, os.R_OK):
1229
os.makedirs(dirpath)
1230
1231
with open_write_file(filepath) as f:
1232
f.write(m.to_markdown(**kwargs))
1233
1234
for submodule in m.submodules:
1235
recursive_write_files(submodule, root_dir=root_dir, clear=False, **kwargs)
1236
1237
1238
def generate_api(
1239
module_name,
1240
root_dir=".",
1241
clear=True,
1242
docfilter=None,
1243
reload=False,
1244
skip_errors=False,
1245
curr_dir="api",
1246
**kwargs,
1247
):
1248
m = Module(
1249
import_module(module_name, reload=reload),
1250
docfilter=docfilter,
1251
skip_errors=skip_errors,
1252
curr_dir=curr_dir,
1253
)
1254
link_inheritance()
1255
recursive_write_files(m, root_dir=root_dir, clear=clear, **kwargs)
1256
1257
1258
if __name__ == "__main__":
1259
generate_api(
1260
"vectorbt",
1261
root_dir="docs",
1262
get_icon=lambda module: None,
1263
get_tags=lambda module: set(),
1264
format_github_link=partial(format_github_link, user="polakowo", repo="vectorbt"),
1265
)
1266
1267