Skip to content

Commit

Permalink
Fix problems with the built in help system
Browse files Browse the repository at this point in the history
This fixes several problems with the built in help system:
- The WBrowser plugin is often unable to intialize correctly because
  it requires a QWebEngineView widget which is often not installed
  (for example it is offered separately in conda)
- Even when the widget is installed, it often can fail to start if
  OpenGL libraries are not configured correctly (often the case for
  conda on Linux, for example)
- The RTD web site often fails to access certain items, giving a
  HTTP 403 (Forbidden) error, particularly with trying to fetch the
  list if versions (ginga.doc.download_doc._find_rtd_version())
- The HTML ZIP download (ginga.doc.download_doc. _download_rtd_zip())
  sometimes fails as well

Solutions in this PR:
- WBrowser is made more robust, so that if it cannot create the
  browser widget the plugin will still start in a degraded mode
- In the degraded mode, WBrowser will pop up a dialog to ask the
  user whether they want to view the documentation in an external
  browser from the RTD link, or view the (local) plugin docstring
  in a text widget
- If the browser widget is successfully created, but the RTD version
  check fails, or the download of the zipped documentation fails,
  it will fall back to displaying the online documentation; if that
  fails it will fall back to showing the local plugin docstring in
  a text widget.
  • Loading branch information
ejeschke committed Jan 6, 2024
1 parent 5868816 commit 28466f5
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 121 deletions.
1 change: 1 addition & 0 deletions doc/WhatsNew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Ver 5.0.0 (unreleased)
- Added color distribution ("stretch") control to Info plugin
- Added LoaderConfig plugin; allows setting of loader priorities for
various MIME types
- Fix a number of issues with the help system to make it more robust

Ver 4.1.0 (2022-06-30)
======================
Expand Down
18 changes: 4 additions & 14 deletions ginga/GingaPlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,12 @@ def _help_docstring(self):
plg_mod = inspect.getmodule(self)
plg_doc = ('{}\n{}\n'.format(plg_name, '=' * len(plg_name)) +
plg_mod.__doc__)
return plg_name, plg_doc

self.fv.help_text(plg_name, plg_doc, text_kind='rst', trim_pfx=4)

def help(self):
def help(self, text_kind='rst'):
"""Display help for the plugin."""
if not self.fv.gpmon.has_plugin('WBrowser'):
self._help_docstring()
return

self.fv.start_global_plugin('WBrowser')

# need to let GUI finish processing, it seems
self.fv.update_pending()

obj = self.fv.gpmon.get_plugin('WBrowser')
obj.show_help(plugin=self, no_url_callback=self._help_docstring)
plg_name, plg_doc = self._help_docstring()
self.fv.help_plugin(self, plg_name, plg_doc, text_kind=text_kind)


class GlobalPlugin(BasePlugin):
Expand Down
61 changes: 44 additions & 17 deletions ginga/doc/download_doc.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
"""Download rendered HTML doc from RTD."""
"""Tools for accessing or downloading HTML doc from RTD."""
import os
import re
import shutil
import zipfile
import urllib

from astropy.utils import minversion
from astropy.utils.data import get_pkg_data_path

from ginga.GingaPlugin import GlobalPlugin, LocalPlugin
from ginga import toolkit
import ginga

__all__ = ['get_doc']
__all__ = ['get_doc', 'get_online_docs_url']


