def __init__(self): QThread.__init__(self) self._queue = queue.Queue() self.start(QThread.LowPriority) self._ac = AsyncController('QThread', self) self._ac.defaultPriority = QThread.LowPriority self._SphinxInvocationCount = 1
def test_9(self): for _ in self.poolAndThread: with AsyncController(_) as ac: def f(): raise TypeError em = Emitter() # Fun with exceptions: if an exception is raised while handling # a signal, it doesn't show up with normal try/catch semantics. # Instead, ``sys.excepthook`` must be overridden to see it. # ``WaitForSignal`` does this in ``__exit__``, so use it for the # convenience. Put another way, removing ``WaitForSignal`` and # adding a time.sleep(1.0) produces test failures, since the # exceptions raised are not caught by standard Python mechanisms # (here, ``self.assertRaises``). # # **However**, ``WaitForSignal`` doesn't do this in the body of # the ``with`` statement, so 'Sync' raises an exception but this # is discarded. For simplicity, skip this test case for now. with self.assertRaises(TypeError), WaitForSignal( em.bing, 1000, printExcTraceback=False): ac.start(em.g, f) # Make sure that the exception is still raised even if g doesn't # check for it. with self.assertRaises(TypeError), WaitForSignal( em.bing, 1000, printExcTraceback=False): ac.start(lambda result: None, f) # Make sure that the exception is still raised even there is no # g to check for it. with self.assertRaises(TypeError), WaitForSignal( em.bing, 1000, printExcTraceback=False): ac.start(None, f)
def test_5(self): for _ in self.poolAndThread: with AsyncController(_) as ac: def f(currentThread): self.assertNotEqual(currentThread, QThread.currentThread()) em = Emitter() with WaitForSignal(em.bing, 1000): ac.start(em.g, f, QThread.currentThread())
def test_10(self): with AsyncController('QThread') as ac: em1 = Emitter() def f1(): ac.start(em1.g, lambda: QThread.currentThread()) with WaitForSignal(em1.bing, 1000): ac.start(None, f1) self.assertEquals(em1.thread, em1.result)
def test_17(self): for _ in self.singleThreadOnly: with AsyncController(_) as ac: f = ac._wrap(self.fail, lambda: None) f.cancel() ac._start(f) em = Emitter() # Make sure the canceled job was processed by waiting until the # next job finishes. with WaitForSignal(em.bing, 1000): ac.start(em.g, lambda: None)
def test_4(self): for _ in self.syncPoolAndThread: with AsyncController(_) as ac: def f(a, b, c=2, d=4): self.assertEqual(a, 2) self.assertEqual(b, 3) self.assertEqual(c, 4) self.assertEqual(d, 5) em = Emitter() with WaitForSignal(em.bing, 1000): ac.start(em.g, f, 2, 3, d=5, c=4)
def test_7(self): for _ in ('Sync', 'QThread'): with AsyncController(_) as ac: em1 = Emitter(15, self.assertEqual) em2 = Emitter(16, self.assertEqual) em3 = Emitter(17, self.assertEqual) ac.start(em1.g, lambda: 15) ac.start(em2.g, lambda: 16) future3 = ac._wrap(em3.g, lambda: 17) with WaitForSignal(em3.bing, 1000): ac._start(future3)
def test_1(self): for _ in self.syncPoolAndThread: with AsyncController(_) as ac: # gotHere must be a list in order to f to change it in a way # that is visible outside of f. gotHere = [False] def f(): gotHere[0] = True future = ac._wrap(None, f) with WaitForSignal(future._signalInvoker.doneSignal, 1000): ac._start(future) self.assertTrue(gotHere[0])
def test_11(self): # Don't test with one pooled thread -- this test expects at least two # threads. with AsyncController(2) as ac: def f1(): em2 = Emitter() with WaitForSignal(em2.bing, 1000): ac.start(em2.g, lambda x: x, QThread.currentThread()) self.assertEqual(em2.thread, em2.result) em1 = Emitter() with WaitForSignal(em1.bing, 1000): ac.start(em1.g, f1)
def test_14(self): for _ in self.poolAndThread: with AsyncController(_) as ac: def f(assertEqual, priority): assertEqual(QThread.currentThread().priority(), priority) em = Emitter() ac.defaultPriority = QThread.LowPriority with WaitForSignal(em.bing, 1000): ac.start(em.g, f, self.assertEqual, QThread.LowestPriority, _futurePriority=QThread.LowestPriority) with WaitForSignal(em.bing, 1000): ac.start(em.g, f, self.assertEqual, QThread.LowPriority) with WaitForSignal(em.bing, 1000): ac.start(em.g, f, self.assertEqual, QThread.HighestPriority, _futurePriority=QThread.HighestPriority)
def test_11(self): # Don't test with one pooled thread -- this test expects at least two # threads. with AsyncController(2) as ac: em2 = Emitter() def f2(): future = ac.start(em2.g, lambda x: x, QThread.currentThread()) # The doneSignal won't be processed without an event loop. A # thread pool doesn't create one, so make our own to run ``g``. qe = QEventLoop() future._signalInvoker.doneSignal.connect(qe.exit) qe.exec_() with WaitForSignal(em2.bing, 1000): ac.start(None, f2) self.assertEquals(em2.thread, em2.result)
def test_6(self): # Don't test with one pooled thread -- this test expects at least two # threads. with AsyncController(2) as ac: q = Queue() def f(): q.get() return QThread.currentThread() em1 = Emitter() em2 = Emitter() ac.start(em1.g, f) ac.start(em2.g, f) with WaitForSignal(em1.bing, 1000), WaitForSignal(em2.bing, 1000): q.put(None) q.put(None) s = set([em1.result, em2.result, QThread.currentThread()]) self.assertEquals(len(s), 3)
def test_18(self): for _ in self.syncPoolAndThread: # Terminate using a context manager. with AsyncController(_) as ac1: pass with self.assertRaises(AssertionError): ac1.start(None, lambda: None) # Termiante by calling terminate. ac2 = AsyncController(_) ac2.terminate() with self.assertRaises(AssertionError): ac2.start(None, lambda: None) # Terminate via the QT object tree. o = QObject() ac3 = AsyncController(_, o) sip.delete(o) with self.assertRaises(AssertionError): ac3.start(None, lambda: None)
def test_8(self): for _ in self.poolOnly: with AsyncController(_) as ac: q1 = Queue() q2 = Queue() q3 = Queue() em1 = Emitter(15, self.assertEqual) em2 = Emitter(16, self.assertEqual) em3 = Emitter(17, self.assertEqual) ac.start(em1.g, lambda: q1.get()) ac.start(em2.g, lambda: q2.get()) ac.start(em3.g, lambda: q3.get()) sc = SignalCombiner() em1.bing.connect(sc.onBing) em2.bing.connect(sc.onBing) em3.bing.connect(sc.onBing) with WaitForSignal(sc.allEmitted, 1000): q1.put(15) q2.put(16) q3.put(17)
def test_12(self): for _ in self.singleThreadOnly: with AsyncController(_) as ac: q1a = Queue() q1b = Queue() def f1(): q1b.put(None) q1a.get() em1 = Emitter() future1 = ac.start(em1.g, f1) q1b.get() self.assertEquals(future1.state, Future.STATE_RUNNING) future2 = ac.start(None, lambda: None) QTest.qWait(100) self.assertEquals(future2.state, Future.STATE_WAITING) with WaitForSignal(em1.bing, 1000): future2.cancel() q1a.put(None) self.assertEquals(future1.state, Future.STATE_FINISHED) QTest.qWait(100) self.assertEquals(future2.state, Future.STATE_CANCELED)
def test_13(self): for _ in self.singleThreadOnly: with AsyncController(_) as ac: q1a = Queue() q1b = Queue() def f1(): q1b.put(None) q1a.get() # Cancel future3 while it's running in the other thread. em1 = Emitter('em1 should never be called by {}'.format(_), self.assertEqual) em1.bing.connect(self.fail) future1 = ac.start(em1.g, f1) q1b.get() self.assertEqual(future1.state, Future.STATE_RUNNING) future1.cancel(True) q1a.put(None) # If the result is discarded, it should never emit a signal or # invoke its callback, even if the task is already running. Wait # to make sure neither happened. QTest.qWait(100) # In addition, the signal from a finished task that is discarded # should not invoke the callback, even after the task has # finihsed and the sigal emitted. em2 = Emitter('em2 should never be called be {}'.format(_), self.assertEqual) em2.bing.connect(self.fail) future2 = ac.start(em2.g, lambda: None) # Don't use qWait here, since it will process messages, which # causes em2.g to be invoked. time.sleep(0.1) self.assertEqual(future2.state, Future.STATE_FINISHED) future2.cancel(True) # Test per-task priority. # Wait, in case a pending signal will invoke em2.g. QTest.qWait(100)
class ConverterThread(QThread): """Thread converts markdown to HTML. """ # This signal is emitted by the converter thread when a file has been # converted to HTML. htmlReady = pyqtSignal( # Path to the file which should be converted to / displayed as HTML. str, # HTML rendering of the file; empty if the HTML is provided in a file # specified by the URL below. str, # Error text resulting from the conversion process. str, # A reference to a file containing HTML rendering. Empty if the second # parameter above contains the HTML instead. QUrl) # This signal clears the context of the log window. logWindowClear = pyqtSignal() # This signal emits messages for the log window. logWindowText = pyqtSignal( # A string to append to the log window. str) _Task = collections.namedtuple("Task", ["filePath", "language", "text"]) def __init__(self): QThread.__init__(self) self._queue = queue.Queue() self.start(QThread.LowPriority) self._ac = AsyncController('QThread', self) self._ac.defaultPriority = QThread.LowPriority self._SphinxInvocationCount = 1 def process(self, filePath, language, text): """Convert data and emit result. """ self._queue.put(self._Task(filePath, language, text)) def stop_async(self): self._queue.put(None) def _getHtml(self, language, text, filePath): """Get HTML for document """ if language == 'Markdown': return self._convertMarkdown(text), None, QUrl() # For ReST, use docutils only if Sphinx isn't available. elif language == 'Restructured Text' and not sphinxEnabledForFile(filePath): htmlUnicode, errString = self._convertReST(text) return htmlUnicode, errString, QUrl() elif filePath and sphinxEnabledForFile(filePath): # Use Sphinx to generate the HTML if possible. return self._convertSphinx(filePath) elif filePath and canUseCodeChat(filePath): # Otherwise, fall back to using CodeChat+docutils. return self._convertCodeChat(text, filePath) else: return 'No preview for this type of file', None, QUrl() def _convertMarkdown(self, text): """Convert Markdown to HTML """ try: import markdown except ImportError: return 'Markdown preview requires <i>python-markdown</i> package<br/>' \ 'Install it with your package manager or see ' \ '<a href="http://packages.python.org/Markdown/install.html">installation instructions</a>' extensions = ['fenced_code', 'nl2br', 'tables', 'enki.plugins.preview.mdx_math'] # version 2.0 supports only extension names, not instances if markdown.version_info[0] > 2 or \ (markdown.version_info[0] == 2 and markdown.version_info[1] > 0): class _StrikeThroughExtension(markdown.Extension): """http://achinghead.com/python-markdown-adding-insert-delete.html Class is placed here, because depends on imported markdown, and markdown import is lazy """ DEL_RE = r'(~~)(.*?)~~' def extendMarkdown(self, md, md_globals): # Create the del pattern delTag = markdown.inlinepatterns.SimpleTagPattern(self.DEL_RE, 'del') # Insert del pattern into markdown parser md.inlinePatterns.add('del', delTag, '>not_strong') extensions.append(_StrikeThroughExtension()) return markdown.markdown(text, extensions) def _convertReST(self, text): """Convert ReST """ try: import docutils.core import docutils.writers.html4css1 except ImportError: return 'Restructured Text preview requires the <i>python-docutils</i> package.<br/>' \ 'Install it with your package manager or see ' \ '<a href="http://pypi.python.org/pypi/docutils"/>this page.</a>', None errStream = io.StringIO() settingsDict = { # Make sure to use Unicode everywhere. 'output_encoding': 'unicode', 'input_encoding' : 'unicode', # Don't stop processing, no matter what. 'halt_level' : 5, # Capture errors to a string and return it. 'warning_stream' : errStream } # Frozen-specific settings. if isFrozen: settingsDict['template'] = ( # The default docutils stylesheet and template uses a relative path, # which doesn't work when frozen ???. Under Unix when not frozen, # it produces: # ``IOError: [Errno 2] No such file or directory: # '/usr/lib/python2.7/dist-packages/docutils/writers/html4css1/template.txt'``. os.path.join(os.path.dirname(docutils.writers.html4css1.__file__), docutils.writers.html4css1.Writer.default_template) ) settingsDict['stylesheet_dirs'] = ['.', os.path.dirname(docutils.writers.html4css1.__file__)] htmlString = docutils.core.publish_string(text, writer_name='html', settings_overrides=settingsDict) errString = errStream.getvalue() errStream.close() return htmlString, errString def _convertSphinx(self, filePath): # Run the builder. errString = self._runHtmlBuilder() # Look for the HTML output. # # Get an absolute path to the output path, which could be relative. outputPath = core.config()['Sphinx']['OutputPath'] projectPath = core.config()['Sphinx']['ProjectPath'] if not os.path.isabs(outputPath): outputPath = os.path.join(projectPath, outputPath) # Create an htmlPath as OutputPath + remainder of filePath. htmlPath = os.path.join(outputPath + filePath[len(projectPath):]) html_file_suffix = '.html' try: with codecs.open(os.path.join(projectPath, 'sphinx-enki-info.txt')) as f: hfs = f.read() # If the file is empty, then html_file_suffix wasn't defined # or is None. In this case, use the default extension. # Otherwise, use the extension read from the file. if hfs: html_file_suffix = hfs except: errString = "Warning: assuming .html extension. Use " + \ "the conf.py template to set the extension.\n" + errString pass # First place to look: file.html. For example, look for foo.py # in foo.py.html. htmlFile = htmlPath + html_file_suffix # Second place to look: file without extension.html. For # example, look for foo.html for foo.rst. htmlFileAlter = os.path.splitext(htmlPath)[0] + html_file_suffix # Check that the output file produced by Sphinx is newer than # the source file it was built from. if os.path.exists(htmlFile): return _checkModificationTime(filePath, htmlFile, errString) elif os.path.exists(htmlFileAlter): return _checkModificationTime(filePath, htmlFileAlter, errString) else: return ('No preview for this type of file.<br>Expected ' + htmlFile + " or " + htmlFileAlter, errString, QUrl()) def _convertCodeChat(self, text, filePath): # Use StringIO to pass CodeChat compilation information back to # the UI. errStream = io.StringIO() try: htmlString = CodeToRest.code_to_html_string(text, errStream, filename=filePath) except KeyError: # Although the file extension may be in the list of supported # extensions, CodeChat may not support the lexer chosen by Pygments. # For example, a ``.v`` file may be Verilog (supported by CodeChat) # or Coq (not supported). In this case, provide an error messsage errStream.write('Error: this file is not supported by CodeChat.') htmlString = '' errString = errStream.getvalue() errStream.close() return htmlString, errString, QUrl() def _runHtmlBuilder(self): # Build the commond line for Sphinx. if core.config()['Sphinx']['AdvancedMode']: htmlBuilderCommandLine = core.config()['Sphinx']['Cmdline'] if sys.platform.startswith('linux'): # If Linux is used, then subprocess cannot take the whole # commandline as the name of an executable file. Module shlex # has to be used to parse commandline. htmlBuilderCommandLine = shlex.split(htmlBuilderCommandLine) else: # For available builder options, refer to: http://sphinx-doc.org/builders.html htmlBuilderCommandLine = [core.config()['Sphinx']['Executable'], # Place doctrees in the ``_build`` directory; by default, Sphinx # places this in _build/html/.doctrees. '-d', os.path.join('_build', 'doctrees'), # Source directory -- the current directory, since we'll chdir to # the project directory before executing this. '.', # Build directory core.config()['Sphinx']['OutputPath']] # Invoke it. try: # Clear the log at the beginning of a Sphinx build. self.logWindowClear.emit() cwd = core.config()['Sphinx']['ProjectPath'] # If the command line is already a string (advanced mode), just print it. # Otherwise, it's a list that should be transformed to a string. if isinstance(htmlBuilderCommandLine, str): htmlBuilderCommandLineStr = htmlBuilderCommandLine else: htmlBuilderCommandLineStr = ' '.join(htmlBuilderCommandLine) self.logWindowText.emit('{} : {}\n\n'.format(cwd, htmlBuilderCommandLineStr)) # Run Sphinx, reading stdout in a separate thread. self._qe = QEventLoop() # Sphinx will output just a carriage return (0x0D) to simulate a # single line being updated by build status and the build # progresses. Without universal newline support here, we'll wait # until the build is complete (with a \n\r) to report any build # progress! So, enable universal newlines, so that each \r will be # treated as a separate line, providing immediate feedback on build # progress. popen = open_console_output(htmlBuilderCommandLine, cwd=cwd, universal_newlines=True) # Perform reads in an event loop. The loop is exit when all reads # have completed. We can't simply start the _stderr_read thread # here, because calls to self._qe_exit() will be ignored until # we're inside the event loop. QTimer.singleShot(0, lambda: self._popen_read(popen)) self._qe.exec_() except OSError as ex: return ( 'Failed to execute HTML builder:\n' '{}\n'.format(str(ex)) + 'Go to Settings -> Settings -> CodeChat to set HTML' ' builder configurations.') return self._stderr # Read from stdout (in this thread) and stderr (in another thread), # so that the user sees output as the build progresses, rather than only # producing output after the build is complete. def _popen_read(self, popen): # Read are blocking; we can't read from both stdout and stderr in the # same thread without possible buffer overflows. So, use this thread to # read from and immediately report progress from stdout. In another # thread, read all stderr and report that after the build finishes. self._ac.start(None, self._stderr_read, popen.stderr) # Read a line of stdout then report it to the user immediately. s = popen.stdout.readline() while s: self.logWindowText.emit(s.rstrip('\n')) s = popen.stdout.readline() self._SphinxInvocationCount += 1 # I would expect the following code to do the same thing. It doesn't: # instead, it waits until Sphinx completes before returning anything. # ??? # # .. code-block: python # :linenos: # # for s in popen.stdout: # self.logWindowText.emit(s) # Runs in a separate thread to read stdout. It then exits the QEventLoop as # a way to signal that stderr reads have completed. def _stderr_read(self, stderr): self._stderr = stderr.read() self._qe.exit() def run(self): """Thread function """ while True: # exits with break # wait task task = self._queue.get() # take the last task while self._queue.qsize(): task = self._queue.get() if task is None: # None is a quit command self._ac.terminate() break # TODO: This is ugly. Should pass this exception back to the main # thread and re-raise it there, or use a QFuture like approach which # does this automaticlaly. try: html, errString, url = self._getHtml(task.language, task.text, task.filePath) except Exception: traceback.print_exc() self.htmlReady.emit(task.filePath, html, errString, url) # Free resources. self._ac.terminate()
def test_2(self): for _ in self.syncPoolAndThread: with AsyncController(_) as ac: em = Emitter(2, self.assertEqual) with WaitForSignal(em.bing, 1000): ac.start(em.g, lambda: 2)
def test_18(self): for _ in self.syncPoolAndThread: # Terminate using a context manager. with AsyncController(_) as ac1: pass with self.assertRaises(AssertionError): ac1.start(None, lambda: None) # Termiante by calling terminate. ac2 = AsyncController(_) ac2.terminate() with self.assertRaises(AssertionError): ac2.start(None, lambda: None) # Terminate via __del__, I hope. ac3 = AsyncController(_) del ac3 # Can't try start, since the object was deleted. # Terminate via the QT object tree. o = QObject() ac3 = AsyncController(_, o) sip.delete(o) with self.assertRaises(AssertionError): ac3.start(None, lambda: None)
class SphinxConverter(QObject): """This class converts Sphinx input to HTML. It is run in a separate thread. """ # This signal clears the context of the log window. logWindowClear = pyqtSignal() # This signal emits messages for the log window. logWindowText = pyqtSignal( # A string to append to the log window. str) def __init__(self, parent): super().__init__(parent) # Use an additional thread to process Sphinx output. self._ac = AsyncController('QThread', self) self._ac.defaultPriority = QThread.LowPriority self._SphinxInvocationCount = 1 def terminate(self): # Free resources. self._ac.terminate() def convert(self, filePath): # Run the builder. errString = self._runHtmlBuilder() # Look for the HTML output. # # Get an absolute path to the output path, which could be relative. outputPath = core.config()['Sphinx']['OutputPath'] projectPath = core.config()['Sphinx']['ProjectPath'] if not os.path.isabs(outputPath): outputPath = os.path.join(projectPath, outputPath) # Create an htmlPath as OutputPath + remainder of filePath. htmlPath = os.path.join(outputPath + filePath[len(projectPath):]) html_file_suffix = '.html' try: with codecs.open(os.path.join(projectPath, 'sphinx-enki-info.txt')) as f: hfs = f.read() # If the file is empty, then html_file_suffix wasn't defined # or is None. In this case, use the default extension. # Otherwise, use the extension read from the file. if hfs: html_file_suffix = hfs except: errString = "Warning: assuming .html extension. Use " + \ "the conf.py template to set the extension.\n" + errString pass # First place to look: file.html. For example, look for foo.py # in foo.py.html. htmlFile = htmlPath + html_file_suffix # Second place to look: file without extension.html. For # example, look for foo.html for foo.rst. htmlFileAlter = os.path.splitext(htmlPath)[0] + html_file_suffix # Check that the output file produced by Sphinx is newer than # the source file it was built from. if os.path.exists(htmlFile): return _checkModificationTime(filePath, htmlFile, errString) elif os.path.exists(htmlFileAlter): return _checkModificationTime(filePath, htmlFileAlter, errString) else: return (filePath, 'No preview for this type of file.<br>Expected ' + htmlFile + " or " + htmlFileAlter, errString, QUrl()) def _runHtmlBuilder(self): # Build the commond line for Sphinx. if core.config()['Sphinx']['AdvancedMode']: htmlBuilderCommandLine = core.config()['Sphinx']['Cmdline'] if sys.platform.startswith('linux'): # If Linux is used, then subprocess cannot take the whole # commandline as the name of an executable file. Module shlex # has to be used to parse commandline. htmlBuilderCommandLine = shlex.split(htmlBuilderCommandLine) else: # For available builder options, refer to: http://sphinx-doc.org/builders.html htmlBuilderCommandLine = [ core.config()['Sphinx']['Executable'], # Place doctrees in the ``_build`` directory; by default, Sphinx # places this in _build/html/.doctrees. '-d', os.path.join('_build', 'doctrees'), # Source directory -- the current directory, since we'll chdir to # the project directory before executing this. '.', # Build directory core.config()['Sphinx']['OutputPath'] ] # Invoke it. try: # Clear the log at the beginning of a Sphinx build. self.logWindowClear.emit() cwd = core.config()['Sphinx']['ProjectPath'] # If the command line is already a string (advanced mode), just print it. # Otherwise, it's a list that should be transformed to a string. if isinstance(htmlBuilderCommandLine, str): htmlBuilderCommandLineStr = htmlBuilderCommandLine else: htmlBuilderCommandLineStr = ' '.join(htmlBuilderCommandLine) self.logWindowText.emit('{} : {}\n\n'.format( cwd, htmlBuilderCommandLineStr)) # Run Sphinx, reading stdout in a separate thread. self._qe = QEventLoop() # Sphinx will output just a carriage return (0x0D) to simulate a # single line being updated by build status and the build # progresses. Without universal newline support here, we'll wait # until the build is complete (with a \n\r) to report any build # progress! So, enable universal newlines, so that each \r will be # treated as a separate line, providing immediate feedback on build # progress. popen = open_console_output(htmlBuilderCommandLine, cwd=cwd, universal_newlines=True) # Perform reads in an event loop. The loop is exit when all reads # have completed. We can't simply start the _stderr_read thread # here, because calls to self._qe_exit() will be ignored until # we're inside the event loop. QTimer.singleShot(0, lambda: self._popen_read(popen)) self._qe.exec_() except OSError as ex: return ('Failed to execute HTML builder:\n' '{}\n'.format(str(ex)) + 'Go to Settings -> Settings -> CodeChat to set HTML' ' builder configurations.') return self._stderr # Read from stdout (in this thread) and stderr (in another thread), # so that the user sees output as the build progresses, rather than only # producing output after the build is complete. def _popen_read(self, popen): # Read are blocking; we can't read from both stdout and stderr in the # same thread without possible buffer overflows. So, use this thread to # read from and immediately report progress from stdout. In another # thread, read all stderr and report that after the build finishes. self._ac.start(None, self._stderr_read, popen.stderr) # Read a line of stdout then report it to the user immediately. s = popen.stdout.readline() while s: self.logWindowText.emit(s.rstrip('\n')) s = popen.stdout.readline() self._SphinxInvocationCount += 1 # I would expect the following code to do the same thing. It doesn't: # instead, it waits until Sphinx completes before returning anything. # ??? # # .. code-block: python # :linenos: # # for s in popen.stdout: # self.logWindowText.emit(s) # Runs in a separate thread to read stdout. It then exits the QEventLoop as # a way to signal that stderr reads have completed. def _stderr_read(self, stderr): self._stderr = stderr.read() self._qe.exit()
def __init__(self, parent): super().__init__(parent) # Use an additional thread to process Sphinx output. self._ac = AsyncController('QThread', self) self._ac.defaultPriority = QThread.LowPriority self._SphinxInvocationCount = 1
class SphinxConverter(QObject): """This class converts Sphinx input to HTML. It is run in a separate thread. """ # This signal clears the context of the log window. logWindowClear = pyqtSignal() # This signal emits messages for the log window. logWindowText = pyqtSignal( # A string to append to the log window. str) def __init__(self, parent): super().__init__(parent) # Use an additional thread to process Sphinx output. self._ac = AsyncController('QThread', self) self._ac.defaultPriority = QThread.LowPriority self._SphinxInvocationCount = 1 def terminate(self): # Free resources. self._ac.terminate() def convert(self, filePath): # Run the builder. errString = self._runHtmlBuilder() # Look for the HTML output. # # Get an absolute path to the output path, which could be relative. outputPath = core.config()['Sphinx']['OutputPath'] projectPath = core.config()['Sphinx']['ProjectPath'] if not os.path.isabs(outputPath): outputPath = os.path.join(projectPath, outputPath) # Create an htmlPath as OutputPath + remainder of filePath. htmlPath = os.path.join(outputPath + filePath[len(projectPath):]) html_file_suffix = '.html' try: with codecs.open(os.path.join(projectPath, 'sphinx-enki-info.txt')) as f: hfs = f.read() # If the file is empty, then html_file_suffix wasn't defined # or is None. In this case, use the default extension. # Otherwise, use the extension read from the file. if hfs: html_file_suffix = hfs except: errString = "Warning: assuming .html extension. Use " + \ "the conf.py template to set the extension.\n" + errString pass # First place to look: file.html. For example, look for foo.py # in foo.py.html. htmlFile = htmlPath + html_file_suffix # Second place to look: file without extension.html. For # example, look for foo.html for foo.rst. htmlFileAlter = os.path.splitext(htmlPath)[0] + html_file_suffix # Check that the output file produced by Sphinx is newer than # the source file it was built from. if os.path.exists(htmlFile): return _checkModificationTime(filePath, htmlFile, errString) elif os.path.exists(htmlFileAlter): return _checkModificationTime(filePath, htmlFileAlter, errString) else: return (filePath, 'No preview for this type of file.<br>Expected ' + htmlFile + " or " + htmlFileAlter, errString, QUrl()) def _runHtmlBuilder(self): # Build the commond line for Sphinx. if core.config()['Sphinx']['AdvancedMode']: htmlBuilderCommandLine = core.config()['Sphinx']['Cmdline'] if sys.platform.startswith('linux'): # If Linux is used, then subprocess cannot take the whole # commandline as the name of an executable file. Module shlex # has to be used to parse commandline. htmlBuilderCommandLine = shlex.split(htmlBuilderCommandLine) else: # For available builder options, refer to: http://sphinx-doc.org/builders.html htmlBuilderCommandLine = [core.config()['Sphinx']['Executable'], # Place doctrees in the ``_build`` directory; by default, Sphinx # places this in _build/html/.doctrees. '-d', os.path.join('_build', 'doctrees'), # Source directory -- the current directory, since we'll chdir to # the project directory before executing this. '.', # Build directory core.config()['Sphinx']['OutputPath']] # Invoke it. try: # Clear the log at the beginning of a Sphinx build. self.logWindowClear.emit() cwd = core.config()['Sphinx']['ProjectPath'] # If the command line is already a string (advanced mode), just print it. # Otherwise, it's a list that should be transformed to a string. if isinstance(htmlBuilderCommandLine, str): htmlBuilderCommandLineStr = htmlBuilderCommandLine else: htmlBuilderCommandLineStr = ' '.join(htmlBuilderCommandLine) self.logWindowText.emit('{} : {}\n\n'.format(cwd, htmlBuilderCommandLineStr)) # Run Sphinx, reading stdout in a separate thread. self._qe = QEventLoop() # Sphinx will output just a carriage return (0x0D) to simulate a # single line being updated by build status and the build # progresses. Without universal newline support here, we'll wait # until the build is complete (with a \n\r) to report any build # progress! So, enable universal newlines, so that each \r will be # treated as a separate line, providing immediate feedback on build # progress. popen = open_console_output(htmlBuilderCommandLine, cwd=cwd, universal_newlines=True) # Perform reads in an event loop. The loop is exit when all reads # have completed. We can't simply start the _stderr_read thread # here, because calls to self._qe_exit() will be ignored until # we're inside the event loop. QTimer.singleShot(0, lambda: self._popen_read(popen)) self._qe.exec_() except OSError as ex: return ( 'Failed to execute HTML builder:\n' '{}\n'.format(str(ex)) + 'Go to Settings -> Settings -> CodeChat to set HTML' ' builder configurations.') return self._stderr # Read from stdout (in this thread) and stderr (in another thread), # so that the user sees output as the build progresses, rather than only # producing output after the build is complete. def _popen_read(self, popen): # Read are blocking; we can't read from both stdout and stderr in the # same thread without possible buffer overflows. So, use this thread to # read from and immediately report progress from stdout. In another # thread, read all stderr and report that after the build finishes. self._ac.start(None, self._stderr_read, popen.stderr) # Read a line of stdout then report it to the user immediately. s = popen.stdout.readline() while s: self.logWindowText.emit(s.rstrip('\n')) s = popen.stdout.readline() self._SphinxInvocationCount += 1 # I would expect the following code to do the same thing. It doesn't: # instead, it waits until Sphinx completes before returning anything. # ??? # # .. code-block: python # :linenos: # # for s in popen.stdout: # self.logWindowText.emit(s) # Runs in a separate thread to read stdout. It then exits the QEventLoop as # a way to signal that stderr reads have completed. def _stderr_read(self, stderr): self._stderr = stderr.read() self._qe.exit()