1# -*- coding: utf-8 -*- 2# :Project: pglast -- Extract enums from PostgreSQL headers 3# :Created: gio 03 ago 2017 14:54:39 CEST 4# :Author: Lele Gaifax <lele@metapensiero.it> 5# :License: GNU General Public License version 3 or later 6# :Copyright: © 2017, 2018 Lele Gaifax 7# 8 9from os.path import basename, splitext 10from re import match 11import subprocess 12 13from pycparser import c_ast, c_parser 14 15 16PY_HEADER = """\ 17# -*- coding: utf-8 -*- 18# :Project: pglast -- DO NOT EDIT: automatically extracted from %s @ %s 19# :Author: Lele Gaifax <lele@metapensiero.it> 20# :License: GNU General Public License version 3 or later 21# :Copyright: © 2017 Lele Gaifax 22# 23 24try: 25 from enum import Enum, IntEnum, IntFlag, auto 26except ImportError: #pragma: no cover 27 # Python < 3.6 28 from aenum import Enum, IntEnum, IntFlag, auto 29 30""" 31 32RST_HEADER = """\ 33.. -*- coding: utf-8 -*- 34.. :Project: pglast -- DO NOT EDIT: generated automatically 35.. :Author: Lele Gaifax <lele@metapensiero.it> 36.. :License: GNU General Public License version 3 or later 37.. :Copyright: © 2017 Lele Gaifax 38.. 39 40==========================================================%(extra_decoration)s 41 :mod:`pglast.enums.%(mod_name)s` --- Constants extracted from `%(header_fname)s`__ 42==========================================================%(extra_decoration)s 43 44__ %(header_url)s 45 46.. module:: pglast.enums.%(mod_name)s 47 :synopsis: Constants extracted from %(header_fname)s 48""" 49 50 51def get_libpg_query_info(): 52 "Return a tuple with (version, baseurl) of the libpg_query library." 53 54 version = subprocess.check_output(['git', 'describe', '--all', '--long'], 55 cwd='libpg_query') 56 version = version.decode('utf-8').strip().split('/')[-1] 57 remote = subprocess.check_output(['git', 'remote', 'get-url', 'origin'], 58 cwd='libpg_query') 59 remote = remote.decode('utf-8') 60 baseurl = '%s/blob/%s/' % (remote[:-5], version[-7:]) 61 return version, baseurl 62 63 64def preprocess(fname, cpp_args=[]): 65 "Preprocess the given header and return the result." 66 67 result = subprocess.check_output(['cpp', '-E', *cpp_args, fname]) 68 69 return result.decode('utf-8') 70 71 72def extract_toc(header): 73 "Extract the enums and defines with their position in the header." 74 75 toc = {} 76 77 with open(header, encoding='utf-8') as f: 78 content = f.read() 79 80 for lineno, line in enumerate(content.splitlines(), 1): 81 if line.startswith('typedef enum '): 82 m = match(r'typedef enum\s+([\w_]+)', line) 83 if m is not None: 84 toc[m.group(1)] = lineno 85 elif line.startswith('#define'): 86 m = match(r'#define\s+([A-Z_]+)', line) 87 if m is not None: 88 toc[m.group(1)] = lineno 89 90 return toc 91 92 93def extract_enums(toc, source): 94 "Yield all enum definitions belonging to the given header." 95 96 typedefs = [] 97 in_typedef = False 98 typedef = [] 99 100 for line in source.splitlines(): 101 if line and not line.startswith('#'): 102 if in_typedef: 103 typedef.append(line) 104 if line.startswith('}'): 105 in_typedef = False 106 typedefs.append(typedef) 107 typedef = [] 108 elif line.startswith('typedef enum '): 109 in_typedef = True 110 typedef.append(line) 111 112 parser = c_parser.CParser() 113 for typedef in typedefs: 114 td = parser.parse(''.join(typedef)) 115 if td.ext[0].name in toc: 116 yield td 117 118 119def extract_defines(source): 120 "Yield all #defined constants in the given header." 121 122 for line in source.splitlines(): 123 if line and line.startswith('#define'): 124 m = match(r"#define\s+([A-Z_]+)\s+\(?(\d+<<\d+|0x\d+|'[a-zA-Z]')\)?", line) 125 if m is not None: 126 yield m.group(1), m.group(2) 127 128 129def emit_constant(value): 130 return value.value 131 132 133def emit_binary_op(value): 134 assert isinstance(value.left, c_ast.Constant) 135 assert isinstance(value.right, c_ast.Constant) 136 return '%s %s %s' % (emit_constant(value.left), 137 value.op, 138 emit_constant(value.right)) 139 140 141def emit_unary_op(value): 142 return '%s%s' % (value.op, emit_constant(value.expr)) 143 144 145def int_enum_value_factory(index, enumerator): 146 if enumerator.value is None: 147 return '0' if index == 0 else 'auto()' 148 149 if isinstance(enumerator.value, c_ast.BinaryOp): 150 return emit_binary_op(enumerator.value) 151 elif isinstance(enumerator.value, c_ast.Constant): 152 return emit_constant(enumerator.value) 153 elif isinstance(enumerator.value, c_ast.UnaryOp): 154 return emit_unary_op(enumerator.value) 155 elif enumerator.value.name == 'PG_INT32_MAX': 156 return '0x7FFFFFFF' 157 158 assert enumerator.value.type == 'int' 159 return enumerator.value.value 160 161 162def char_enum_value_factory(index, enumerator): 163 assert enumerator.value.type == 'char' 164 return enumerator.value.value 165 166 167def determine_enum_type_and_value(enum): 168 type = 'IntEnum' 169 value = int_enum_value_factory 170 171 for item in enum.values.enumerators: 172 if item.value: 173 if isinstance(item.value, c_ast.Constant) and item.value.type == 'char': 174 type = 'str, Enum' 175 value = char_enum_value_factory 176 break 177 elif isinstance(item.value, c_ast.BinaryOp) and item.value.op == '<<': 178 type = 'IntFlag' 179 break 180 181 return type, value 182 183 184def write_enum(enum, output): 185 enum_type, value_factory = determine_enum_type_and_value(enum) 186 output.write('\n') 187 output.write('class %s(%s):\n' % (enum.name, enum_type)) 188 for index, item in enumerate(enum.values.enumerators): 189 output.write(' %s = %s\n' % (item.name, value_factory(index, item))) 190 191 192def write_enum_doc(enum, output, toc, url, mod_name): 193 output.write('\n\n.. class:: pglast.enums.%s.%s\n' % (mod_name, enum.name)) 194 if enum.name in toc: 195 output.write('\n Corresponds to the `%s enum <%s#L%d>`__.\n' % 196 (enum.name, url, toc[enum.name])) 197 for index, item in enumerate(enum.values.enumerators): 198 output.write('\n .. data:: %s\n' % item.name) 199 200 201def workhorse(args): 202 libpg_query_version, libpg_query_baseurl = get_libpg_query_info() 203 header_url = libpg_query_baseurl + args.header[12:] 204 toc = extract_toc(args.header) 205 preprocessed = preprocess(args.header, ['-I%s' % idir for idir in args.include_directory]) 206 with open(args.output, 'w', encoding='utf-8') as output, \ 207 open(args.rstdoc, 'w', encoding='utf-8') as rstdoc: 208 header_fname = basename(args.header) 209 mod_name = splitext(header_fname)[0] 210 output.write(PY_HEADER % (header_fname, libpg_query_version)) 211 rstdoc.write(RST_HEADER % dict( 212 mod_name=mod_name, header_fname=header_fname, 213 extra_decoration='='*(len(mod_name) + len(header_fname)), 214 header_url=header_url)) 215 216 for node in sorted(extract_enums(toc, preprocessed), 217 key=lambda x: x.ext[0].name): 218 write_enum(node.ext[0].type.type, output) 219 write_enum_doc(node.ext[0].type.type, rstdoc, toc, header_url, mod_name) 220 221 separator_emitted = False 222 with open(args.header, encoding='utf-8') as header: 223 for constant, value in extract_defines(header.read()): 224 if not separator_emitted: 225 output.write('\n\n') 226 output.write('# #define-ed constants\n') 227 rstdoc.write('\n') 228 separator_emitted = True 229 output.write('\n%s = %s\n' % (constant, value)) 230 rstdoc.write('\n.. data:: %s\n' % constant) 231 if constant in toc: 232 rstdoc.write('\n See `here for details <%s#L%d>`__.\n' 233 % (header_url, toc[constant])) 234 235 236def main(): 237 from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter 238 239 parser = ArgumentParser(description="PG enum extractor", 240 formatter_class=ArgumentDefaultsHelpFormatter) 241 242 parser.add_argument('-I', '--include-directory', action='append', metavar='DIR', 243 help="add DIR to the list of include directories") 244 parser.add_argument('header', 245 help="source header to be processed") 246 parser.add_argument('output', 247 help="Python source to be created") 248 parser.add_argument('rstdoc', 249 help="reST documentation to be created") 250 251 args = parser.parse_args() 252 253 workhorse(args) 254 255 256if __name__ == '__main__': 257 main() 258