def _find_rtd_version():
"""Find closest RTD doc version."""
vstr = 'latest'
try:
import ginga
from bs4 import BeautifulSoup
except ImportError:
return vstr
Expand Down Expand Up @@ -138,8 +140,6 @@ def get_doc(logger=None, plugin=None, reporthook=None):
URL to local documentation, if available.
"""
from ginga.GingaPlugin import GlobalPlugin, LocalPlugin

if isinstance(plugin, GlobalPlugin):
plugin_page = 'plugins_global'
plugin_name = str(plugin)
Expand All @@ -153,20 +153,13 @@ def get_doc(logger=None, plugin=None, reporthook=None):
try:
index_html = _download_rtd_zip(reporthook=reporthook)

# Download failed, use online resource
# Download failed
except Exception as e:
url = 'https://ginga.readthedocs.io/en/latest/'

if plugin_name is not None:
if toolkit.family.startswith('qt'):
# This displays plugin docstring.
url = None
else:
# This redirects to online doc.
url += 'manual/{}/{}.html'.format(plugin_page, plugin_name)

if logger is not None:
logger.error(str(e))
logger.error(f"failed to download documentation: {e}",
exc_info=True)
# fall back to online version
url = get_online_docs_url(plugin=plugin)

# Use local resource
else:
Expand All @@ -178,3 +171,37 @@ def get_doc(logger=None, plugin=None, reporthook=None):
url += '#{}'.format(plugin_name)

return url


def get_online_docs_url(plugin=None):
"""
Return URL to online documentation closest to this Ginga version.
Parameters
----------
plugin : obj or `None`
Plugin object. If given, URL points to plugin doc directly.
If this function is called from within plugin class,
pass ``self`` here.
Returns
-------
url : str
URL to online documentation (possibly top-level).
"""
ginga_ver = ginga.__version__
if re.match(r'^v\d+\.\d+\.\d+$', ginga_ver):
rtd_version = ginga_ver
else:
# default to latest
rtd_version = 'latest'
url = f"https://ginga.readthedocs.io/en/{rtd_version}"
if plugin is not None:
plugin_name = str(plugin)
if isinstance(plugin, GlobalPlugin):
url += f'/manual/plugins_global/{plugin_name}.html'
elif isinstance(plugin, LocalPlugin):
url += f'/manual/plugins_local/{plugin_name}.html'

return url
23 changes: 17 additions & 6 deletions ginga/gtk3w/Widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2457,18 +2457,28 @@ class Dialog(TopLevelMixin, WidgetBase):
def __init__(self, title='', flags=0, buttons=[],
parent=None, modal=False):
WidgetBase.__init__(self)
self.buttons = []

if parent is not None:
self.parent = parent.get_widget()
else:
self.parent = None

button_list = []
self.widget = Gtk.Dialog(title=title, flags=flags)
btn_box = Gtk.ButtonBox()
btn_box.set_border_width(5)
btn_box.set_spacing(4)

for name, val in buttons:
button_list.extend([name, val])
btn = Button(name)
self.buttons.append(btn)

def cb(val):
return lambda w: self._cb_redirect(val)

btn.add_callback('activated', cb(val))
btn_box.pack_start(btn.get_widget(), True, True, 0)

self.widget = Gtk.Dialog(title=title, flags=flags,
buttons=tuple(button_list))
self.widget.set_modal(modal)

TopLevelMixin.__init__(self, title=title)
Expand All @@ -2477,12 +2487,13 @@ def __init__(self, title='', flags=0, buttons=[],
self.content.set_border_width(0)
content = self.widget.get_content_area()
content.pack_start(self.content.get_widget(), True, True, 0)
content.pack_end(btn_box, True, True, 0)

self.widget.connect("response", self._cb_redirect)
#self.widget.connect("response", self._cb_redirect)

self.enable_callback('activated')

def _cb_redirect(self, w, val):
def _cb_redirect(self, val):
self.make_callback('activated', val)

def get_content_area(self):
Expand Down
2 changes: 1 addition & 1 deletion ginga/qtw/QtHelp.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ def set_default_opengl_context():
fmt.setVersion(req.major, req.minor)
fmt.setProfile(QSurfaceFormat.CoreProfile)
fmt.setDefaultFormat(fmt)
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts, True)


def delete_widget(w):
Expand Down
12 changes: 8 additions & 4 deletions ginga/qtw/Widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2197,6 +2197,7 @@ def __init__(self, title='', flags=None, buttons=[],
parent = parent.get_widget()
self.widget = QtGui.QDialog(parent)
self.widget.setModal(modal)
self.buttons = []

vbox = QtGui.QVBoxLayout()
vbox.setContentsMargins(0, 0, 0, 0)
Expand All @@ -2210,16 +2211,19 @@ def __init__(self, title='', flags=None, buttons=[],
if len(buttons) > 0:
hbox_w = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
hbox.setContentsMargins(5, 5, 5, 5)
hbox.setSpacing(4)
hbox_w.setLayout(hbox)

for name, val in buttons:
btn = QtGui.QPushButton(name)
btn = Button(name)
self.buttons.append(btn)

def cb(val):
return lambda: self._cb_redirect(val)
return lambda w: self._cb_redirect(val)

btn.clicked.connect(cb(val))
hbox.addWidget(btn, stretch=0)
btn.add_callback('activated', cb(val))
hbox.addWidget(btn.get_widget(), stretch=1)

vbox.addWidget(hbox_w, stretch=0)
# self.widget.closeEvent = lambda event: self.delete()
Expand Down
64 changes: 31 additions & 33 deletions ginga/rv/Control.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,6 @@
from ginga.rv.Channel import Channel
from ginga.rv.rvmode import RVMode

have_docutils = False
try:
from docutils.core import publish_string
have_docutils = True
except ImportError:
pass


pluginconfpfx = None

Expand Down Expand Up @@ -430,10 +423,10 @@ def help_text(self, name, text, text_kind='plain', trim_pfx=0):
"""
Provide help text for the user.
This method will convert the text as necessary with docutils and
display it in the WBrowser plugin, if available. If the plugin is
not available and the text is type 'rst' then the text will be
displayed in a plain text widget.
This method will trim the text as necessary and display it in
the WBrowser plugin, if available. If the plugin is not
available and the text is type 'rst' or 'plain' then the text
will be displayed in a plain text widget.
Parameters
----------
Expand All @@ -456,33 +449,16 @@ def help_text(self, name, text, text_kind='plain', trim_pfx=0):
# of each line
text = toolbox.trim_prefix(text, trim_pfx)

