File: cli.py

package info (click to toggle)
python-stone 3.3.8-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 1,956 kB
  • sloc: python: 21,786; objc: 498; sh: 29; makefile: 11
file content (380 lines) | stat: -rw-r--r-- 13,977 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"""
A command-line interface for StoneAPI.
"""

import importlib.util
import importlib.machinery
import io
import json
import logging
import os
import sys
import traceback

from .cli_helpers import parse_route_attr_filter
from .compiler import (
    BackendException,
    Compiler,
)
from .frontend.exception import InvalidSpec
from .frontend.frontend import specs_to_ir

_MYPY = False
if _MYPY:
    import typing  # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression

import argparse

# These backends come by default
_builtin_backends = (
    'obj_c_client',
    'obj_c_types',
    'obj_c_tests',
    'js_client',
    'js_types',
    'tsd_client',
    'tsd_types',
    'python_types',
    'python_type_stubs',
    'python_client',
    'swift_types',
    'swift_client',
)

# The parser for command line arguments
_cmdline_description = (
    'Write your APIs in Stone. Use backends to translate your specification '
    'into a target language or format. The following describes arguments to '
    'the Stone CLI. To specify arguments that are specific to a backend, '
    'add "--" followed by arguments. For example, "stone python_client . '
    'example.spec -- -h".'
)
_cmdline_parser = argparse.ArgumentParser(description=_cmdline_description)
_cmdline_parser.add_argument(
    '-v',
    '--verbose',
    action='count',
    help='Print debugging statements.',
)
_backend_help = (
    'Either the name of a built-in backend or the path to a backend '
    'module. Paths to backend modules must end with a .stoneg.py extension. '
    'The following backends are built-in: ' + ', '.join(_builtin_backends))
_cmdline_parser.add_argument(
    'backend',
    type=str,
    help=_backend_help,
)
_cmdline_parser.add_argument(
    'output',
    type=str,
    help='The folder to save generated files to.',
)
_cmdline_parser.add_argument(
    'spec',
    nargs='*',
    type=str,
    help=('Path to API specifications. Each must have a .stone extension. '
          'If omitted or set to "-", the spec is read from stdin. Multiple '
          'namespaces can be provided over stdin by concatenating multiple '
          'specs together.'),
)
_cmdline_parser.add_argument(
    '--clean-build',
    action='store_true',
    help='The path to the template SDK for the target language.',
)
_cmdline_parser.add_argument(
    '-f',
    '--filter-by-route-attr',
    type=str,
    help=('Removes routes that do not match the expression. The expression '
          'must specify a route attribute on the left-hand side and a value '
          'on the right-hand side. Use quotes for strings and bytes. The only '
          'supported operators are "=" and "!=". For example, if "hide" is a '
          'route attribute, we can use this filter: "hide!=true". You can '
          'combine multiple expressions with "and"/"or" and use parentheses '
          'to enforce precedence.'),
)
_cmdline_parser.add_argument(
    '-r',
    '--route-whitelist-filter',
    type=str,
    help=('Restrict datatype generation to only the routes specified in the whitelist '
          'and their dependencies. Input should be a file containing a JSON dict with '
          'the following form: {"route_whitelist": {}, "datatype_whitelist": {}} '
          'where each object maps namespaces to lists of routes or datatypes to whitelist.'),
)
_cmdline_parser.add_argument(
    '-a',
    '--attribute',
    action='append',
    type=str,
    default=[],
    help=('Route attributes that the backend will have access to and '
          'presumably expose in generated code. Use ":all" to select all '
          'attributes defined in stone_cfg.Route. Note that you can filter '
          '(-f) by attributes that are not listed here.'),
)
_filter_ns_group = _cmdline_parser.add_mutually_exclusive_group()
_filter_ns_group.add_argument(
    '-w',
    '--whitelist-namespace-routes',
    action='append',
    type=str,
    default=[],
    help='If set, backends will only see the specified namespaces as having routes.',
)
_filter_ns_group.add_argument(
    '-b',
    '--blacklist-namespace-routes',
    action='append',
    type=str,
    default=[],
    help='If set, backends will not see any routes for the specified namespaces.',
)

