Skip to content

Instantly share code, notes, and snippets.

@vsajip
Last active April 8, 2019 07:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vsajip/1672347 to your computer and use it in GitHub Desktop.
Save vsajip/1672347 to your computer and use it in GitHub Desktop.
Sphinx HTML documentation watcher
#!/usr/bin/env python3
#
# Copyright (C) 2012 Vinay Sajip. Licensed under the MIT license.
#
# Based on Roberto Alsina's 128-line web browser, see
#
# http://lateral.netmanagers.com.ar/weblog/posts/BB948.html
#
import json
import os
import subprocess
import sys
import tempfile
try:
from urllib.request import pathname2url
except ImportError:
from urllib import pathname2url
import sip
sip.setapi("QString", 2)
sip.setapi("QVariant", 2)
from PyQt5 import QtGui, QtCore, QtWebKit, QtNetwork, QtWidgets, QtWebKitWidgets
settings = QtCore.QSettings("Vinay Sajip", "DocWatch")
class Watcher(QtCore.QThread):
"""
A watcher which looks for source file changes, builds the documentation,
and notifies the browser to refresh its contents
"""
def run(self):
self._stop = False
watch_command = 'inotifywait -rq -e close_write --exclude \'"*.html"\' .'.split()
make_command = 'make html'.split()
while not self._stop:
# Perhaps should put notifier access in a mutex - not bothering yet
self.notifier = subprocess.Popen(watch_command)
self.notifier.wait()
if self._stop:
break
subprocess.call(make_command)
# Refresh the UI ...
self.parent().changed.emit()
def stop(self):
self._stop = True
# Perhaps should put notifier access in a mutex - not bothering for now
if self.notifier.poll() is None: # not yet terminated ...
self.notifier.terminate()
class MainWindow(QtWidgets.QMainWindow):
"""
A browser intended for viewing HTML documentation generated by Sphinx.
"""
changed = QtCore.pyqtSignal()
def __init__(self, url):
super(MainWindow, self).__init__()
self.sb=self.statusBar()
self.pbar = QtWidgets.QProgressBar()
self.pbar.setMaximumWidth(120)
self.wb=QtWebKitWidgets.QWebView(loadProgress=self.pbar.setValue,
loadFinished=self.pbar.hide,
loadStarted=self.pbar.show,
titleChanged=self.setWindowTitle)
self.setCentralWidget(self.wb)
self.tb=self.addToolBar("Main Toolbar")
for a in (QtWebKitWidgets.QWebPage.Back,
QtWebKitWidgets.QWebPage.Forward,
QtWebKitWidgets.QWebPage.Reload):
self.tb.addAction(self.wb.pageAction(a))
self.url = QtWidgets.QLineEdit(returnPressed=lambda:self.wb.setUrl(QtCore.QUrl.fromUserInput(self.url.text())))
self.tb.addWidget(self.url)
self.wb.urlChanged.connect(lambda u: self.url.setText(u.toString()))
self.wb.urlChanged.connect(lambda: self.url.setCompleter(QtWidgets.QCompleter([i.url().toString() for i in self.wb.history().items()],
caseSensitivity=QtCore.Qt.CaseInsensitive)))
self.wb.statusBarMessage.connect(self.sb.showMessage)
self.wb.page().linkHovered.connect(lambda l: self.sb.showMessage(l, 3000))
self.search = QtWidgets.QLineEdit(returnPressed=lambda: self.wb.findText(self.search.text()))
self.search.hide()
self.showSearch = QtWidgets.QShortcut("Ctrl+F", self, activated = lambda: (self.search.show() , self.search.setFocus()))
self.hideSearch = QtWidgets.QShortcut("Esc", self, activated = lambda: (self.search.hide(), self.wb.setFocus()))
self.quit = QtWidgets.QShortcut("Ctrl+Q", self, activated = self.close)
self.zoomIn = QtWidgets.QShortcut("Ctrl++", self, activated = lambda: self.wb.setZoomFactor(self.wb.zoomFactor()+.2))
self.zoomOut = QtWidgets.QShortcut("Ctrl+-", self, activated = lambda: self.wb.setZoomFactor(self.wb.zoomFactor()-.2))
self.zoomOne = QtWidgets.QShortcut("Ctrl+=", self, activated = lambda: self.wb.setZoomFactor(1))
self.wb.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True)
self.sb.addPermanentWidget(self.search)
self.sb.addPermanentWidget(self.pbar)
self.load_settings()
self.wb.load(url)
self.watcher = Watcher(self)
self.changed.connect(self.wb.reload)
self.watcher.start()
def load_settings(self):
settings.beginGroup('mainwindow')
pos = settings.value('pos')
size = settings.value('size')
if isinstance(pos, QtCore.QPoint):
self.move(pos)
if isinstance(size, QtCore.QSize):
self.resize(size)
settings.endGroup()
def save_settings(self):
settings.beginGroup('mainwindow')
settings.setValue('pos', self.pos())
settings.setValue('size', self.size())
settings.endGroup()
def closeEvent(self, event):
self.save_settings()
self.watcher.stop()
if __name__ == "__main__":
if not os.path.isdir('_build'):
# very simplistic sanity check. Works for me, as I generally use
# sphinx-quickstart defaults
print('You must run this application from a Sphinx directory containing _build')
rc = 1
else:
app=QtWidgets.QApplication(sys.argv)
path = os.path.join('_build', 'html', 'index.html')
url = 'file:///' + pathname2url(os.path.abspath(path))
url = QtCore.QUrl(url)
wb=MainWindow(url)
wb.show()
rc = app.exec_()
sys.exit(rc)
@ofenerci
Copy link

Dear,

Thanks for the code. It is a great help to my sphinx writings. But I get an error such as:

Traceback (most recent call last):
File "/usr/local/bin/doc-watch", line 72, in
self.wb.urlChanged.connect(lambda: self.url.setCompleter(QtGui.QCompleter(QtCore.QStringList([QtCore.QString(i.url().toString()) for i in self.wb.history().items()]), caseSensitivity = QtCore.Qt.CaseInsensitive)))

AttributeError: 'module' object has no attribute 'QStringList'

Do you know the reason and how to correct it.

Thanks,
Ozhan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment