|
| 1 | +"""Dump the runtime structure of a module as JSON. |
| 2 | +
|
| 3 | +This is used for testing stubs. |
| 4 | +
|
| 5 | +This needs to run in Python 2.7 and 3.x. |
| 6 | +""" |
| 7 | + |
| 8 | +from __future__ import print_function |
| 9 | + |
| 10 | +import importlib |
| 11 | +import json |
| 12 | +import sys |
| 13 | +import types |
| 14 | +from typing import Text |
| 15 | + |
| 16 | + |
| 17 | +if sys.version_info >= (3, 0): |
| 18 | + import inspect |
| 19 | + long = int |
| 20 | +else: |
| 21 | + import inspect2 as inspect |
| 22 | + |
| 23 | + |
| 24 | + |
| 25 | +def dump_module(id): |
| 26 | + m = importlib.import_module(id) |
| 27 | + data = module_to_json(m) |
| 28 | + print(json.dumps(data, ensure_ascii=True, indent=4, sort_keys=True)) |
| 29 | + |
| 30 | + |
| 31 | +def module_to_json(m): |
| 32 | + result = {} |
| 33 | + for name, value in m.__dict__.items(): |
| 34 | + # Filter out some useless attributes. |
| 35 | + if name in ('__file__', |
| 36 | + '__doc__', |
| 37 | + '__name__', |
| 38 | + '__builtins__', |
| 39 | + '__package__'): |
| 40 | + continue |
| 41 | + if name == '__all__': |
| 42 | + result[name] = sorted(value) |
| 43 | + else: |
| 44 | + result[name] = dump_value(value) |
| 45 | + return result |
| 46 | + |
| 47 | + |
| 48 | +def dump_value(value, depth=0): |
| 49 | + if depth > 10: |
| 50 | + return 'max_recursion_depth_exceeded' |
| 51 | + if isinstance(value, type): |
| 52 | + return dump_class(value, depth + 1) |
| 53 | + if inspect.isfunction(value): |
| 54 | + return dump_function(value) |
| 55 | + if callable(value): |
| 56 | + return 'callable' # TODO more information |
| 57 | + if isinstance(value, types.ModuleType): |
| 58 | + return 'module' # TODO module name |
| 59 | + if inspect.isdatadescriptor(value): |
| 60 | + return 'datadescriptor' |
| 61 | + |
| 62 | + if inspect.ismemberdescriptor(value): |
| 63 | + return 'memberdescriptor' |
| 64 | + return dump_simple(value) |
| 65 | + |
| 66 | + |
| 67 | +def dump_simple(value): |
| 68 | + if type(value) in (int, bool, float, str, bytes, Text, long, list, set, dict, tuple): |
| 69 | + return type(value).__name__ |
| 70 | + if value is None: |
| 71 | + return 'None' |
| 72 | + if value is inspect.Parameter.empty: |
| 73 | + return None |
| 74 | + return 'unknown' |
| 75 | + |
| 76 | + |
| 77 | +def dump_class(value, depth): |
| 78 | + return { |
| 79 | + 'type': 'class', |
| 80 | + 'attributes': dump_attrs(value, depth), |
| 81 | + } |
| 82 | + |
| 83 | + |
| 84 | +special_methods = [ |
| 85 | + '__init__', |
| 86 | + '__str__', |
| 87 | + '__int__', |
| 88 | + '__float__', |
| 89 | + '__bool__', |
| 90 | + '__contains__', |
| 91 | + '__iter__', |
| 92 | +] |
| 93 | + |
| 94 | + |
| 95 | +def dump_attrs(d, depth): |
| 96 | + result = [] |
| 97 | + seen = set() |
| 98 | + try: |
| 99 | + mro = d.mro() |
| 100 | + except TypeError: |
| 101 | + mro = [d] |
| 102 | + for base in mro: |
| 103 | + v = vars(base) |
| 104 | + for name, value in v.items(): |
| 105 | + if name not in seen: |
| 106 | + result.append([name, dump_value(value, depth + 1)]) |
| 107 | + seen.add(name) |
| 108 | + for m in special_methods: |
| 109 | + if hasattr(d, m) and m not in seen: |
| 110 | + result.append([m, dump_value(getattr(d, m), depth + 1)]) |
| 111 | + return result |
| 112 | + |
| 113 | + |
| 114 | +kind_map = { |
| 115 | + inspect.Parameter.POSITIONAL_ONLY: 'POS_ONLY', |
| 116 | + inspect.Parameter.POSITIONAL_OR_KEYWORD: 'POS_OR_KW', |
| 117 | + inspect.Parameter.VAR_POSITIONAL: 'VAR_POS', |
| 118 | + inspect.Parameter.KEYWORD_ONLY: 'KW_ONLY', |
| 119 | + inspect.Parameter.VAR_KEYWORD: 'VAR_KW', |
| 120 | +} |
| 121 | + |
| 122 | + |
| 123 | +def param_kind(p): |
| 124 | + s = kind_map[p.kind] |
| 125 | + if p.default != inspect.Parameter.empty: |
| 126 | + assert s in ('POS_ONLY', 'POS_OR_KW', 'KW_ONLY') |
| 127 | + s += '_OPT' |
| 128 | + return s |
| 129 | + |
| 130 | + |
| 131 | +def dump_function(value): |
| 132 | + try: |
| 133 | + sig = inspect.signature(value) |
| 134 | + except ValueError: |
| 135 | + # The signature call sometimes fails for some reason. |
| 136 | + return 'invalid_signature' |
| 137 | + params = list(sig.parameters.items()) |
| 138 | + return { |
| 139 | + 'type': 'function', |
| 140 | + 'args': [(name, |
| 141 | + param_kind(p), |
| 142 | + dump_simple(p.default)) |
| 143 | + for name, p in params], |
| 144 | + } |
| 145 | + |
| 146 | + |
| 147 | +if __name__ == '__main__': |
| 148 | + import sys |
| 149 | + if len(sys.argv) != 2: |
| 150 | + sys.exit('usage: dumpmodule.py module-name') |
| 151 | + dump_module(sys.argv[1]) |
0 commit comments