summaryrefslogtreecommitdiff
path: root/grc/python/extract_docs.py
diff options
context:
space:
mode:
authorSebastian Koslowski <koslowski@kit.edu>2015-08-18 16:47:41 +0200
committerSebastian Koslowski <koslowski@kit.edu>2015-11-13 21:47:58 +0100
commitd15065de35a535eae9448dff60270053f35885bd (patch)
treef44d59c1ba0875eb0ddafdb4e81f78d3afec77e1 /grc/python/extract_docs.py
parentf7add89403edcfdf8baa59c80b3e1d2b5c4ee307 (diff)
grc: move docstring extraction into subprocess
Diffstat (limited to 'grc/python/extract_docs.py')
-rw-r--r--grc/python/extract_docs.py201
1 files changed, 182 insertions, 19 deletions
diff --git a/grc/python/extract_docs.py b/grc/python/extract_docs.py
index 1124459282..837d26ca6f 100644
--- a/grc/python/extract_docs.py
+++ b/grc/python/extract_docs.py
@@ -17,10 +17,20 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+import sys
import re
+import subprocess
+import threading
+import json
+import Queue
+import random
import itertools
+###############################################################################
+# The docstring extraction
+###############################################################################
+
def docstring_guess_from_key(key):
"""Extract the documentation from the python __doc__ strings
@@ -67,28 +77,181 @@ def docstring_guess_from_key(key):
return doc_strings
-_docs_cache = dict()
+###############################################################################
+# Manage docstring extraction in separate process
+###############################################################################
+class SubprocessLoader(object):
+ """Start and manage docstring extraction process
-def extract(key):
+ Manages subprocess and handles RPC.
"""
- Call the private extract and cache the result.
+ BOOTSTRAP = "import runpy; runpy.run_path({!r}, run_name='__worker__')"
+ AUTH_CODE = random.random() # sort out unwanted output of worker process
+ RESTART = 5 # number of worker restarts before giving up
+ DONE = object() # sentinel value to signal end-of-queue
- Args:
- key: the block key
+ def __init__(self, callback_query_result, callback_finished=None):
+ self.callback_query_result = callback_query_result
+ self.callback_finished = callback_finished or (lambda: None)
- Returns:
- a string with documentation
+ self._queue = Queue.Queue()
+ self._thread = None
+ self._worker = None
+ self._shutdown = threading.Event()
+ self._last_cmd = None
+
+ def start(self):
+ """Start the worker process handler thread"""
+ if self._thread is not None:
+ return
+ self._shutdown.clear()
+ thread = self._thread = threading.Thread(target=self.run_worker)
+ thread.daemon = True
+ thread.start()
+
+ def run_worker(self):
+ """Read docstring back from worker stdout and execute callback."""
+ for _ in range(self.RESTART):
+ if self._shutdown.is_set():
+ break
+ try:
+ self._worker = subprocess.Popen(
+ args=(sys.executable, '-uc', self.BOOTSTRAP.format(__file__)),
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+ self._handle_worker()
+
+ except (OSError, IOError):
+ msg = "Warning: restarting the docstring loader"
+ cmd, args = self._last_cmd
+ if cmd == 'query':
+ msg += " (crashed while loading {0!r})".format(args[0])
+ print >> sys.stderr, msg
+ continue # restart
+ else:
+ break # normal termination, return
+ finally:
+ self._worker.terminate()
+ else:
+ print >> sys.stderr, "Warning: docstring loader crashed too often"
+ self._thread = None
+ self._worker = None
+ self.callback_finished()
+
+ def _handle_worker(self):
+ """Send commands and responses back from worker."""
+ assert '1' == self._worker.stdout.read(1)
+ for cmd, args in iter(self._queue.get, self.DONE):
+ self._last_cmd = cmd, args
+ self._send(cmd, args)
+ cmd, args = self._receive()
+ self._handle_response(cmd, args)
+
+ def _send(self, cmd, args):
+ """send a command to worker's stdin"""
+ fd = self._worker.stdin
+ json.dump((self.AUTH_CODE, cmd, args), fd)
+ fd.write('\n'.encode())
+
+ def _receive(self):
+ """receive response from worker's stdout"""
+ for line in iter(self._worker.stdout.readline, ''):
+ try:
+ key, cmd, args = json.loads(line, encoding='utf-8')
+ if key != self.AUTH_CODE:
+ raise ValueError('Got wrong auth code')
+ return cmd, args
+ except ValueError:
+ continue # ignore invalid output from worker
+ else:
+ raise IOError("Can't read worker response")
+
+ def _handle_response(self, cmd, args):
+ """Handle response from worker, call the callback"""
+ if cmd == 'result':
+ key, docs = args
+ self.callback_query_result(key, docs)
+ elif cmd == 'error':
+ print args
+ else:
+ print >> sys.stderr, "Unknown response:", cmd, args
+
+ def query(self, key):
+ """request docstring extraction for a certain key"""
+ if self._thread is None:
+ self.start()
+ self._queue.put(('query', (key,)))
+
+ def finish(self):
+ """signal end of requests"""
+ self._queue.put(self.DONE)
+
+ def wait(self):
+ """Wait for the handler thread to die"""
+ if self._thread:
+ self._thread.join()
+
+ def terminate(self):
+ """Terminate the worker and wait"""
+ self._shutdown.set()
+ try:
+ self._worker.terminate()
+ self.wait()
+ except (OSError, AttributeError):
+ pass
+
+
+###############################################################################
+# Main worker entry point
+###############################################################################
+
+def worker_main():
+ """Main entry point for the docstring extraction process.
+
+ Manages RPC with main process through.
+ Runs a docstring extraction for each key it read on stdin.
"""
- if not _docs_cache.has_key(key):
- docstrings = docstring_guess_from_key(key)
- _docs_cache[key] = '\n\n'.join(
- ' --- {0} --- \n\n{1}'.format(match, docstring)
- for match, docstring in docstrings.iteritems()
- )
- return _docs_cache[key]
-
-
-if __name__ == '__main__':
- import sys
- print extract(sys.argv[1])
+ def send(cmd, args):
+ json.dump((code, cmd, args), sys.stdout)
+ sys.stdout.write('\n'.encode())
+
+ sys.stdout.write('1')
+ for line in iter(sys.stdin.readline, ''):
+ code, cmd, args = json.loads(line, encoding='utf-8')
+ try:
+ if cmd == 'query':
+ key, = args
+ send('result', (key, docstring_guess_from_key(key)))
+ elif cmd == 'exit':
+ break
+ except Exception as e:
+ send('error', repr(e))
+
+
+if __name__ == '__worker__':
+ worker_main()
+
+elif __name__ == '__main__':
+ def callback(key, docs):
+ print key
+ for match, doc in docs.iteritems():
+ print '-->', match
+ print doc.strip()
+ print
+ print
+
+ r = SubprocessLoader(callback)
+
+ # r.query('analog_feedforward_agc_cc')
+ # r.query('uhd_source')
+ r.query('expr_utils_graph')
+ r.query('blocks_add_cc')
+ # r.query('analog_feedforward_agc_cc')
+ # r.query('uhd_source')
+ # r.query('uhd_source')
+ # r.query('analog_feedforward_agc_cc')
+ r.finish()
+ # r.terminate()
+ r.wait()