This is an automated email from the ASF dual-hosted git repository. kentontaylor pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/allura.git
commit 57e484eb0433c9eb80bc6692402d2b8fa14c1534 Author: Dave Brondsema <[email protected]> AuthorDate: Thu Sep 22 10:50:05 2022 -0400 [#8467] DefOptScriptTask and other improvements to task submission in web ui --- Allura/allura/controllers/site_admin.py | 3 ++ Allura/allura/lib/helpers.py | 1 + Allura/allura/scripts/scripttask.py | 69 ++++++++++++++++++++++-- Allura/allura/templates/site_admin_task_new.html | 7 +-- Allura/docs/api/scripts.rst | 25 +++++++++ Allura/docs/conf.py | 2 +- 6 files changed, 100 insertions(+), 7 deletions(-) diff --git a/Allura/allura/controllers/site_admin.py b/Allura/allura/controllers/site_admin.py index d7b2cf97a..d8372e44c 100644 --- a/Allura/allura/controllers/site_admin.py +++ b/Allura/allura/controllers/site_admin.py @@ -620,6 +620,9 @@ class TaskManagerController: try: task = v.TaskValidator.to_python(task_name) doc = task.__doc__ or 'No doc string available' + doc = re.sub(r'^usage: ([^-][a-z_-]+ )?', # remove usage: and possible incorrect binary like "mod_wsgi" + 'Enter CLI formatted args above, like "args": ["--foo bar baz"]\n\n', + doc) except Invalid as e: error = str(e) return dict(doc=doc, error=error) diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py index 4a5445b26..77523e856 100644 --- a/Allura/allura/lib/helpers.py +++ b/Allura/allura/lib/helpers.py @@ -828,6 +828,7 @@ def datetimeformat(value, format='%Y-%m-%d %H:%M:%S'): @contextmanager def log_output(log): + # TODO: replace with contextlib.redirect_stdout and redirect_stderr? class Writer: def __init__(self, func): diff --git a/Allura/allura/scripts/scripttask.py b/Allura/allura/scripts/scripttask.py index f466610ea..0a1255c4c 100644 --- a/Allura/allura/scripts/scripttask.py +++ b/Allura/allura/scripts/scripttask.py @@ -32,12 +32,26 @@ To use, subclass ScriptTask and implement two methods:: '''Your main code goes here.''' pass +Or using the `defopt library <https://defopt.readthedocs.io/>`_ (must be installed), +subclass `DefOptScriptTask` and implement one method:: + + class MyScript(DefOptScriptTask): + @classmethod + def execute(cls, *, limit: int = 10, dry_run: bool = False): + ''' + Description/usage of this script + + :param limit: + Explanation of parametes, if desired + ''' + pass + To call as a script:: if __name__ == '__main__': MyScript.main() -To call as a task:: +To run as a background task:: # post the task with cmd-line-style args MyScript.post('-p myproject --dry-run') @@ -45,10 +59,10 @@ To call as a task:: """ import argparse +import contextlib +import io import logging -import six - from allura.lib.decorators import task from allura.lib.helpers import shlex_split @@ -62,6 +76,7 @@ class MetaParserDocstring(type): return cls.parser().format_help() def __new__(meta, classname, bases, classDict): + # make it look like a task return task(type.__new__(meta, classname, bases, classDict)) @@ -70,6 +85,8 @@ class ScriptTask(metaclass=MetaParserDocstring): """Base class for a command-line script that is also executable as a task.""" def __new__(cls, arg_string=''): + # when taskd calls SomeTaskClass(), then this runs. Not really the normal way to use __new__ + # and can't use __init__ since we want to return a value return cls._execute_task(arg_string) @classmethod @@ -94,3 +111,49 @@ class ScriptTask(metaclass=MetaParserDocstring): def main(cls): options = cls.parser().parse_args() cls.execute(options) + + +try: + import defopt +except ModuleNotFoundError: + pass +else: + + class MetaDefOpt(type): + def __new__(meta, classname, bases, classDict): + return task(type.__new__(meta, classname, bases, classDict)) + + @property + def __doc__(cls): + with contextlib.redirect_stdout(io.StringIO()) as stderr: + try: + cls.main(argv=['--help']) + except SystemExit: + pass + return stderr.getvalue() + + + class DefOptScriptTask(metaclass=MetaDefOpt): + """Base class for a command-line script that is also executable as a task.""" + + def __new__(cls, arg_string=''): + # when taskd calls SomeTaskClass(), then this runs. Not really the normal way to use __new__ + # and can't use __init__ since we want to return a value + return cls._execute_task(arg_string) + + @classmethod + def _execute_task(cls, arg_string): + try: + return cls.main(argv=shlex_split(arg_string or '')) + except SystemExit: + raise Exception("Error parsing args: '%s'" % arg_string) + + @classmethod + def main(cls, **extra_kwargs): + return defopt.run(cls.execute, no_negated_flags=True, **extra_kwargs) + + @classmethod + def execute(cls, *args, **kwargs): + """User code goes here, using defopt kwargs with type annotations""" + raise NotImplementedError + diff --git a/Allura/allura/templates/site_admin_task_new.html b/Allura/allura/templates/site_admin_task_new.html index ecd41daea..3ee645d41 100644 --- a/Allura/allura/templates/site_admin_task_new.html +++ b/Allura/allura/templates/site_admin_task_new.html @@ -71,8 +71,9 @@ <div> <label>Task Name *</label> <div class="input"> - <input name="task" value="{{form_values.get('task', '')}}" /> - <span class="note">Dotted python path to task callable</span> + <input name="task" value="{{form_values.get('task', '')}}"/> + <span class="note">Dotted python path to task callable + <br>e.g. allura.tasks.admin_tasks.install_app or allura.scripts.disable_users.DisableUsers</span> </div> {{error('task')}} </div> @@ -100,7 +101,7 @@ {{error('task_args')}} </div> - <input type="submit" /><br/> + <input type="submit" value="Run Task"/><br/> <pre class="doc"></pre> {{lib.csrf_token()}} diff --git a/Allura/docs/api/scripts.rst b/Allura/docs/api/scripts.rst new file mode 100644 index 000000000..c72abe658 --- /dev/null +++ b/Allura/docs/api/scripts.rst @@ -0,0 +1,25 @@ +.. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +.. _controllers_module: + +:mod:`allura.scripts` +-------------------------------- + +.. automodule:: allura.scripts + + .. automodule:: allura.scripts.scripttask diff --git a/Allura/docs/conf.py b/Allura/docs/conf.py index 59be57ba9..7ed4b55f4 100644 --- a/Allura/docs/conf.py +++ b/Allura/docs/conf.py @@ -223,4 +223,4 @@ latex_documents = [ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {'https://docs.python.org/3/': None}
