File: C:/Users/fred/anaconda3/Lib/site-packages/spyder/plugins/onlinehelp/widgets.py
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""
PyDoc widget.
"""
# Standard library imports
import os
import os.path as osp
import pydoc
import sys
# Third party imports
from qtpy.QtCore import Qt, QThread, QUrl, Signal, Slot
from qtpy.QtGui import QCursor
from qtpy.QtWebEngineWidgets import WEBENGINE
from qtpy.QtWidgets import QApplication, QLabel, QVBoxLayout
# Local imports
from spyder.api.translations import _
from spyder.api.widgets.main_widget import PluginMainWidget
from spyder.plugins.onlinehelp.pydoc_patch import _start_server, _url_handler
from spyder.widgets.browser import FrameWebView, WebViewActions
from spyder.widgets.comboboxes import UrlComboBox
from spyder.widgets.findreplace import FindReplace
# --- Constants
# ----------------------------------------------------------------------------
PORT = 30128
class PydocBrowserActions:
# Triggers
Home = 'home_action'
Find = 'find_action'
class PydocBrowserMainToolbarSections:
Main = 'main_section'
class PydocBrowserToolbarItems:
PackageLabel = 'package_label'
UrlCombo = 'url_combo'
# =============================================================================
# Pydoc adjustments
# =============================================================================
# This is needed to prevent pydoc raise an ErrorDuringImport when
# trying to import numpy.
# See spyder-ide/spyder#10740
DIRECT_PYDOC_IMPORT_MODULES = ['numpy', 'numpy.core']
try:
from pydoc import safeimport
def spyder_safeimport(path, forceload=0, cache={}):
if path in DIRECT_PYDOC_IMPORT_MODULES:
forceload = 0
return safeimport(path, forceload=forceload, cache=cache)
pydoc.safeimport = spyder_safeimport
except Exception:
pass
# Needed to prevent showing a warning message regarding debugging.
# Fixes spyder-ide/spyder#20390 and spyder-ide/spyder#21171
os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
class PydocServer(QThread):
"""
Pydoc server.
"""
sig_server_started = Signal()
def __init__(self, parent, port):
QThread.__init__(self, parent)
self.port = port
self.server = None
self.complete = False
self.closed = False
def run(self):
self.callback(
_start_server(
_url_handler,
hostname='127.0.0.1',
port=self.port,
)
)
def callback(self, server):
self.server = server
if self.closed:
self.quit_server()
else:
self.sig_server_started.emit()
def completer(self):
self.complete = True
def is_running(self):
"""Check if the server is running"""
if self.isRunning():
# Startup
return True
if self.server is None:
return False
return self.server.serving
def quit_server(self):
self.closed = True
if self.server is None:
return
if self.server.serving:
self.server.stop()
class PydocBrowser(PluginMainWidget):
"""PyDoc browser widget."""
ENABLE_SPINNER = True
# --- Signals
# ------------------------------------------------------------------------
sig_load_finished = Signal()
"""
This signal is emitted to indicate the help page has finished loading.
"""
def __init__(self, name=None, plugin=None, parent=None):
super().__init__(name, plugin, parent=parent)
self._is_running = False
self.home_url = None
self.server = None
# Widgets
self.label = QLabel(_("Package:"))
self.label.ID = PydocBrowserToolbarItems.PackageLabel
self.url_combo = UrlComboBox(
self, id_=PydocBrowserToolbarItems.UrlCombo)
# Setup web view frame
self.webview = FrameWebView(
self,
handle_links=self.get_conf('handle_links')
)
self.webview.setup()
self.webview.set_zoom_factor(self.get_conf('zoom_factor'))
self.webview.loadStarted.connect(self._start)
self.webview.loadFinished.connect(self._finish)
self.webview.titleChanged.connect(self.setWindowTitle)
self.webview.urlChanged.connect(self._change_url)
if not WEBENGINE:
self.webview.iconChanged.connect(self._handle_icon_change)
# Setup find widget
self.find_widget = FindReplace(self)
self.find_widget.set_editor(self.webview)
self.find_widget.hide()
self.url_combo.setMaxCount(self.get_conf('max_history_entries'))
tip = _('Write a package name here, e.g. pandas')
self.url_combo.lineEdit().setPlaceholderText(tip)
self.url_combo.lineEdit().setToolTip(tip)
self.url_combo.valid.connect(
lambda x: self._handle_url_combo_activation())
# Layout
layout = QVBoxLayout()
layout.addWidget(self.webview)
layout.addSpacing(1)
layout.addWidget(self.find_widget)
self.setLayout(layout)
# --- PluginMainWidget API
# ------------------------------------------------------------------------
def get_title(self):
return _('Online help')
def get_focus_widget(self):
self.url_combo.lineEdit().selectAll()
return self.url_combo
def setup(self):
# Actions
home_action = self.create_action(
PydocBrowserActions.Home,
text=_("Home"),
tip=_("Home"),
icon=self.create_icon('home'),
triggered=self.go_home,
)
find_action = self.create_action(
PydocBrowserActions.Find,
text=_("Find"),
tip=_("Find text"),
icon=self.create_icon('find'),
toggled=self.toggle_find_widget,
initial=False,
)
stop_action = self.get_action(WebViewActions.Stop)
refresh_action = self.get_action(WebViewActions.Refresh)
# Toolbar
toolbar = self.get_main_toolbar()
for item in [self.get_action(WebViewActions.Back),
self.get_action(WebViewActions.Forward), refresh_action,
stop_action, home_action, self.label, self.url_combo,
self.get_action(WebViewActions.ZoomIn),
self.get_action(WebViewActions.ZoomOut), find_action,
]:
self.add_item_to_toolbar(
item,
toolbar=toolbar,
section=PydocBrowserMainToolbarSections.Main,
)
# Signals
self.find_widget.visibility_changed.connect(find_action.setChecked)
for __, action in self.get_actions().items():
if action:
# IMPORTANT: Since we are defining the main actions in here
# and the context is WidgetWithChildrenShortcut we need to
# assign the same actions to the children widgets in order
# for shortcuts to work
try:
self.webview.addAction(action)
except RuntimeError:
pass
self.sig_toggle_view_changed.connect(self.initialize)
def update_actions(self):
stop_action = self.get_action(WebViewActions.Stop)
refresh_action = self.get_action(WebViewActions.Refresh)
refresh_action.setVisible(not self._is_running)
stop_action.setVisible(self._is_running)
# --- Private API
# ------------------------------------------------------------------------
def _start(self):
"""Webview load started."""
self._is_running = True
self.start_spinner()
self.update_actions()
def _finish(self, code):
"""Webview load finished."""
self._is_running = False
self.stop_spinner()
self.update_actions()
self.sig_load_finished.emit()
def _continue_initialization(self):
"""Load home page."""
self.go_home()
QApplication.restoreOverrideCursor()
def _handle_url_combo_activation(self):
"""Load URL from combo box first item."""
if not self._is_running:
text = str(self.url_combo.currentText())
self.go_to(self.text_to_url(text))
else:
self.get_action(WebViewActions.Stop).trigger()
self.get_focus_widget().setFocus()
def _change_url(self, url):
"""
Displayed URL has changed -> updating URL combo box.
"""
self.url_combo.add_text(self.url_to_text(url))
def _handle_icon_change(self):
"""
Handle icon changes.
"""
self.url_combo.setItemIcon(self.url_combo.currentIndex(),
self.webview.icon())
self.setWindowIcon(self.webview.icon())
# --- Qt overrides
# ------------------------------------------------------------------------
def closeEvent(self, event):
self.webview.web_widget.stop()
if self.server:
self.server.finished.connect(self.deleteLater)
self.quit_server()
super().closeEvent(event)
# --- Public API
# ------------------------------------------------------------------------
def load_history(self, history):
"""
Load history.
Parameters
----------
history: list
List of searched items.
"""
self.url_combo.addItems(history)
@Slot(bool)
def initialize(self, checked=True, force=False):
"""
Start pydoc server.
Parameters
----------
checked: bool, optional
This method is connected to the `sig_toggle_view_changed` signal,
so that the first time the widget is made visible it will start
the server. Default is True.
force: bool, optional
Force a server start even if the server is running.
Default is False.
"""
server_needed = checked and self.server is None
if force or server_needed or not self.is_server_running():
self.sig_toggle_view_changed.disconnect(self.initialize)
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
self.start_server()
def is_server_running(self):
"""Return True if pydoc server is already running."""
return self.server is not None and self.server.is_running()
def start_server(self):
"""Start pydoc server."""
if self.server is None:
self.set_home_url('http://127.0.0.1:{}/'.format(PORT))
elif self.server.is_running():
self.server.sig_server_started.disconnect(
self._continue_initialization)
self.server.quit()
self.server.wait()
self.server = PydocServer(None, port=PORT)
self.server.sig_server_started.connect(
self._continue_initialization)
self.server.start()
def quit_server(self):
"""Quit the server."""
if self.server is None:
return
if self.server.is_running():
self.server.sig_server_started.disconnect(
self._continue_initialization)
self.server.quit_server()
self.server.quit()
self.server.wait()
def get_label(self):
"""Return address label text"""
return _("Package:")
def reload(self):
"""Reload page."""
if self.server:
self.webview.reload()
def text_to_url(self, text):
"""
Convert text address into QUrl object.
Parameters
----------
text: str
Url address.
"""
if text != 'about:blank':
text += '.html'
if text.startswith('/'):
text = text[1:]
return QUrl(self.home_url.toString() + text)
def url_to_text(self, url):
"""
Convert QUrl object to displayed text in combo box.
Parameters
----------
url: QUrl
Url address.
"""
string_url = url.toString()
if 'about:blank' in string_url:
return 'about:blank'
elif 'get?key=' in string_url or 'search?key=' in string_url:
return url.toString().split('=')[-1]
return osp.splitext(str(url.path()))[0][1:]
def set_home_url(self, text):
"""
Set home URL.
Parameters
----------
text: str
Home url address.
"""
self.home_url = QUrl(text)
def set_url(self, url):
"""
Set current URL.
Parameters
----------
url: QUrl or str
Url address.
"""
self._change_url(url)
self.go_to(url)
def go_to(self, url_or_text):
"""
Go to page URL.
"""
if isinstance(url_or_text, str):
url = QUrl(url_or_text)
else:
url = url_or_text
self.webview.load(url)
@Slot()
def go_home(self):
"""
Go to home page.
"""
if self.home_url is not None:
self.set_url(self.home_url)
def get_zoom_factor(self):
"""
Get the current zoom factor.
Returns
-------
int
Zoom factor.
"""
return self.webview.get_zoom_factor()
def get_history(self):
"""
Return the list of history items in the combobox.
Returns
-------
list
List of strings.
"""
history = []
for index in range(self.url_combo.count()):
history.append(str(self.url_combo.itemText(index)))
return history
@Slot(bool)
def toggle_find_widget(self, state):
"""
Show/hide the find widget.
Parameters
----------
state: bool
True to show and False to hide the find widget.
"""
if state:
self.find_widget.show()
else:
self.find_widget.hide()
def test():
"""Run web browser."""
from spyder.utils.qthelpers import qapplication
from unittest.mock import MagicMock
plugin_mock = MagicMock()
plugin_mock.CONF_SECTION = 'onlinehelp'
app = qapplication(test_time=8)
widget = PydocBrowser(None, plugin=plugin_mock)
widget._setup()
widget.setup()
widget.show()
sys.exit(app.exec_())
if __name__ == '__main__':
test()