Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
help: Fall back to plain text when Qt WebEngine is not available
  • Loading branch information
rear1019 committed Feb 18, 2026
commit 24b79f500b6230d9389ffd4293741cf10473be56
5 changes: 4 additions & 1 deletion spyder/app/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
from spyder import requirements
requirements.check_qt()

# Local imports
from spyder.app.utils import HAVE_WEBENGINE

#==============================================================================
# Third-party imports
#==============================================================================
Expand Down Expand Up @@ -748,7 +751,7 @@ def setup(self):
# https://github.com/spyder-ide/spyder/pull/
# 22196#issuecomment-2189377043
if PluginClass.REQUIRE_WEB_WIDGETS and (
not WEBENGINE or
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qtpy.QtWebEngineWidgets.WEBENGINE and its usage in Spyder is confusing: qtpy used to define it to differentiate between Qt WebEngine and Qt WebKit. This is also the way it is used in most places in Spyder. With WebKit gone/not used by Spyder, the differentiation between WebEngine/WebKit can be removed. I can do this as part of this PR (when the PR is accepted).

Concerning the usage in the sense "is WebEngine available", see the new spyder.app.utils.HAVE_WEBENGINE.

not HAVE_WEBENGINE or
self._cli_options.no_web_widgets
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the fallback to plaintext implemented by this PR, the command line argument --no-web-widgets can be removed, deprecated or made an no-operation.

):
continue
Expand Down
6 changes: 6 additions & 0 deletions spyder/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
except Exception:
QQuickWindow = QSGRendererInterface = None

try:
import qtpy.QtWebEngineWidgets
except ImportError:
HAVE_WEBENGINE = False
else:
HAVE_WEBENGINE = True

root_logger = logging.getLogger()
FILTER_NAMES = os.environ.get('SPYDER_FILTER_LOG', "").split(',')
Expand Down
2 changes: 1 addition & 1 deletion spyder/plugins/help/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class Help(SpyderDockablePlugin):
CONF_FILE = False
LOG_PATH = get_conf_path(CONF_SECTION)
DISABLE_ACTIONS_WHEN_HIDDEN = False
REQUIRE_WEB_WIDGETS = True
REQUIRE_WEB_WIDGETS = False
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR implements a fallback to plain text when Qt WebEngine is not available.

CAN_HANDLE_SEARCH_ACTIONS = True

# Signals
Expand Down
78 changes: 48 additions & 30 deletions spyder/plugins/help/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from spyder.api.widgets.comboboxes import SpyderComboBox
from spyder.api.widgets.main_widget import PluginMainWidget
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.app.utils import HAVE_WEBENGINE
from spyder.config.base import get_module_source_path
from spyder.plugins.help.utils.sphinxify import (CSS_PATH, generate_context,
loading, usage, warning)
Expand Down Expand Up @@ -308,16 +309,18 @@ def __init__(self, name=None, plugin=None, parent=None):
self.docstring = True # TODO: What is this used for?

# Widgets
self._sphinx_thread = SphinxThread(
None,
html_text_no_doc=warning(self.no_docs, css_path=self.css_path),
css_path=self.css_path,
)
self.shell = None
self.internal_console = None
self.internal_shell = None
self.plain_text = PlainText(self)
self.rich_text = RichText(self)

if HAVE_WEBENGINE:
self._sphinx_thread = SphinxThread(
None,
html_text_no_doc=warning(self.no_docs, css_path=self.css_path),
css_path=self.css_path,
)
self.rich_text = RichText(self)

self.source_label = QLabel(_("Source"))
self.source_label.ID = HelpWidgetToolbarItems.SourceLabel
Expand Down Expand Up @@ -347,20 +350,22 @@ def __init__(self, name=None, plugin=None, parent=None):

# Layout
self.stacked_widget = QStackedWidget()
self.stacked_widget.addWidget(self.rich_text)
if HAVE_WEBENGINE:
self.stacked_widget.addWidget(self.rich_text)
self.stacked_widget.addWidget(self.plain_text)

layout = QVBoxLayout()
layout.addWidget(self.stacked_widget)
self.setLayout(layout)

