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()
|