import click
from whisk import whisk
from whisk.project import Project
import os
[docs]class WhiskMultiCommand(click.MultiCommand):
"""
This is a custom class based on
https://github.com/pallets/click/blob/master/examples/complex/complex/cli.py
that loads click command files at run time from both the whisk module
commands and commands specified within the created project. This lets users
override existing whisk cli commands and create new commands.
Project-specific commands are only loaded if the the cwd
is the project root.
"""
[docs] def core_commands_dir(self):
"""Commands available whether in or outside a project."""
return whisk.root_module_dir() / "cli/commands"
[docs] def core_project_commands_dir(self):
"""whisk-provided project-only commands directory."""
return self.core_commands_dir() / "project"
[docs] def project_commands_dir(self):
"""Custom project commands directory."""
return Project().commands_dir
[docs] def list_commands(self, ctx):
"""
MultiCommand classes must implement this function.
This returns a set of file names across all directories.
"""
project = Project()
rv = []
rv = rv + self._command_file_names(self.core_commands_dir())
if project.in_project():
rv = rv + self._command_file_names(
self.core_project_commands_dir()
)
# Possible commands directory is deleted in project
if project.commands_dir.exists():
rv = rv + self._command_file_names(project.commands_dir)
rv.sort()
return set(rv)
[docs] def _command_file_names(self, dir_name):
"""
Returns a list of file names minus the *.py file extension.
This assumes that click commands are listed in directories
containing only click command files.
"""
rv = []
for filename in os.listdir(dir_name):
if filename != '__init__.py' and filename.endswith(".py"):
rv.append(filename[:-3])
rv.sort()
return rv
[docs] def _eval_file(self, fn):
"""
Evals the given filename, return the 'cli' namespace.
The file must contain a 'cli' click command or group.
If the file doesn't exist, nothing is returned.
"""
if not os.path.isfile(fn):
return
ns = {}
with open(fn) as f:
code = compile(f.read(), fn, 'exec')
eval(code, ns, ns)
if 'cli' in ns:
return ns['cli']
else:
# The code was eval'd but did not have a `cli`
# key in the namespace.
return
[docs] def get_command(self, ctx, name):
"""
MultiCommand classes must implement this function.
Returns the CLI command w/the given name. The following load order
is used:
* Project-specific commands
* Core commands
* Core project-specific commands
If a command is found it is returned immediately. For example, if
both a custom project-specific command and a core project-specific
command exist with the same name the custom command is returned,
overriding the default.
"""
project = Project()
if project.in_project():
fn = os.path.join(self.project_commands_dir(), name + '.py')
ns = self._eval_file(fn)
if ns:
return ns
fn = os.path.join(self.core_commands_dir(), name + '.py')
ns = self._eval_file(fn)
if ns:
return ns
fn = os.path.join(self.core_project_commands_dir(), name + '.py')
ns = self._eval_file(fn)
return ns