You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2022/09/22 14:50:31 UTC

[allura] 01/02: [#8467] DefOptScriptTask and other improvements to task submission in web ui

This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8467
in repository https://gitbox.apache.org/repos/asf/allura.git

commit eefe2541595705db0ab8320456e665476dae3531
Author: Dave Brondsema <db...@slashdotmedia.com>
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 5ecb7862f..1df3497df 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}