# Signals
self._sphinx_thread.html_ready.connect(
self._on_sphinx_thread_html_ready)
self._sphinx_thread.error_msg.connect(
self._on_sphinx_thread_error_msg)
self.object_combo.valid.connect(self.force_refresh)
self.rich_text.sig_link_clicked.connect(self.handle_link_clicks)
if HAVE_WEBENGINE:
self._sphinx_thread.html_ready.connect(
self._on_sphinx_thread_html_ready)
self._sphinx_thread.error_msg.connect(
self._on_sphinx_thread_error_msg)
self.rich_text.sig_link_clicked.connect(self.handle_link_clicks)
self.source_combo.currentIndexChanged.connect(
lambda x: self.source_changed())
self.sig_render_started.connect(self.start_spinner)
Expand Down Expand Up @@ -404,18 +409,21 @@ def setup(self):
toggled=True,
option='show_source'
)

self.rich_text_action = self.create_action(
name=HelpWidgetActions.ToggleRichMode,
text=_("Rich Text"),
toggled=True,
initial=self.get_conf('rich_mode'),
initial=self.get_conf('rich_mode') and HAVE_WEBENGINE,
option='rich_mode'
)
self.rich_text_action.setEnabled(HAVE_WEBENGINE)

self.plain_text_action = self.create_action(
name=HelpWidgetActions.TogglePlainMode,
text=_("Plain Text"),
toggled=True,
initial=self.get_conf('plain_mode'),
initial=self.get_conf('plain_mode') or not HAVE_WEBENGINE,
option='plain_mode'
)
self.locked_action = self.create_action(
Expand Down Expand Up @@ -486,7 +494,10 @@ def setup(self):
)

self.source_changed()
self.switch_to_rich_text()
if self.get_conf("rich_mode") and HAVE_WEBENGINE:
self.switch_to_rich_text()
else:
self.switch_to_plain_text()
self.show_intro_message()

# Signals
Expand Down Expand Up @@ -526,16 +537,18 @@ def on_automatic_import_update(self, value):

@on_conf_change(option='rich_mode')
def on_rich_mode_update(self, value):
if value:
if value and HAVE_WEBENGINE:
# Plain Text OFF / Rich text ON
self.docstring = not value
self.stacked_widget.setCurrentWidget(self.rich_text)
self.get_action(HelpWidgetActions.ToggleShowSource).setChecked(
False)
self.get_action(HelpWidgetActions.ToggleRichMode).setChecked(True)
else:
# Plain Text ON / Rich text OFF
self.docstring = value
self.stacked_widget.setCurrentWidget(self.plain_text)
self.get_action(HelpWidgetActions.TogglePlainMode).setChecked(True)

if self._should_display_welcome_page():
self.show_intro_message()
Expand Down Expand Up @@ -666,23 +679,23 @@ def restore_text(self):
cb = self._last_editor_cb

if cb is None:
if self.get_conf('plain_mode'):
if self.get_conf('plain_mode') or not HAVE_WEBENGINE:
self.switch_to_plain_text()
else:
self.switch_to_rich_text()
else:
func = cb[0]
args = cb[1:]
func(*args)
if func.__self__ is self.rich_text:
if HAVE_WEBENGINE and func.__self__ is self.rich_text:
self.switch_to_rich_text()
else:
self.switch_to_plain_text()

@property
def find_widget(self):
"""Show find widget."""
if self.get_conf('plain_mode'):
if self.get_conf('plain_mode') or not HAVE_WEBENGINE:
return self.plain_text.find_widget
else:
return self.rich_text.find_widget
Expand Down Expand Up @@ -792,7 +805,7 @@ def show_intro_message(self):
shortcut_editor = shortcut_editor.replace('Ctrl', 'Cmd')
shortcut_console = shortcut_console.replace('Ctrl', 'Cmd')

if self.get_conf('rich_mode'):
if self.get_conf('rich_mode') and HAVE_WEBENGINE:
title = _("Usage")
tutorial_message = _("New to Spyder? Read our")
tutorial = _("tutorial")
Expand All @@ -811,9 +824,10 @@ def show_intro_message(self):
css_path=self.css_path),
QUrl.fromLocalFile(self.css_path))
else:
install_sphinx = "\n\n%s" % _("Please consider installing Sphinx "
"to get documentation rendered in "
"rich text.")
# TOOD plain text fallback
install_sphinx = "\n\n%s" % _("Please consider installing "
"Qt WebEngine to get documentation "
"rendered in rich text.")
if shortcut_editor == shortcut_console:
intro_message = (intro_message_eq + intro_message_common) % (
shortcut_editor, "\n\n", prefs)
Expand All @@ -838,10 +852,13 @@ def show_rich_text(self, text, collapse=False, img_path=''):
Path to folder with additional images needed to correctly
display the rich text help. Default is ''.
"""
self.switch_to_rich_text()
context = generate_context(collapse=collapse, img_path=img_path,
css_path=self.css_path)
self.render_sphinx_doc(text, context)
if HAVE_WEBENGINE:
self.switch_to_rich_text()
context = generate_context(collapse=collapse, img_path=img_path,
css_path=self.css_path)
self.render_sphinx_doc(text, context)
else:
self.show_plain_text(text)

def show_plain_text(self, text):
"""
Expand Down Expand Up @@ -971,7 +988,7 @@ def set_editor_doc(self, help_data, force_refresh=False):
self._last_editor_doc = help_data
self.object_edit.setText(help_data['obj_text'])

