""" A command-line interface for StoneAPI. """ from __future__ import absolute_import, division, print_function, unicode_literals import codecs import imp import io import json import logging import os import six 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 # Hack to get around some of Python 2's standard library modules that # accept ascii-encodable unicode literals in lieu of strs, but where # actually passing such literals results in errors with mypy --py2. See # and # . import importlib argparse = importlib.import_module(str('argparse')) # type: typing.Any # 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=six.text_type, help=_backend_help, ) _cmdline_parser.add_argument( 'output', type=six.text_type, help='The folder to save generated files to.', ) _cmdline_parser.add_argument( 'spec', nargs='*', type=six.text_type, 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=six.text_type, 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=six.text_type, 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 = imp.load_source('api', args.api[0]).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) 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.') if six.PY2: UTF8Reader = codecs.getreader('utf8') sys.stdin = UTF8Reader(sys.stdin) stdin_text = sys.stdin.read() else: 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', '%snamespace%s' % (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) 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('%s:%s: error: %s' % (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 = imp.load_source('user_backend', 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 if __name__ == '__main__': # Assign api variable for easy debugging from a Python console api = main()