def main():
    """The entry point for the program."""
    if '--' in sys.argv:
        cli_args = sys.argv[1:sys.argv.index('--')]
        backend_args = sys.argv[sys.argv.index('--') + 1:]
    else:
        cli_args = sys.argv[1:]
        backend_args = []

    args = _cmdline_parser.parse_args(cli_args)
    debug = False
    if args.verbose is None:
        logging_level = logging.WARNING
    elif args.verbose == 1:
        logging_level = logging.INFO
    elif args.verbose == 2:
        logging_level = logging.DEBUG
        debug = True
    else:
        print('error: I can only be so garrulous, try -vv.', file=sys.stderr)
        sys.exit(1)

    logging.basicConfig(level=logging_level)

    if args.spec and args.spec[0].startswith('+') and args.spec[0].endswith('.py'):
        # Hack: Special case for defining a spec in Python for testing purposes
        # Use this if you want to define a Stone spec using a Python module.
        # The module should should contain an api variable that references a
        # :class:`stone.api.Api` object.
        try:
            api_module = _load_module(args.api[0])
            api = api_module.api  # pylint: disable=redefined-outer-name
        except ImportError as e:
            print('error: Could not import API description due to:',
                  e, file=sys.stderr)
            sys.exit(1)
    else:
        if args.spec:
            specs = []
            read_from_stdin = False
            for spec_path in args.spec:
                if spec_path == '-':
                    read_from_stdin = True
                elif not spec_path.endswith('.stone'):
                    print("error: Specification '%s' must have a .stone extension."
                          % spec_path,
                          file=sys.stderr)
                    sys.exit(1)
                elif not os.path.exists(spec_path):
                    print("error: Specification '%s' cannot be found." % spec_path,
                          file=sys.stderr)
                    sys.exit(1)
                else:
                    with open(spec_path, encoding='utf-8') as f:
                        specs.append((spec_path, f.read()))
            if read_from_stdin and specs:
                print("error: Do not specify stdin and specification files "
                      "simultaneously.", file=sys.stderr)
                sys.exit(1)

        if not args.spec or read_from_stdin:
            specs = []
            if debug:
                print('Reading specification from stdin.')

            stdin_buffer = sys.stdin.buffer  # pylint: disable=no-member,useless-suppression
            stdin_text = io.TextIOWrapper(stdin_buffer, encoding='utf-8').read()

            parts = stdin_text.split('namespace')
            if len(parts) == 1:
                specs.append(('stdin.1', parts[0]))
            else:
                specs.append(
                    ('stdin.1', '{}namespace{}'.format(parts.pop(0), parts.pop(0))))
                while parts:
                    specs.append(('stdin.%s' % (len(specs) + 1),
                                  'namespace%s' % parts.pop(0)))

        if args.filter_by_route_attr:
            route_filter, route_filter_errors = parse_route_attr_filter(
                args.filter_by_route_attr, debug)
            if route_filter_errors:
                print('Error(s) in route filter:', file=sys.stderr)
                for err in route_filter_errors:
                    print(err, file=sys.stderr)
                sys.exit(1)

        else:
            route_filter = None

        if args.route_whitelist_filter:
            with open(args.route_whitelist_filter, encoding='utf-8') as f:
                route_whitelist_filter = json.loads(f.read())
        else:
            route_whitelist_filter = None

        try:
            # TODO: Needs version
            api = specs_to_ir(specs, debug=debug,
                              route_whitelist_filter=route_whitelist_filter)
        except InvalidSpec as e:
            print('{}:{}: error: {}'.format(e.path, e.lineno, e.msg), file=sys.stderr)
            if debug:
                print('A traceback is included below in case this is a bug in '
                      'Stone.\n', traceback.format_exc(), file=sys.stderr)
            sys.exit(1)
        if api is None:
            print('You must fix the above parsing errors for generation to '
                  'continue.', file=sys.stderr)
            sys.exit(1)

        if args.whitelist_namespace_routes:
            for namespace_name in args.whitelist_namespace_routes:
                if namespace_name not in api.namespaces:
                    print('error: Whitelisted namespace missing from spec: %s' %
                          namespace_name, file=sys.stderr)
                    sys.exit(1)
            for namespace in api.namespaces.values():
                if namespace.name not in args.whitelist_namespace_routes:
                    namespace.routes = []
                    namespace.route_by_name = {}
                    namespace.routes_by_name = {}

        if args.blacklist_namespace_routes:
            for namespace_name in args.blacklist_namespace_routes:
                if namespace_name not in api.namespaces:
                    print('error: Blacklisted namespace missing from spec: %s' %
                          namespace_name, file=sys.stderr)
                    sys.exit(1)
                else:
                    namespace = api.namespaces[namespace_name]
                    namespace.routes = []
                    namespace.route_by_name = {}
                    namespace.routes_by_name = {}

        if route_filter:
            for namespace in api.namespaces.values():
                filtered_routes = []
                for route in namespace.routes:
                    if route_filter.eval(route):
                        filtered_routes.append(route)

                namespace.routes = []
                namespace.route_by_name = {}
                namespace.routes_by_name = {}
                for route in filtered_routes:
                    namespace.add_route(route)

        if args.attribute:
            attrs = set(args.attribute)
            if ':all' in attrs:
                attrs = {field.name for field in api.route_schema.fields}
        else:
            attrs = set()

        for namespace in api.namespaces.values():
            for route in namespace.routes:
                for k in list(route.attrs.keys()):
                    if k not in attrs:
                        del route.attrs[k]

        # Remove attrs that weren't specified from the route schema
        for field in api.route_schema.fields[:]:
            if field.name not in attrs:
                api.route_schema.fields.remove(field)
                del api.route_schema._fields_by_name[field.name]
            else:
                attrs.remove(field.name)

        # Error if specified attr isn't even a field in the route schema
        if attrs:
            attr = attrs.pop()
            print('error: Attribute not defined in stone_cfg.Route: %s' %
                  attr, file=sys.stderr)
            sys.exit(1)

    if args.backend in _builtin_backends:
        backend_module = __import__(
            'stone.backends.%s' % args.backend, fromlist=[''])
    elif not os.path.exists(args.backend):
        print("error: Backend '%s' cannot be found." % args.backend,
              file=sys.stderr)
        sys.exit(1)
    elif not os.path.isfile(args.backend):
        print("error: Backend '%s' must be a file." % args.backend,
              file=sys.stderr)
        sys.exit(1)
    elif not Compiler.is_stone_backend(args.backend):
        print("error: Backend '%s' must have a .stoneg.py extension." %
              args.backend, file=sys.stderr)
        sys.exit(1)
    else:
        # A bit hacky, but we add the folder that the backend is in to our
        # python path to support the case where the backend imports other
        # files in its local directory.
        new_python_path = os.path.dirname(args.backend)
        if new_python_path not in sys.path:
            sys.path.append(new_python_path)
        try:
            backend_module = _load_module(args.backend)
        except Exception:
            print("error: Importing backend '%s' module raised an exception:" %
                  args.backend, file=sys.stderr)
            raise

    c = Compiler(
        api,
        backend_module,
        backend_args,
        args.output,
        clean_build=args.clean_build,
    )
    try:
        c.build()
    except BackendException as e:
        print('%s: error: %s raised an exception:\n%s' %
              (args.backend, e.backend_name, e.traceback),
              file=sys.stderr)
        sys.exit(1)

    if not sys.argv[0].endswith('stone'):
        # If we aren't running from an entry_point, then return api to make it
        # easier to do debugging.
        return api

def _load_module(path):
    file_name = os.path.basename(path)
    module_name = file_name.replace('.', '_')

    if sys.version_info[0] == 3 and sys.version_info[1] >= 5:
        module_specs = importlib.util.spec_from_file_location(module_name, path)
        module = importlib.util.module_from_spec(module_specs)
        module_specs.loader.exec_module(module)
    else:
        loader = importlib.machinery.SourceFileLoader(module_name, path)
        module = loader.load_module()  # pylint: disable=deprecated-method,no-value-for-parameter

    sys.modules[module_name] = module
    logging.info("Loading module: %s", module_name)
    return module

if __name__ == '__main__':
    # Assign api variable for easy debugging from a Python console
    api = main()