Last active
April 8, 2019 07:27
-
-
Save vsajip/1672347 to your computer and use it in GitHub Desktop.
Sphinx HTML documentation watcher
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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