381 lines
14 KiB
Python
381 lines
14 KiB
Python
"""
|
|
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
|
|
# <https://github.com/python/typeshed/issues/756> and
|
|
# <https://github.com/python/mypy/issues/2536>.
|
|
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()
|