if self.get_conf('rich_mode'):
if self.get_conf('rich_mode') and HAVE_WEBENGINE:
self.render_sphinx_doc(help_data)
else:
self.set_plain_text(help_data, is_code=False)
Expand Down Expand Up @@ -1058,7 +1075,7 @@ def show_help(self, obj_text, ignore_unknown=False):

is_code = False

if self.get_conf('rich_mode'):
if self.get_conf('rich_mode') and HAVE_WEBENGINE:
self.render_sphinx_doc(doc, css_path=self.css_path)
return doc is not None
elif self.docstring:
Expand Down Expand Up @@ -1090,7 +1107,8 @@ def set_rich_text_font(self, font, fixed_font):
fixed_font: QFont
The current rich text font to use.
"""
self.rich_text.set_font(font, fixed_font=fixed_font)
if HAVE_WEBENGINE:
self.rich_text.set_font(font, fixed_font=fixed_font)

def set_plain_text_font(self, font, color_scheme=None):
"""
Expand Down
16 changes: 10 additions & 6 deletions spyder/plugins/ipythonconsole/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from spyder.plugins.debugger.plugin import Debugger
from spyder.plugins.help.utils.sphinxify import CSS_PATH
from spyder.plugins.ipythonconsole.plugin import IPythonConsole
import spyder.plugins.ipythonconsole.widgets.main_widget
from spyder.utils.conda import get_list_conda_envs


Expand Down Expand Up @@ -90,19 +91,14 @@ def get_conda_test_env():
# ---- Fixtures
# =============================================================================
@pytest.fixture
def ipyconsole(qtbot, request, tmpdir):
def ipyconsole(qtbot, request, tmpdir, monkeypatch):
"""IPython console fixture."""
configuration = CONF
no_web_widgets = request.node.get_closest_marker('no_web_widgets')

class MainWindowMock(QMainWindow):

def __init__(self):
# This avoids using the cli options passed to pytest
sys_argv = [sys.argv[0]]
self._cli_options = get_options(sys_argv)[0]
if no_web_widgets:
self._cli_options.no_web_widgets = True
super().__init__()

def __getattr__(self, attr):
Expand Down Expand Up @@ -190,6 +186,14 @@ def __getattr__(self, attr):
# Conf css_path in the Appeareance plugin
configuration.set('appearance', 'css_path', CSS_PATH)

no_web_widgets = request.node.get_closest_marker("no_web_widgets")
monkeypatch.setattr(
spyder.plugins.ipythonconsole.widgets.main_widget,
"HAVE_WEBENGINE",
not no_web_widgets,
raising=True,
)

# Create the console and a new client and set environment
os.environ['IPYCONSOLE_TESTING'] = 'True'
window = MainWindowMock()
Expand Down
7 changes: 2 additions & 5 deletions spyder/plugins/ipythonconsole/widgets/main_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from spyder.api.config.decorators import on_conf_change
from spyder.api.translations import _
from spyder.api.widgets.main_widget import PluginMainWidget
from spyder.app.utils import HAVE_WEBENGINE
from spyder.config.base import get_home_dir, running_under_pytest
from spyder.plugins.application.api import ApplicationActions
from spyder.plugins.ipythonconsole.api import (
Expand Down Expand Up @@ -310,11 +311,7 @@ def __init__(self, name=None, plugin=None, parent=None):
self._last_time_for_restart_dialog = None

# Disable infowidget if requested by the user
self.enable_infowidget = True
if plugin:
cli_options = plugin.get_command_line_options()
if cli_options.no_web_widgets or not WEBENGINE:
self.enable_infowidget = False
self.enable_infowidget = HAVE_WEBENGINE

# Attrs for testing
self._testing = bool(os.environ.get('IPYCONSOLE_TESTING'))
Expand Down
Loading