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