if text_kind == 'rst':
# try to convert RST to HTML using docutils
try:
overrides = {'input_encoding': 'ascii',
'output_encoding': 'utf-8'}
text_html = publish_string(text, writer_name='html',
settings_overrides=overrides)
# docutils produces 'bytes' output, but webkit needs
# a utf-8 string
text = text_html.decode('utf-8')
text_kind = 'html'
if text_kind in ['rst', 'plain']:
self.show_help_text(name, text)

except Exception as e:
self.logger.error("Error converting help text to HTML: %s" % (
str(e)))
# revert to showing RST as plain text
elif text_kind == 'html':
self.help(text=text, text_kind='html')

else:
raise ValueError(
"I don't know how to display text of kind '%s'" % (text_kind))

if text_kind == 'html':
self.help(text=text, text_kind='html')

else:
self.show_help_text(name, text)

def help(self, text=None, text_kind='url'):

if not self.gpmon.has_plugin('WBrowser'):
Expand All @@ -503,7 +479,29 @@ def help(self, text=None, text_kind='url'):
else:
obj.show_help()

def show_help_text(self, name, help_txt, wsname='right'):
def help_plugin(self, plugin_obj, plugin_name, plugin_doc, text_kind='rst'):
"""
Called from a plugin's default help() method. Attempts to display
the plugin documentation in the WBrowser plugin (preferably) or
falling back to showing plain text in a text widget.
"""
if not self.gpmon.has_plugin('WBrowser'):
# text kind is assumed to not be a URL, but 'rst' or 'plain'
self.show_help_text(plugin_name, plugin_doc)
return

self.start_global_plugin('WBrowser')

# need to let GUI finish processing, it seems
self.update_pending()

def _fallback():
self.show_help_text(plugin_name, plugin_doc)

obj = self.gpmon.get_plugin('WBrowser')
obj.show_help(plugin=plugin_obj, no_url_callback=_fallback)

def show_help_text(self, name, help_txt, wsname='channels'):
"""
Show help text in a closeable tab window. The title of the
window is set from ``name`` prefixed with 'HELP:'
Expand Down
Loading

0 comments on commit 28466f5

Please sign in to comment.