|
5 | 5 |
|
6 | 6 | import click
|
7 | 7 | import flask
|
| 8 | +from flask.cli import AppGroup, with_appcontext, run_command, shell_command, ScriptInfo |
8 | 9 | from six.moves import urllib
|
9 | 10 |
|
10 | 11 | from keg import current_app
|
11 |
| -from keg._flask_cli import FlaskGroup, script_info_option, with_appcontext, run_command, \ |
12 |
| - shell_command |
13 | 12 | from keg.keyring import keyring as keg_keyring
|
14 | 13 |
|
15 | 14 |
|
16 |
| -class KegGroup(FlaskGroup): |
17 |
| - def __init__(self, add_default_commands=True, *args, **kwargs): |
18 |
| - FlaskGroup.__init__(self, add_default_commands=False, *args, **kwargs) |
| 15 | +class KegAppGroup(AppGroup): |
| 16 | + def __init__(self, create_app, add_default_commands=True, *args, **kwargs): |
| 17 | + self.create_app = create_app |
| 18 | + |
| 19 | + AppGroup.__init__(self, *args, **kwargs) |
19 | 20 | if add_default_commands:
|
20 | 21 | self.add_command(dev_command)
|
21 | 22 |
|
| 23 | + self._loaded_plugin_commands = False |
| 24 | + |
| 25 | + def _load_plugin_commands(self): |
| 26 | + if self._loaded_plugin_commands: |
| 27 | + return |
| 28 | + try: |
| 29 | + import pkg_resources |
| 30 | + except ImportError: |
| 31 | + self._loaded_plugin_commands = True |
| 32 | + return |
| 33 | + |
| 34 | + for ep in pkg_resources.iter_entry_points('flask.commands'): |
| 35 | + self.add_command(ep.load(), ep.name) |
| 36 | + for ep in pkg_resources.iter_entry_points('keg.commands'): |
| 37 | + self.add_command(ep.load(), ep.name) |
| 38 | + self._loaded_plugin_commands = True |
| 39 | + |
| 40 | + def list_commands(self, ctx): |
| 41 | + self._load_plugin_commands() |
| 42 | + |
| 43 | + info = ctx.ensure_object(ScriptInfo) |
| 44 | + info.load_app() |
| 45 | + rv = set(click.Group.list_commands(self, ctx)) |
| 46 | + return sorted(rv) |
| 47 | + |
| 48 | + def get_command(self, ctx, name): |
| 49 | + info = ctx.ensure_object(ScriptInfo) |
| 50 | + info.load_app() |
| 51 | + return click.Group.get_command(self, ctx, name) |
| 52 | + |
| 53 | + def main(self, *args, **kwargs): |
| 54 | + obj = kwargs.get('obj') |
| 55 | + if obj is None: |
| 56 | + obj = ScriptInfo(create_app=self.create_app) |
| 57 | + kwargs['obj'] = obj |
| 58 | + # TODO: figure out if we want to use this next line. |
| 59 | + #kwargs.setdefault('auto_envvar_prefix', 'FLASK') |
| 60 | + return AppGroup.main(self, *args, **kwargs) |
| 61 | + |
22 | 62 |
|
23 | 63 | @click.group('develop', help='Developer info and utils.')
|
24 | 64 | def dev_command():
|
@@ -244,19 +284,85 @@ def keyring_delete(key):
|
244 | 284 | flask.current_app.keyring_manager.delete(key)
|
245 | 285 |
|
246 | 286 |
|
247 |
| -def init_app_cli(appcls): |
248 |
| - |
249 |
| - # this function will be used to initialize the app along, including the value of config_profile |
250 |
| - # which can be passed on the command line. |
251 |
| - def _create_app(script_info): |
252 |
| - app = appcls() |
253 |
| - app.init(config_profile=script_info.data['config_profile']) |
254 |
| - return app |
255 |
| - |
256 |
| - @click.group(cls=KegGroup, create_app=_create_app) |
257 |
| - @script_info_option('--profile', script_info_key='config_profile', default=None, |
258 |
| - help='Name of the configuration profile to use.') |
259 |
| - def cli(**kwargs): |
260 |
| - pass |
261 |
| - |
262 |
| - return cli |
| 287 | +class CLILoader(object): |
| 288 | + """ |
| 289 | + This loader takes care of the complexity of click object setup and instantiation in the |
| 290 | + correct order so that application level CLI options are available before the Keg app is |
| 291 | + instantiated (so the options can be used to configure the app). |
| 292 | +
|
| 293 | + The order of events is: |
| 294 | +
|
| 295 | + - instantiate KegAppGroup |
| 296 | + - KegAppGroup.main() is called |
| 297 | + 1. the ScriptInfo object is instantiated |
| 298 | + 2. normal click behavior starts, including argument parsing |
| 299 | + - arguments are always parsed due to invoke_without_command=True |
| 300 | + 3. during argument parsing, option callbacks are excuted which will result in at least |
| 301 | + the profile name being saved in ScriptInfo().data |
| 302 | + 4. Normal click .main() behavior will continue which could include processing commands |
| 303 | + decorated with flask.cli.with_appcontext() or calls to KegAppGroup.list_commands() |
| 304 | + or KegAppGroup.get_command(). |
| 305 | + - ScriptInfo.init_app() will be called during any of these operations |
| 306 | + - ScriptInfo.init_app() will call self.create_app() below with the ScriptInfo |
| 307 | + instance |
| 308 | + """ |
| 309 | + def __init__(self, appcls): |
| 310 | + self.appcls = appcls |
| 311 | + # Don't store instance-level vars here. This object is only instantiated once per Keg |
| 312 | + # sub-class but can be used across multiple app instance creations. So, anything app |
| 313 | + # instance specific should go on the ScriptInfo instance (see self.options_callback()). |
| 314 | + |
| 315 | + def create_group(self): |
| 316 | + """ Create the top most click Group instance which is the entry point for any Keg app |
| 317 | + being called in a CLI context. |
| 318 | +
|
| 319 | + The return value of this context gets set on Keg.cli |
| 320 | + """ |
| 321 | + |
| 322 | + return KegAppGroup( |
| 323 | + self.create_app, |
| 324 | + params=self.create_script_options(), |
| 325 | + callback=self.main_callback, |
| 326 | + invoke_without_command=True, |
| 327 | + ) |
| 328 | + |
| 329 | + def create_app(self, script_info): |
| 330 | + """ Instantiate our app, sending CLI option values through as needed. """ |
| 331 | + return self.appcls().init(config_profile=script_info.data['profile']) |
| 332 | + |
| 333 | + def create_script_options(self): |
| 334 | + """ Create app level options, ideally that are used to configure the app itself. """ |
| 335 | + return [ |
| 336 | + click.Option(['--profile'], is_eager=True, default=None, callback=self.options_callback, |
| 337 | + help='Name of the configuration profile to use.') |
| 338 | + ] |
| 339 | + |
| 340 | + def options_callback(self, ctx, param, value): |
| 341 | + """ This method is called after argument parsing, after ScriptInfo instantiation but before |
| 342 | + create_app() is called. It's the only way to get the options into ScriptInfo.data |
| 343 | + before the Keg app instance is instantiated. |
| 344 | + """ |
| 345 | + si = ctx.ensure_object(ScriptInfo) |
| 346 | + si.data[param.name] = value |
| 347 | + |
| 348 | + def main_callback(self, **kwargs): |
| 349 | + """ |
| 350 | + Default Click behavior is to call the help method if no arguments are given to the |
| 351 | + top-most command. That's good UX, but the way Click impliments it, the help is |
| 352 | + shown before argument parsing takes place. That's bad, because it means our |
| 353 | + options_callback() is never called and the config profile isn't available when the |
| 354 | + help command calls KegAppGroup.list_commands(). |
| 355 | +
|
| 356 | + So, we force Click to always parse the args using `invoke_without_command=True` above, |
| 357 | + but when we do that, Click turns off the automatic display of help. So, we just |
| 358 | + impliment help-like behavior in this method, which gives us the same net result |
| 359 | + as default Click behavior. |
| 360 | + """ |
| 361 | + ctx = click.get_current_context() |
| 362 | + |
| 363 | + if ctx.invoked_subcommand is not None: |
| 364 | + # A subcommand is present, so arguments were passed. |
| 365 | + return |
| 366 | + |
| 367 | + click.echo(ctx.get_help(), color=ctx.color) |
| 368 | + ctx.exit() |
0 commit comments