Skip to content

Commit

Permalink
Fix problems with the built in help system (#1084)
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:
* Remove WBrowser plugin
* Ginga 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
* now handles URLs by calling Python's "webbrowser" module to open them
* option to view local docstring in a plain text widget
* Remove WebView widget from various Widgets.py
* Fix Help menu item
* Remove requirements for `beautifulsoup` and `docutils`
  • Loading branch information
ejeschke authored Feb 16, 2024
1 parent 1c3cd84 commit 0f976ac
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 537 deletions.
3 changes: 3 additions & 0 deletions doc/WhatsNew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ Ver 5.0.0 (unreleased)
- Added PluginConfig plugin; allows configuration of all Ginga plugins
graphically; can enable/disable, change menu categories, etc.
- Removed --profile and --debug command-line options
- Fix a number of issues with the help system to make it simpler and
more robust; removed WBrowser plugin, handle URLs with Python webbrowser
module

Ver 4.1.0 (2022-06-30)
======================
Expand Down
1 change: 0 additions & 1 deletion doc/manual/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ Global plugins
plugins_global/colorbar
plugins_global/cursor
plugins_global/operations
plugins_global/wbrowser
plugins_global/fbrowser
plugins_global/colormappicker
plugins_global/errors
Expand Down
8 changes: 0 additions & 8 deletions doc/manual/plugins_global/wbrowser.rst

This file was deleted.

19 changes: 6 additions & 13 deletions ginga/GingaPlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,23 @@ def stop(self):
"""This method is called to stop the plugin."""
pass

def _help_docstring(self):
def _get_docstring(self):
import inspect

# Insert section title at the beginning
plg_name = self.__class__.__name__
plg_mod = inspect.getmodule(self)
plg_doc = ('{}\n{}\n'.format(plg_name, '=' * len(plg_name)) +
plg_mod.__doc__)
return plg_name, plg_doc

def _help_docstring(self):
plg_name, plg_doc = self._get_docstring()
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)
self.fv.help_plugin(self, text_kind=text_kind)


class GlobalPlugin(BasePlugin):
Expand Down
181 changes: 23 additions & 158 deletions ginga/doc/download_doc.py
Original file line number Diff line number Diff line change
@@ -1,180 +1,45 @@
"""Download rendered HTML doc from RTD."""
import os
import shutil
import zipfile
import urllib
"""Tools for accessing HTML doc from RTD."""

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

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

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

# base of our online documentation
rtd_base_url = "https://ginga.readthedocs.io/en/"

def _find_rtd_version():
"""Find closest RTD doc version."""
vstr = 'latest'
try:
import ginga
from bs4 import BeautifulSoup
except ImportError:
return vstr

# No active doc build before this release, just use latest.
if not minversion(ginga, '2.6.0'):
return vstr

# Get RTD download listing.
url = 'https://readthedocs.org/projects/ginga/downloads/'
with urllib.request.urlopen(url) as r: # nosec
soup = BeautifulSoup(r, 'html.parser')

# Compile a list of available HTML doc versions for download.
all_rtd_vernums = []
for link in soup.find_all('a'):
href = link.get('href')
if 'htmlzip' not in href:
continue
s = href.split('/')[-2]
if s.startswith('v'): # Ignore latest and stable
all_rtd_vernums.append(s)
all_rtd_vernums.sort(reverse=True)

# Find closest match.
ginga_ver = ginga.__version__
for rtd_ver in all_rtd_vernums:
if ginga_ver > rtd_ver[1:]: # Ignore "v" in comparison
break
else:
vstr = rtd_ver

return vstr


def _download_rtd_zip(rtd_version=None, **kwargs):
def get_online_docs_url(plugin=None):
"""
Download and extract HTML ZIP from RTD to installed doc data path.
Download is skipped if content already exists.
Return URL to online documentation closest to this Ginga version.
Parameters
----------
rtd_version : str or `None`
RTD version to download; e.g., "latest", "stable", or "v2.6.0".
If not given, download closest match to software version.
kwargs : dict
Keywords for ``urlretrieve()``.
Returns
-------
index_html : str
Path to local "index.html".
"""
# https://github.com/ejeschke/ginga/pull/451#issuecomment-298403134
if not toolkit.family.startswith('qt'):
raise ValueError('Downloaded documentation not compatible with {} '
'UI toolkit browser'.format(toolkit.family))

if rtd_version is None:
rtd_version = _find_rtd_version()

data_path = os.path.dirname(
get_pkg_data_path('help.html', package='ginga.doc'))
index_html = os.path.join(data_path, 'index.html')

# There is a previous download of documentation; Do nothing.
# There is no check if downloaded version is outdated; The idea is that
# this folder would be empty again when installing new version.
if os.path.isfile(index_html):
return index_html

