Commit 98e111f3 authored by payno's avatar payno

Merge branch 'add_doc_screenshots' into 'master'

Add example for generating automatically doc images from screenshots

See merge request !8
parents db1cf2c1 3cd435e9
......@@ -3,14 +3,14 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXBUILD = python -msphinx
PAPER =
BUILDDIR = build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# User-friendly check for python -msphinx
#ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
#$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
#endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
......
......@@ -14,15 +14,27 @@
# serve to show the default.
import sys
import os
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ext'))
import os.path
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
project = u'id06workflow'
try:
import id06workflow
project_dir = os.path.abspath(os.path.join(__file__, "..", "..", ".."))
build_dir = os.path.abspath(id06workflow.__file__)
if not build_dir.startswith(project_dir):
raise RuntimeError("%s looks to come from the system. Fix your PYTHONPATH and restart sphinx." % project)
except ImportError:
raise RuntimeError("%s is not on the path. Fix your PYTHONPATH and restart sphinx." % project)
# Add local sphinx extension directory
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ext'))
# -- General configuration ------------------------------------------------
......@@ -34,6 +46,9 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ext'))
# ones.
extensions = [
'sphinx.ext.autodoc',
'snapshotqt_directive',
'snapshotqt_link',
'sphinx.ext.doctest',
]
# Add any paths that contain templates here, relative to this directory.
......
"""RST directive to include snapshot of a Qt application in Sphinx doc.
Configuration variable in conf.py:
- snapshotqt_image_type: image file extension (default 'png').
- snapshotqt_script_dir: relative path of the root directory for scripts from
the documentation source directory (i.e., the directory of conf.py)
(default: '..').
"""
from __future__ import absolute_import
import logging
import os
import subprocess
import sys
from docutils.parsers.rst.directives.images import Figure
logging.basicConfig()
_logger = logging.getLogger(__name__)
# TODO:
# - Check if it is needed to patch block_text?
# RST directive ###############################################################
class SnapshotQtDirective(Figure):
"""Figure of a Qt application snapshot.
Directive Type: "snapshotqt"
Doctree Elements: As for figure
Directive Arguments: One or more, required (script URI + script arguments).
Directive Options: Possible.
Directive Content: Interpreted as the figure caption and optional legend.
A "snapshotqt" is a rst `figure
<http://docutils.sourceforge.net/docs/ref/rst/directives.html#figure>`_
that is generated from a Python script that uses Qt.
The path of the script to take a snapshot is relative to
the path given in conf.py 'snapshotqt_script_dir' value.
::
.. snapshotqt: ../examples/demo.py
:align: center
:height: 5cm
Caption of the figure.
"""
# TODO this should be configured in conf.py
SNAPSHOTS_QT = os.path.join('snapshotsqt_directive')
"""The path where to store images relative to doc directory."""
def run(self):
def createNeededDirs(_dir):
parentDir = os.path.dirname(_dir)
if parentDir not in ('', os.sep):
createNeededDirs(parentDir)
if os.path.exists(_dir) is False:
os.mkdir(_dir)
# Run script stored in arguments and replace by snapshot filename
env = self.state.document.settings.env
# Create an image filename from arguments
image_ext = env.config.snapshotqt_image_type.lower()
image_name = '_'.join(self.arguments) + '.' + image_ext
image_name = image_name.replace('./\\', '_')
image_name = ''.join([c for c in image_name
if c.isalnum() or c in '_-.'])
snapshot_dir = os.path.join(env.app.outdir, self.SNAPSHOTS_QT)
image_name = os.path.join(snapshot_dir, image_name)
createNeededDirs(os.path.dirname(image_name))
assert os.path.isdir(snapshot_dir)
# Change path to absolute path to run the script
script_dir = os.path.join(env.srcdir, env.config.snapshotqt_script_dir)
script_cmd = self.arguments[:]
script_cmd[0] = os.path.join(script_dir, script_cmd[0])
# Run snapshot
snapshot_tool = os.path.abspath(__file__)
_logger.info('Running script: %s', script_cmd)
_logger.info('Saving snapshot to: %s', image_name)
abs_image_name = os.path.join(env.srcdir, image_name)
cmd = [sys.executable, snapshot_tool, '--output', abs_image_name]
cmd += script_cmd
subprocess.check_call(cmd)
# Use created image as in Figure
self.arguments = [os.sep + image_name]
return super(SnapshotQtDirective, self).run()
def setup(app):
app.add_config_value('snapshotqt_image_type', 'png', 'env')
app.add_config_value('snapshotqt_script_dir', '..', 'env')
app.add_directive('snapshotqt', SnapshotQtDirective)
return {'version': '0.1'}
# Qt monkey-patch #############################################################
def monkeyPatchQApplication(filename, qt=None):
"""Monkey-patch QApplication to take a snapshot and close the application.
:param str filename: The image filename where to save the snapshot.
:param str qt: The Qt binding to patch.
This MUST be the same as the one used by the script
for which to take a snapshot.
In: 'PyQt4', 'PyQt5', 'PySide2' or None (the default).
If None, it will try to use PyQt4, then PySide2 and
finally PyQt4.
"""
# Probe Qt binding
if qt is None:
try:
import PyQt4.QtCore # noqa
qt = 'PyQt4'
except ImportError:
try:
import PySide2.QtCore # noqa
qt = 'PySide2'
except ImportError:
try:
import PyQt5.QtCore # noqa
qt = 'PyQt5'
except ImportError:
raise RuntimeError('Cannot find any supported Qt binding.')
if qt == 'PyQt4':
from PyQt4.QtGui import QApplication, QPixmap
from PyQt4.QtCore import QTimer
import PyQt4.QtGui as _QApplicationPackage
def grabWindow(winID):
return QPixmap.grabWindow(winID)
elif qt == 'PyQt5':
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer
import PyQt5.QtWidgets as _QApplicationPackage
def grabWindow(winID):
screen = QApplication.primaryScreen()
return screen.grabWindow(winID)
elif qt == 'PySide2':
from PySide2.QtGui import QApplication, QPixmap
from PySide2.QtCore import QTimer
import PySide2.QtGui as _QApplicationPackage
def grabWindow(winID):
return QPixmap.grabWindow(winID)
else:
raise ValueError('Unsupported Qt binding: %s' % qt)
_logger.info('Using Qt bindings: %s', qt)
class _QApplication(QApplication):
_TIMEOUT = 1000.
_FILENAME = filename
def _grabActiveWindowAndClose(self):
activeWindow = QApplication.activeWindow()
if activeWindow is not None:
if activeWindow.isVisible():
pixmap = grabWindow(activeWindow.winId())
saveOK = pixmap.save(self._FILENAME)
if not saveOK:
_logger.error(
'Cannot save snapshot to %s', self._FILENAME)
else:
_logger.error('activeWindow is not visible.')
self.quit()
else:
self._count -= 1
if self._count > 0:
# Only restart a timer if everything is OK
QTimer.singleShot(self._TIMEOUT,
self._grabActiveWindowAndClose)
else:
raise RuntimeError(
'Aborted: It took too long to have an active window.')
def exec_(self):
self._count = 10
QTimer.singleShot(self._TIMEOUT, self._grabActiveWindowAndClose)
return super(_QApplication, self).exec_()
_QApplicationPackage.QApplication = _QApplication
# main ########################################################################
if __name__ == '__main__':
import argparse
import runpy
# Parse argv
parser = argparse.ArgumentParser(
description=__doc__,
epilog="""Arguments provided after the script or module name are passed
to the script or module.""")
parser.add_argument(
'-o', '--output', nargs=1, type=str,
default='snapshot.png',
help='Image filename of the snapshot (default: snapshot.png).')
parser.add_argument(
'--bindings', nargs='?',
choices=('PySide2', 'PyQt4', 'PyQt5', 'auto'),
default='auto',
help="""Qt bindings used by the script/module.
If 'auto' (the default), it is probed from available python modules.
""")
parser.add_argument(
'-m', '--module', action='store_true',
help='Run the corresponding module as a script.')
parser.add_argument(
'script_or_module', nargs=1, type=str,
help='Python script to run for the snapshot.')
args, unknown = parser.parse_known_args()
script_or_module = args.script_or_module[0]
# arguments provided after the script or module
extra_args = sys.argv[sys.argv.index(script_or_module) + 1:]
if unknown != extra_args:
parser.print_usage()
_logger.error(
'%s: incorrect arguments', os.path.basename(sys.argv[0]))
sys.exit(1)
# Monkey-patch Qt
monkeyPatchQApplication(args.output[0],
args.bindings if args.bindings != 'auto' else None)
# Update sys.argv and sys.path
sys.argv = [script_or_module] + extra_args
sys.path.insert(0, os.path.abspath(os.path.dirname(script_or_module)))
if args.module:
_logger.info('Running module: %s', ' '.join(sys.argv))
runpy.run_module(script_or_module, run_name='__main__')
else:
with open(script_or_module) as f:
code = f.read()
_logger.info('Running script: %s', ' '.join(sys.argv))
exec(code)
"""RST directive to include snapshot of a Qt application in Sphinx doc.
Configuration variable in conf.py:
- snapshotqt_image_type: image file extension (default 'png').
- snapshotqt_script_dir: relative path of the root directory for scripts from
the documentation source directory (i.e., the directory of conf.py)
(default: '..').
"""
from __future__ import absolute_import
import logging
import urllib
import sys
from docutils.parsers.rst.directives.images import Figure, Image
from docutils import nodes
import os
from docutils.parsers.rst import directives
try: # check for the Python Imaging Library
import PIL.Image
except ImportError:
try: # sometimes PIL modules are put in PYTHONPATH's root
import Image
class PIL(object): pass # dummy wrapper
PIL.Image = Image
except ImportError:
PIL = None
logging.basicConfig()
_logger = logging.getLogger(__name__)
# TODO:
# - Create image in build directory
# - Check if it is needed to patch block_text?
# RST directive ###############################################################
SNAPSHOTS_QT = os.path.join('snapshotsqt_directive')
class SnapshotQtLink(Figure):
"""
TODO
"""
def align(argument):
return directives.choice(argument, Figure.align_h_values)
def figwidth_value(argument):
if argument.lower() == 'image':
return 'image'
else:
return directives.length_or_percentage_or_unitless(argument, 'px')
option_spec = Image.option_spec.copy()
option_spec['figwidth'] = figwidth_value
option_spec['figclass'] = directives.class_option
option_spec['align'] = align
has_content = True
def run(self):
env = self.state.document.settings.env
# Create an image filename from arguments
image_ext = env.config.snapshotqt_image_type.lower()
image_name = '_'.join(self.arguments) + '.' + image_ext
image_name = image_name.replace('./\\', '_')
image_name = ''.join([c for c in image_name
if c.isalnum() or c in '_-.'])
snapshot_dir = os.path.join(env.app.outdir, SNAPSHOTS_QT)
image_name = os.path.join(snapshot_dir, image_name)
self.arguments = [os.sep + image_name]
return super(SnapshotQtLink, self).run()
def setup(app):
app.add_directive('snapshotqt_link', SnapshotQtLink)
return {'version': '0.1'}
......@@ -15,6 +15,7 @@ Widgets
.. toctree::
:maxdepth: 1
widgets/com
widgets/datareduction
widgets/dataselection
widgets/geometry
......
......@@ -3,7 +3,7 @@
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
set SPHINXBUILD=python -msphinx
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
......@@ -48,7 +48,7 @@ if "%1" == "clean" (
)
REM Check if sphinx-build is available and fallback to Python version if any
REM Check if python -msphinx is available and fallback to Python version if any
%SPHINXBUILD% 2> nul
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
......
COM
===
.. snapshotqt:: orangecontrib/id06workflow/widgets/screenshots/com.py
Signals
-------
- (map)
**Outputs**:
- (map, data)
Description
-----------
Compute and display Center of Mass
......@@ -43,7 +43,7 @@ _ReconsParams = namedtuple('_ReconsParam', ['scale'])
class NoiseReductionWidget(qt.QWidget):
"""
Widget used to define and apply a noise removal
Widget used to define and apply a noise reduction
"""
def __init__(self, parent):
qt.QWidget.__init__(self, parent)
......
......@@ -30,7 +30,7 @@ __date__ = "03/10/2018"
import unittest
from id06workflow.gui.geometry import TwoThetaGeometryWidget
from silx.gui.test.utils import TestCaseQt
from silx.gui.utils.testutils import TestCaseQt
class TestTwoThetaExpSetupGUI(TestCaseQt):
......
......@@ -31,6 +31,7 @@ import numpy
import fabio
import tempfile
from id06workflow.core.experiment import Dataset
from id06workflow.core.experiment.operation.mapping import _MappingBase
import os
......@@ -113,3 +114,36 @@ def createDataset(pattern, background, dx=0, dy=0, dz=0, nb_data_files=10,
dataset.addFlatFieldFile(ff_file)
return dataset
def createRandomMap(shape, ndim=1):
"""
Create a dummy map which can be used for displaying some test
:param tuple shape: shape of the maps
:param int ndim:
"""
return _DummyMap(shape=shape, ndim=ndim)
class _DummyMap(_MappingBase):
def __init__(self, shape, ndim):
_MappingBase.__init__(self, None, name='Dummy map')
self.__dim = []
for iDim in range(ndim):
self.__dim.append(numpy.zeros((*(shape), 4)))
for imap in range(4):
self.dim[iDim][:, :, imap] = numpy.random.random(shape)
self.__intensity_map = numpy.random.random(shape)
@property
def dim(self):
return self.__dim
@property
def intensity_map(self):
return self.__intensity_map
@property
def ndim(self):
return len(self.__dim)
......@@ -104,7 +104,7 @@ class TestFirstSetup(OrangeWorflowTest):
def test(self):
"""Simple creation of the following workflow:
data selection -> geometry -> ROI selection -> noise removal
data selection -> geometry -> ROI selection -> noise reduction
-> shift correction
"""
# define the dataset
......
......@@ -64,7 +64,7 @@ class GradientRemovalOW(OWWidget):
def __init__(self):
super().__init__()
layout = gui.vBox(self.mainArea, 'noise removal').layout()
layout = gui.vBox(self.mainArea, 'noise reduction').layout()
self._plot = MappingPlot(parent=self)
layout.addWidget(self._plot)
......
......@@ -65,7 +65,7 @@ class MappingOW(OWWidget):
def __init__(self):
super().__init__()
layout = gui.vBox(self.mainArea, 'noise removal').layout()
layout = gui.vBox(self.mainArea, 'noise reduction').layout()
self._progress = gui.ProgressBar(self, 100)
self._plot = MappingPlot(parent=self)
......
......@@ -61,7 +61,7 @@ class NoiseReductionOW(OWWidget):
super().__init__()
self._widget = NoiseReductionWidget(parent=self)
layout = gui.vBox(self.mainArea, 'noise removal').layout()
layout = gui.vBox(self.mainArea, 'noise reduction').layout()
layout.addWidget(self._widget)
# buttons
......@@ -87,7 +87,7 @@ class NoiseReductionOW(OWWidget):
return
assert isinstance(experiment, Experiment)
if experiment.data is None:
_logger.warning('dataset is empty, won\'t apply noise removal')
_logger.warning('dataset is empty, won\'t apply noise reduction')
else:
self._widget.reset(experiment)
self._buttons.show()
......
from silx.gui import qt
from orangecontrib.id06workflow.widgets.com import ComOW
from id06workflow.test import utils
app = qt.QApplication([])
widget = ComOW()
# TODO: create a more realsitic dataset or store one on edna-site
_map = utils.createRandomMap((200, 200))
widget._process(_map)
widget.show()
app.exec_()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment