Esempio n. 1
0
    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)
Esempio n. 2
0
    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)
Esempio n. 3
0
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()
Esempio n. 4
0
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()
Esempio n. 5
0
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()