url = ('https://readthedocs.org/projects/ginga/downloads/htmlzip/'
'{}/'.format(rtd_version))
local_path = urllib.request.urlretrieve(url, **kwargs)[0] # nosec

with zipfile.ZipFile(local_path, 'r') as zf:
zf.extractall(data_path)

# RTD makes an undesirable sub-directory, so move everything there
# up one level and delete it.
subdir = os.path.join(data_path, 'ginga-{}'.format(rtd_version))
for s in os.listdir(subdir):
src = os.path.join(subdir, s)
if os.path.isfile(src):
shutil.copy(src, data_path)
else: # directory
shutil.copytree(src, os.path.join(data_path, s))
shutil.rmtree(subdir)

if not os.path.isfile(index_html):
raise OSError(
'{} is missing; Ginga doc download failed'.format(index_html))

return index_html


def get_doc(logger=None, plugin=None, reporthook=None):
"""
Return URL to documentation. Attempt download if does not exist.
Parameters
----------
logger : obj or `None`
Ginga logger.
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.
reporthook : callable or `None`
Report hook for ``urlretrieve()``.
Returns
-------
url : str or `None`
URL to local documentation, if available.
url : str
URL to online documentation (top-level, if plugin == None).
"""
from ginga.GingaPlugin import GlobalPlugin, LocalPlugin

if isinstance(plugin, GlobalPlugin):
plugin_page = 'plugins_global'
plugin_name = str(plugin)
elif isinstance(plugin, LocalPlugin):
plugin_page = 'plugins_local'
plugin_name = str(plugin)
else:
plugin_page = None
plugin_name = None

try:
index_html = _download_rtd_zip(reporthook=reporthook)

# Download failed, use online resource
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))

# Use local resource
ginga_ver = ginga.__version__
if re.match(r'^v\d+\.\d+\.\d+$', ginga_ver):
rtd_version = ginga_ver
else:
pfx = 'file:'
url = '{}{}'.format(pfx, index_html)

# https://github.com/rtfd/readthedocs.org/issues/2803
if plugin_name is not None:
url += '#{}'.format(plugin_name)
# default to latest
rtd_version = 'latest'
url = f"{rtd_base_url}{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
7 changes: 0 additions & 7 deletions ginga/examples/configs/plugin_WBrowser.cfg

This file was deleted.

71 changes: 19 additions & 52 deletions ginga/gtk3w/Widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,16 @@
from gi.repository import GObject
from gi.repository import GdkPixbuf

import gi
has_webkit = False

try:
# this is necessary to prevent a warning message on import
gi.require_version('WebKit2', '4.0')

from gi.repository import WebKit2 as WebKit # noqa
has_webkit = True
except Exception:
try:
gi.require_version('WebKit', '3.0')
from gi.repository import WebKit # noqa
except Exception:
pass

__all__ = ['WidgetError', 'WidgetBase', 'TextEntry', 'TextEntrySet',
'TextArea', 'Label', 'Button', 'ComboBox',
'SpinBox', 'Slider', 'Dial', 'ScrollBar', 'CheckBox', 'ToggleButton',
'RadioButton', 'Image', 'ProgressBar', 'StatusBar', 'TreeView',
'WebView', 'ContainerBase', 'Box', 'HBox', 'VBox', 'Frame',
'ContainerBase', 'Box', 'HBox', 'VBox', 'Frame',
'Expander', 'TabWidget', 'StackWidget', 'MDIWidget', 'ScrollArea',
'Splitter', 'GridBox', 'Toolbar', 'MenuAction',
'Menu', 'Menubar', 'TopLevelMixin', 'TopLevel', 'Application',
'Dialog', 'SaveDialog', 'DragPackage', 'WidgetMoveEvent',
'name_mangle', 'make_widget', 'hadjust', 'build_info', 'wrap',
'has_webkit']
'name_mangle', 'make_widget', 'hadjust', 'build_info', 'wrap']

# path to our icons
icondir = os.path.split(ginga.icons.__file__)[0]
Expand Down Expand Up @@ -1161,33 +1144,6 @@ def _start_drag(self, treeview, context, selection,
drag_pkg.start_drag()


class WebView(WidgetBase):
def __init__(self):
if not has_webkit:
raise NotImplementedError("Missing webkit")

super(WebView, self).__init__()
self.widget = WebKit.WebView()

def load_url(self, url):
self.widget.open(url)

def load_html_string(self, html_string):
self.widget.load_string(html_string, 'text/html', 'utf-8', 'file://')

def go_back(self):
self.widget.go_back()

def go_forward(self):
self.widget.go_forward()

def reload_page(self):
self.widget.reload()

def stop_loading(self):
self.widget.stop_loading()


# CONTAINERS

class ContainerBase(WidgetBase):
Expand Down Expand Up @@ -2457,18 +2413,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 +2443,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
Loading

0 comments on commit 0f976ac

Please sign in to comment.