Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add automatic CLI generation from GW tasks #314

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions girder_worker/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from girder_worker_utils.decorators import GWFuncDesc
from girder_worker.cli.arguments import (
VarsArgCli,
KwargsArgCli,
PositionalArgCli,
KeywordArgCli)

GWFuncDesc.VarsArgCls = VarsArgCli
GWFuncDesc.KwargsArgCls = KwargsArgCli
GWFuncDesc.PositionalArgCls = PositionalArgCli
GWFuncDesc.KeywordArgCls = KeywordArgCli
87 changes: 87 additions & 0 deletions girder_worker/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import click
import pkg_resources as pr
from click.core import Command, Argument, Option
from girder_worker.entrypoint import get_extensions, get_extension_tasks, import_all_includes
from stevedore import driver
from girder_worker_utils.decorators import GWFuncDesc

GWRUN_ENTRYPOINT_GROUP = 'gwrun_output_handlers'


def _cast_to_command(f):
if isinstance(f, Command):
return f

description = GWFuncDesc.get_description(f)
kotfic marked this conversation as resolved.
Show resolved Hide resolved

if description is not None:
for arg in reversed(description.arguments):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not loop over description.positional_args and description.keyword_args?

# Decorator here will be of type click.option or click.argument
arg.decorate(f)


# TODO - hide help_ implementation detail in its own function
help_ = None
# If it quacks like a celery task, and the wrapped function inside
# the celery task has no documentation then set the help_ variable
# to an empty string, otherwise this will pick up the
# documentation from the celery.local.Proxy class which (at this
# stage in the execution) is what "f" actually is.
if hasattr(f, "__wrapped__") and \
hasattr(f.__wrapped__, "__doc__") and \
f.__wrapped__.__doc__ is None:
help_ = ""


# Make sure to set the name equal to the function name, Click does
# some weird name mangling around underscores that converts them
# to dashes.

return click.decorators.command(name=f.__name__, help=help_)(f)


def _iterate_tasks():
import_all_includes()
for extension in get_extensions():
if extension not in ('core', 'docker'):
for task in get_extension_tasks(extension).values():
yield task
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the convenience of just using the task names, but I do think it will be necessary to at least give the user's a signal about what plugin added the task.




class GWCommand(click.MultiCommand):
def list_commands(self, ctx):
return [_cast_to_command(task).name for task in _iterate_tasks()]

def get_command(self, ctx, name):
for task in _iterate_tasks():
spec = GWFuncDesc.get_description(task)
if spec is not None and spec.func_name == name:
return _cast_to_command(task)

def __call__(self, *args, **kwargs):
return self.main(*args, **kwargs)



@click.group(cls=GWCommand, invoke_without_command=True)
@click.option('-o', '--output',
type=click.Choice([ep.name for ep in pr.iter_entry_points(GWRUN_ENTRYPOINT_GROUP)]),
default='stdout')
@click.pass_context
def main(ctx, output):
pass

@main.resultcallback()
def process_output(processors, output):
for ep in pr.iter_entry_points(GWRUN_ENTRYPOINT_GROUP):
if ep.name == output:
_handler = ep.load()
return _handler(processors)

# Throw warning here?
return processors


if __name__ == '__main__':
main() #noqa
76 changes: 76 additions & 0 deletions girder_worker/cli/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import click
from girder_worker_utils.decorators import (
VarsArg,
KwargsArg,
PositionalArg,
KeywordArg)

# TODO Implement MVP
class VarsArgCli(VarsArg):
click_type = click.argument

def decorate(self, f):
pass

def get_args(self):
return []

def get_opts(self):
return {}


# TODO Implement MVP
class KwargsArgCli(KwargsArg):
click_type = click.argument

def decorate(self, f):
pass

def get_args(self):
return []

def get_opts(self):
return {}


class PositionalArgCli(PositionalArg):

def decorate(self, f):
click.argument(*self.get_args(), **self.get_opts())(f)

def get_args(self):
if not hasattr(self, 'cli_args'):
cli_args = (self.name, )
else:
cli_args = self.cli_args

return cli_args

def get_opts(self):
if not hasattr(self, 'cli_opts'):
cli_opts = {}
else:
cli_opts = self.cli_opts

return cli_opts


class KeywordArgCli(KeywordArg):
def decorate(self, f):
click.option(*self.get_args(), **self.get_opts())(f)

def get_opts(self):
if not hasattr(self, 'cli_opts'):
cli_opts = {}
else:
cli_opts = self.cli_opts

return cli_opts

def get_args(self):
if not hasattr(self, 'cli_args'):
cli_args = ('-{}'.format(self.name), )
else:
cli_args = self.cli_args

return cli_args
23 changes: 23 additions & 0 deletions girder_worker/cli/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from functools import wraps
import json

def gwrun_output_handler(func):
@wraps(func)
def _handler(value, **kwargs):
# Call the function for its output side effects
func(value, **kwargs)

# Return the value unmodified
return value

return _handler


@gwrun_output_handler
def stdout_output_handler(value):
print(value)


@gwrun_output_handler
def json_output_handler(value):
print(json.dumps(value))
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,17 @@ def run(self, *args, **kwargs):
'console_scripts': [
'girder-worker = girder_worker.__main__:main',
'girder-worker-cleanup = girder_worker.core.cleanup:main',
'girder-worker-config = girder_worker.configure:main'
'girder-worker-config = girder_worker.configure:main',
'gwrun = girder_worker.cli.__main__:main'
],
'girder_worker_plugins': [
'core = girder_worker:GirderWorkerPlugin',
'docker = girder_worker.docker:DockerPlugin [docker]'
],
'gwrun_output_handlers': [
'stdout = girder_worker.cli.handlers:stdout_output_handler',
'json = girder_worker.cli.handlers:json_output_handler',
],
'girder_worker._test_plugins.valid_plugins': [
'core = girder_worker._test_plugins.plugins:TestCore',
'plugin1 = girder_worker._test_plugins.plugins:TestPlugin1',
Expand Down