Merge branch 'per-url'
This commit is contained in:
commit
52b5492c6a
@ -274,7 +274,8 @@ Set all settings back to their default.
|
||||
|
||||
[[config-cycle]]
|
||||
=== config-cycle
|
||||
Syntax: +:config-cycle [*--temp*] [*--print*] 'option' ['values' ['values' ...]]+
|
||||
Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*]
|
||||
'option' ['values' ['values' ...]]+
|
||||
|
||||
Cycle an option between multiple values.
|
||||
|
||||
@ -283,6 +284,7 @@ Cycle an option between multiple values.
|
||||
* +'values'+: The values to cycle through.
|
||||
|
||||
==== optional arguments
|
||||
* +*-u*+, +*--pattern*+: The URL pattern to use.
|
||||
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
|
||||
* +*-p*+, +*--print*+: Print the value after setting.
|
||||
|
||||
@ -1110,7 +1112,7 @@ Save a session.
|
||||
|
||||
[[set]]
|
||||
=== set
|
||||
Syntax: +:set [*--temp*] [*--print*] ['option'] ['value']+
|
||||
Syntax: +:set [*--temp*] [*--print*] [*--pattern* 'pattern'] ['option'] ['value']+
|
||||
|
||||
Set an option.
|
||||
|
||||
@ -1123,6 +1125,7 @@ If the option name ends with '?', the value of the option is shown instead. Usin
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
|
||||
* +*-p*+, +*--print*+: Print the value after setting.
|
||||
* +*-u*+, +*--pattern*+: The URL pattern to use.
|
||||
|
||||
[[set-cmd-text]]
|
||||
=== set-cmd-text
|
||||
|
@ -63,6 +63,10 @@ customizable.
|
||||
Using the link:commands.html#set[`:set`] command and command completion, you
|
||||
can quickly set settings interactively, for example `:set tabs.position left`.
|
||||
|
||||
Some settings are also customizable for a given
|
||||
https://developer.chrome.com/apps/match_patterns[URL pattern] by doing e.g.
|
||||
`:set --pattern=*://example.com/ content.images false`.
|
||||
|
||||
To get more help about a setting, use e.g. `:help tabs.position`.
|
||||
|
||||
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
|
||||
@ -147,7 +151,6 @@ prefix to preserve backslashes) or a Python regex object:
|
||||
If you want to read a setting, you can use the `c` object to do so as well:
|
||||
`c.colors.tabs.even.bg = c.colors.tabs.odd.bg`.
|
||||
|
||||
|
||||
Using strings for setting names
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -171,6 +174,26 @@ To read a setting, use the `config.get` method:
|
||||
color = config.get('colors.completion.fg')
|
||||
----
|
||||
|
||||
Per-domain settings
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Using `config.set`, some settings are also customizable for a given
|
||||
https://developer.chrome.com/apps/match_patterns[URL pattern]:
|
||||
|
||||
[source,python]
|
||||
----
|
||||
config.set('content.images', False, '*://example.com/')
|
||||
----
|
||||
|
||||
Alternatively, you can use `with config.pattern(...) as p:` to get a shortcut
|
||||
similar to `c.` which is scoped to the given domain:
|
||||
|
||||
[source,python]
|
||||
----
|
||||
with config.pattern('*://example.com/') as p:
|
||||
p.content.images = False
|
||||
----
|
||||
|
||||
Binding keys
|
||||
~~~~~~~~~~~~
|
||||
|
||||
|
@ -582,8 +582,14 @@ Default:
|
||||
* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+
|
||||
* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+
|
||||
* +pass:[ss]+: +pass:[set-cmd-text -s :set]+
|
||||
* +pass:[tSH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.javascript.enabled ;; reload]+
|
||||
* +pass:[tSh]+: +pass:[config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload]+
|
||||
* +pass:[tSu]+: +pass:[config-cycle -p -u {url} content.javascript.enabled ;; reload]+
|
||||
* +pass:[th]+: +pass:[back -t]+
|
||||
* +pass:[tl]+: +pass:[forward -t]+
|
||||
* +pass:[tsH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.javascript.enabled ;; reload]+
|
||||
* +pass:[tsh]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload]+
|
||||
* +pass:[tsu]+: +pass:[config-cycle -p -t -u {url} content.javascript.enabled ;; reload]+
|
||||
* +pass:[u]+: +pass:[undo]+
|
||||
* +pass:[v]+: +pass:[enter-mode caret]+
|
||||
* +pass:[wB]+: +pass:[set-cmd-text -s :bookmark-load -w]+
|
||||
@ -1447,6 +1453,8 @@ Default:
|
||||
Enable support for the HTML 5 web application cache feature.
|
||||
An application cache acts like an HTTP cache in some sense. For documents that use the application cache via JavaScript, the loader engine will first ask the application cache for the contents, before hitting the network.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1524,6 +1532,8 @@ This setting is only available with the QtWebKit backend.
|
||||
=== content.dns_prefetch
|
||||
Try to pre-fetch DNS entries to speed up browsing.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1535,6 +1545,8 @@ This setting is only available with the QtWebKit backend.
|
||||
Expand each subframe to its contents.
|
||||
This will flatten all the frames to become one scrollable page.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -1651,6 +1663,8 @@ Default:
|
||||
=== content.hyperlink_auditing
|
||||
Enable hyperlink auditing (`<a ping>`).
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -1659,6 +1673,8 @@ Default: +pass:[false]+
|
||||
=== content.images
|
||||
Load images automatically in web pages.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1676,6 +1692,8 @@ Default: +pass:[true]+
|
||||
Allow JavaScript to read from or write to the clipboard.
|
||||
With QtWebEngine, writing the clipboard as response to a user interaction is always allowed.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -1684,6 +1702,8 @@ Default: +pass:[false]+
|
||||
=== content.javascript.can_close_tabs
|
||||
Allow JavaScript to close tabs.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -1694,6 +1714,8 @@ This setting is only available with the QtWebKit backend.
|
||||
=== content.javascript.can_open_tabs_automatically
|
||||
Allow JavaScript to open new tabs without user interaction.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -1702,6 +1724,8 @@ Default: +pass:[false]+
|
||||
=== content.javascript.enabled
|
||||
Enable JavaScript.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1741,6 +1765,8 @@ Default: +pass:[true]+
|
||||
=== content.local_content_can_access_file_urls
|
||||
Allow locally loaded documents to access other local URLs.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1749,6 +1775,8 @@ Default: +pass:[true]+
|
||||
=== content.local_content_can_access_remote_urls
|
||||
Allow locally loaded documents to access remote URLs.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -1757,6 +1785,8 @@ Default: +pass:[false]+
|
||||
=== content.local_storage
|
||||
Enable support for HTML 5 local storage and Web SQL.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1817,6 +1847,8 @@ This setting is only available with the QtWebKit backend.
|
||||
=== content.plugins
|
||||
Enable plugins in Web pages.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -1825,6 +1857,8 @@ Default: +pass:[false]+
|
||||
=== content.print_element_backgrounds
|
||||
Draw the background color and images also when the page is printed.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1889,6 +1923,8 @@ Default: empty
|
||||
=== content.webgl
|
||||
Enable WebGL.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1906,6 +1942,8 @@ Default: +pass:[false]+
|
||||
Monitor load requests for cross-site scripting attempts.
|
||||
Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -2379,6 +2417,8 @@ Default: +pass:[false]+
|
||||
=== input.links_included_in_focus_chain
|
||||
Include hyperlinks in the keyboard focus chain when tabbing.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -2406,6 +2446,8 @@ Default: +pass:[false]+
|
||||
Enable spatial navigation.
|
||||
Spatial navigation consists in the ability to navigate between focusable elements in a Web page, such as hyperlinks and form controls, by using Left, Right, Up and Down arrow keys. For example, if the user presses the Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -2550,6 +2592,8 @@ Default: +pass:[false]+
|
||||
Enable smooth scrolling for web pages.
|
||||
Note smooth scrolling does not work with the `:scroll-px` command.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
@ -3137,6 +3181,8 @@ Default: +pass:[512]+
|
||||
=== zoom.text_only
|
||||
Apply the zoom factor on a frame only to the text or to all content.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
@ -30,7 +30,8 @@ from PyQt5.QtWidgets import QWidget, QApplication
|
||||
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import utils, objreg, usertypes, log, qtutils
|
||||
from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
|
||||
urlutils, message)
|
||||
from qutebrowser.misc import miscwidgets, objects
|
||||
from qutebrowser.browser import mouse, hints
|
||||
|
||||
@ -94,6 +95,8 @@ class TabData:
|
||||
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
|
||||
load.
|
||||
inspector: The QWebInspector used for this webview.
|
||||
open_target: Where to open the next link.
|
||||
Only used for QtWebKit.
|
||||
override_target: Override for open_target for fake clicks (like hints).
|
||||
Only used for QtWebKit.
|
||||
pinned: Flag to pin the tab.
|
||||
@ -104,6 +107,7 @@ class TabData:
|
||||
|
||||
keep_icon = attr.ib(False)
|
||||
inspector = attr.ib(None)
|
||||
open_target = attr.ib(usertypes.ClickTarget.normal)
|
||||
override_target = attr.ib(None)
|
||||
pinned = attr.ib(False)
|
||||
fullscreen = attr.ib(False)
|
||||
@ -612,6 +616,7 @@ class AbstractTab(QWidget):
|
||||
process terminated.
|
||||
arg 0: A TerminationStatus member.
|
||||
arg 1: The exit code.
|
||||
predicted_navigation: Emitted before we tell Qt to open a URL.
|
||||
"""
|
||||
|
||||
window_close_requested = pyqtSignal()
|
||||
@ -629,6 +634,7 @@ class AbstractTab(QWidget):
|
||||
add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title
|
||||
fullscreen_requested = pyqtSignal(bool)
|
||||
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
|
||||
predicted_navigation = pyqtSignal(QUrl)
|
||||
|
||||
def __init__(self, *, win_id, mode_manager, private, parent=None):
|
||||
self.private = private
|
||||
@ -659,6 +665,9 @@ class AbstractTab(QWidget):
|
||||
objreg.register('hintmanager', hintmanager, scope='tab',
|
||||
window=self.win_id, tab=self.tab_id)
|
||||
|
||||
self.predicted_navigation.connect(
|
||||
lambda url: self.title_changed.emit(url.toDisplayString()))
|
||||
|
||||
def _set_widget(self, widget):
|
||||
# pylint: disable=protected-access
|
||||
self._widget = widget
|
||||
@ -671,6 +680,7 @@ class AbstractTab(QWidget):
|
||||
self.printing._widget = widget
|
||||
self.action._widget = widget
|
||||
self.elements._widget = widget
|
||||
self.settings._settings = widget.settings()
|
||||
|
||||
self._install_event_filter()
|
||||
self.zoom.set_default()
|
||||
@ -719,6 +729,22 @@ class AbstractTab(QWidget):
|
||||
self._set_load_status(usertypes.LoadStatus.loading)
|
||||
self.load_started.emit()
|
||||
|
||||
@pyqtSlot(usertypes.NavigationRequest)
|
||||
def _on_navigation_request(self, navigation):
|
||||
"""Handle common acceptNavigationRequest code."""
|
||||
log.webview.debug("navigation request: url {}, type {}, is_main_frame "
|
||||
"{}".format(navigation.url.toDisplayString(),
|
||||
navigation.navigation_type,
|
||||
navigation.is_main_frame))
|
||||
|
||||
if (navigation.navigation_type == navigation.Type.link_clicked and
|
||||
not navigation.url.isValid()):
|
||||
msg = urlutils.get_errstring(navigation.url,
|
||||
"Invalid link clicked")
|
||||
message.error(msg)
|
||||
self.data.open_target = usertypes.ClickTarget.normal
|
||||
navigation.accepted = False
|
||||
|
||||
def handle_auto_insert_mode(self, ok):
|
||||
"""Handle `input.insert_mode.auto_load` after loading finished."""
|
||||
if not config.val.input.insert_mode.auto_load or not ok:
|
||||
@ -790,7 +816,7 @@ class AbstractTab(QWidget):
|
||||
|
||||
def _openurl_prepare(self, url):
|
||||
qtutils.ensure_valid(url)
|
||||
self.title_changed.emit(url.toDisplayString())
|
||||
self.predicted_navigation.emit(url)
|
||||
|
||||
def openurl(self, url):
|
||||
raise NotImplementedError
|
||||
|
@ -17,9 +17,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# We get various "abstract but not overridden" warnings
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
"""Bridge from QWebEngineSettings to our own settings.
|
||||
|
||||
Module attributes:
|
||||
@ -44,116 +41,143 @@ from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
|
||||
default_profile = None
|
||||
# The QWebEngineProfile used for private (off-the-record) windows
|
||||
private_profile = None
|
||||
# The global WebEngineSettings object
|
||||
global_settings = None
|
||||
|
||||
|
||||
class Base(websettings.Base):
|
||||
class _SettingsWrapper:
|
||||
|
||||
"""Base settings class with appropriate _get_global_settings."""
|
||||
"""Expose a QWebEngineSettings interface which acts on all profiles.
|
||||
|
||||
def _get_global_settings(self):
|
||||
return [default_profile.settings(), private_profile.settings()]
|
||||
|
||||
|
||||
class Attribute(Base, websettings.Attribute):
|
||||
|
||||
"""A setting set via QWebEngineSettings::setAttribute."""
|
||||
|
||||
ENUM_BASE = QWebEngineSettings
|
||||
|
||||
|
||||
class Setter(Base, websettings.Setter):
|
||||
|
||||
"""A setting set via a QWebEngineSettings setter method."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FontFamilySetter(Base, websettings.FontFamilySetter):
|
||||
|
||||
"""A setter for a font family.
|
||||
|
||||
Gets the default value from QFont.
|
||||
For read operations, the default profile value is always used.
|
||||
"""
|
||||
|
||||
def __init__(self, font):
|
||||
# Mapping from WebEngineSettings::initDefaults in
|
||||
# qtwebengine/src/core/web_engine_settings.cpp
|
||||
font_to_qfont = {
|
||||
QWebEngineSettings.StandardFont: QFont.Serif,
|
||||
QWebEngineSettings.FixedFont: QFont.Monospace,
|
||||
QWebEngineSettings.SerifFont: QFont.Serif,
|
||||
QWebEngineSettings.SansSerifFont: QFont.SansSerif,
|
||||
QWebEngineSettings.CursiveFont: QFont.Cursive,
|
||||
QWebEngineSettings.FantasyFont: QFont.Fantasy,
|
||||
def __init__(self):
|
||||
self._settings = [default_profile.settings(),
|
||||
private_profile.settings()]
|
||||
|
||||
def setAttribute(self, *args, **kwargs):
|
||||
for settings in self._settings:
|
||||
settings.setAttribute(*args, **kwargs)
|
||||
|
||||
def setFontFamily(self, *args, **kwargs):
|
||||
for settings in self._settings:
|
||||
settings.setFontFamily(*args, **kwargs)
|
||||
|
||||
def setFontSize(self, *args, **kwargs):
|
||||
for settings in self._settings:
|
||||
settings.setFontSize(*args, **kwargs)
|
||||
|
||||
def setDefaultTextEncoding(self, *args, **kwargs):
|
||||
for settings in self._settings:
|
||||
settings.setDefaultTextEncoding(*args, **kwargs)
|
||||
|
||||
def testAttribute(self, *args, **kwargs):
|
||||
return self._settings[0].testAttribute(*args, **kwargs)
|
||||
|
||||
def fontSize(self, *args, **kwargs):
|
||||
return self._settings[0].fontSize(*args, **kwargs)
|
||||
|
||||
def fontFamily(self, *args, **kwargs):
|
||||
return self._settings[0].fontFamily(*args, **kwargs)
|
||||
|
||||
def defaultTextEncoding(self, *args, **kwargs):
|
||||
return self._settings[0].defaultTextEncoding(*args, **kwargs)
|
||||
|
||||
|
||||
class WebEngineSettings(websettings.AbstractSettings):
|
||||
|
||||
"""A wrapper for the config for QWebEngineSettings."""
|
||||
|
||||
_ATTRIBUTES = {
|
||||
'content.xss_auditing':
|
||||
[QWebEngineSettings.XSSAuditingEnabled],
|
||||
'content.images':
|
||||
[QWebEngineSettings.AutoLoadImages],
|
||||
'content.javascript.enabled':
|
||||
[QWebEngineSettings.JavascriptEnabled],
|
||||
'content.javascript.can_open_tabs_automatically':
|
||||
[QWebEngineSettings.JavascriptCanOpenWindows],
|
||||
'content.javascript.can_access_clipboard':
|
||||
[QWebEngineSettings.JavascriptCanAccessClipboard],
|
||||
'content.plugins':
|
||||
[QWebEngineSettings.PluginsEnabled],
|
||||
'content.hyperlink_auditing':
|
||||
[QWebEngineSettings.HyperlinkAuditingEnabled],
|
||||
'content.local_content_can_access_remote_urls':
|
||||
[QWebEngineSettings.LocalContentCanAccessRemoteUrls],
|
||||
'content.local_content_can_access_file_urls':
|
||||
[QWebEngineSettings.LocalContentCanAccessFileUrls],
|
||||
'content.webgl':
|
||||
[QWebEngineSettings.WebGLEnabled],
|
||||
'content.local_storage':
|
||||
[QWebEngineSettings.LocalStorageEnabled],
|
||||
|
||||
'input.spatial_navigation':
|
||||
[QWebEngineSettings.SpatialNavigationEnabled],
|
||||
'input.links_included_in_focus_chain':
|
||||
[QWebEngineSettings.LinksIncludedInFocusChain],
|
||||
|
||||
'scrolling.smooth':
|
||||
[QWebEngineSettings.ScrollAnimatorEnabled],
|
||||
|
||||
# Missing QtWebEngine attributes:
|
||||
# - ScreenCaptureEnabled
|
||||
# - Accelerated2dCanvasEnabled
|
||||
# - AutoLoadIconsForPage
|
||||
# - TouchIconsEnabled
|
||||
# - FocusOnNavigationEnabled (5.8)
|
||||
# - AllowRunningInsecureContent (5.8)
|
||||
}
|
||||
|
||||
_FONT_SIZES = {
|
||||
'fonts.web.size.minimum':
|
||||
QWebEngineSettings.MinimumFontSize,
|
||||
'fonts.web.size.minimum_logical':
|
||||
QWebEngineSettings.MinimumLogicalFontSize,
|
||||
'fonts.web.size.default':
|
||||
QWebEngineSettings.DefaultFontSize,
|
||||
'fonts.web.size.default_fixed':
|
||||
QWebEngineSettings.DefaultFixedFontSize,
|
||||
}
|
||||
|
||||
_FONT_FAMILIES = {
|
||||
'fonts.web.family.standard': QWebEngineSettings.StandardFont,
|
||||
'fonts.web.family.fixed': QWebEngineSettings.FixedFont,
|
||||
'fonts.web.family.serif': QWebEngineSettings.SerifFont,
|
||||
'fonts.web.family.sans_serif': QWebEngineSettings.SansSerifFont,
|
||||
'fonts.web.family.cursive': QWebEngineSettings.CursiveFont,
|
||||
'fonts.web.family.fantasy': QWebEngineSettings.FantasyFont,
|
||||
|
||||
# Missing QtWebEngine fonts:
|
||||
# - PictographFont
|
||||
}
|
||||
|
||||
# Mapping from WebEngineSettings::initDefaults in
|
||||
# qtwebengine/src/core/web_engine_settings.cpp
|
||||
_FONT_TO_QFONT = {
|
||||
QWebEngineSettings.StandardFont: QFont.Serif,
|
||||
QWebEngineSettings.FixedFont: QFont.Monospace,
|
||||
QWebEngineSettings.SerifFont: QFont.Serif,
|
||||
QWebEngineSettings.SansSerifFont: QFont.SansSerif,
|
||||
QWebEngineSettings.CursiveFont: QFont.Cursive,
|
||||
QWebEngineSettings.FantasyFont: QFont.Fantasy,
|
||||
}
|
||||
|
||||
def __init__(self, settings):
|
||||
super().__init__(settings)
|
||||
# Attributes which don't exist in all Qt versions.
|
||||
new_attributes = {
|
||||
# Qt 5.8
|
||||
'content.print_element_backgrounds': 'PrintElementBackgrounds',
|
||||
}
|
||||
super().__init__(setter=QWebEngineSettings.setFontFamily, font=font,
|
||||
qfont=font_to_qfont[font])
|
||||
for name, attribute in new_attributes.items():
|
||||
try:
|
||||
value = getattr(QWebEngineSettings, attribute)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
|
||||
class DefaultProfileSetter(websettings.Base):
|
||||
|
||||
"""A setting set on the QWebEngineProfile."""
|
||||
|
||||
def __init__(self, setter, converter=None, default=websettings.UNSET):
|
||||
super().__init__(default)
|
||||
self._setter = setter
|
||||
self._converter = converter
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, setter=self._setter, constructor=True)
|
||||
|
||||
def _set(self, value, settings=None):
|
||||
if settings is not None:
|
||||
raise ValueError("'settings' may not be set with "
|
||||
"DefaultProfileSetters!")
|
||||
|
||||
setter = getattr(default_profile, self._setter)
|
||||
if self._converter is not None:
|
||||
value = self._converter(value)
|
||||
|
||||
setter(value)
|
||||
|
||||
|
||||
class PersistentCookiePolicy(DefaultProfileSetter):
|
||||
|
||||
"""The content.cookies.store setting is different from other settings."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('setPersistentCookiesPolicy')
|
||||
|
||||
def _set(self, value, settings=None):
|
||||
if settings is not None:
|
||||
raise ValueError("'settings' may not be set with "
|
||||
"PersistentCookiePolicy!")
|
||||
setter = getattr(QWebEngineProfile.defaultProfile(), self._setter)
|
||||
setter(
|
||||
QWebEngineProfile.AllowPersistentCookies if value else
|
||||
QWebEngineProfile.NoPersistentCookies
|
||||
)
|
||||
|
||||
|
||||
class DictionaryLanguageSetter(DefaultProfileSetter):
|
||||
|
||||
"""Sets paths to dictionary files based on language codes."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('setSpellCheckLanguages', default=[])
|
||||
|
||||
def _find_installed(self, code):
|
||||
local_filename = spell.local_filename(code)
|
||||
if not local_filename:
|
||||
message.warning(
|
||||
"Language {} is not installed - see scripts/dictcli.py "
|
||||
"in qutebrowser's sources".format(code))
|
||||
return local_filename
|
||||
|
||||
def _set(self, value, settings=None):
|
||||
if settings is not None:
|
||||
raise ValueError("'settings' may not be set with "
|
||||
"DictionaryLanguageSetter!")
|
||||
filenames = [self._find_installed(code) for code in value]
|
||||
log.config.debug("Found dicts: {}".format(filenames))
|
||||
super()._set([f for f in filenames if f], settings)
|
||||
self._ATTRIBUTES[name] = [value]
|
||||
|
||||
|
||||
def _init_stylesheet(profile):
|
||||
@ -210,9 +234,47 @@ def _set_http_headers(profile):
|
||||
profile.setHttpAcceptLanguage(accept_language)
|
||||
|
||||
|
||||
def _set_http_cache_size(profile):
|
||||
"""Initialize the HTTP cache size for the given profile."""
|
||||
size = config.val.content.cache.size
|
||||
if size is None:
|
||||
size = 0
|
||||
else:
|
||||
size = qtutils.check_overflow(size, 'int', fatal=False)
|
||||
|
||||
# 0: automatically managed by QtWebEngine
|
||||
profile.setHttpCacheMaximumSize(size)
|
||||
|
||||
|
||||
def _set_persistent_cookie_policy(profile):
|
||||
"""Set the HTTP Cookie size for the given profile."""
|
||||
if config.val.content.cookies.store:
|
||||
value = QWebEngineProfile.AllowPersistentCookies
|
||||
else:
|
||||
value = QWebEngineProfile.NoPersistentCookies
|
||||
profile.setPersistentCookiesPolicy(value)
|
||||
|
||||
|
||||
def _set_dictionary_language(profile):
|
||||
filenames = []
|
||||
for code in config.val.spellcheck.languages or []:
|
||||
local_filename = spell.local_filename(code)
|
||||
if not local_filename:
|
||||
message.warning(
|
||||
"Language {} is not installed - see scripts/dictcli.py "
|
||||
"in qutebrowser's sources".format(code))
|
||||
continue
|
||||
|
||||
filenames.append(local_filename)
|
||||
|
||||
log.config.debug("Found dicts: {}".format(filenames))
|
||||
profile.setSpellCheckLanguages(filenames)
|
||||
|
||||
|
||||
def _update_settings(option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
websettings.update_mappings(MAPPINGS, option)
|
||||
global_settings.update_setting(option)
|
||||
|
||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||
_init_stylesheet(default_profile)
|
||||
_init_stylesheet(private_profile)
|
||||
@ -221,27 +283,46 @@ def _update_settings(option):
|
||||
'content.headers.accept_language']:
|
||||
_set_http_headers(default_profile)
|
||||
_set_http_headers(private_profile)
|
||||
elif option == 'content.cache.size':
|
||||
_set_http_cache_size(default_profile)
|
||||
_set_http_cache_size(private_profile)
|
||||
elif (option == 'content.cookies.store' and
|
||||
# https://bugreports.qt.io/browse/QTBUG-58650
|
||||
qtutils.version_check('5.9', compiled=False)):
|
||||
_set_persistent_cookie_policy(default_profile)
|
||||
# We're not touching the private profile's cookie policy.
|
||||
elif option == 'spellcheck.languages' and qtutils.version_check('5.8'):
|
||||
_set_dictionary_language(default_profile)
|
||||
_set_dictionary_language(private_profile)
|
||||
|
||||
|
||||
def _init_profile(profile):
|
||||
"""Init the given profile."""
|
||||
_init_stylesheet(profile)
|
||||
_set_http_headers(profile)
|
||||
_set_http_cache_size(profile)
|
||||
profile.settings().setAttribute(
|
||||
QWebEngineSettings.FullScreenSupportEnabled, True)
|
||||
if qtutils.version_check('5.8'):
|
||||
profile.setSpellCheckEnabled(True)
|
||||
_set_dictionary_language(profile)
|
||||
|
||||
|
||||
def _init_profiles():
|
||||
"""Init the two used QWebEngineProfiles."""
|
||||
global default_profile, private_profile
|
||||
|
||||
default_profile = QWebEngineProfile.defaultProfile()
|
||||
default_profile.setCachePath(
|
||||
os.path.join(standarddir.cache(), 'webengine'))
|
||||
default_profile.setPersistentStoragePath(
|
||||
os.path.join(standarddir.data(), 'webengine'))
|
||||
_init_stylesheet(default_profile)
|
||||
_set_http_headers(default_profile)
|
||||
_init_profile(default_profile)
|
||||
_set_persistent_cookie_policy(default_profile)
|
||||
|
||||
private_profile = QWebEngineProfile()
|
||||
assert private_profile.isOffTheRecord()
|
||||
_init_stylesheet(private_profile)
|
||||
_set_http_headers(private_profile)
|
||||
|
||||
if qtutils.version_check('5.8'):
|
||||
default_profile.setSpellCheckEnabled(True)
|
||||
private_profile.setSpellCheckEnabled(True)
|
||||
_init_profile(private_profile)
|
||||
|
||||
|
||||
def inject_userscripts():
|
||||
@ -287,111 +368,13 @@ def init(args):
|
||||
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
|
||||
|
||||
_init_profiles()
|
||||
|
||||
# We need to do this here as a WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-58650
|
||||
if not qtutils.version_check('5.9', compiled=False):
|
||||
PersistentCookiePolicy().set(config.val.content.cookies.store)
|
||||
Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True)
|
||||
|
||||
websettings.init_mappings(MAPPINGS)
|
||||
config.instance.changed.connect(_update_settings)
|
||||
|
||||
global global_settings
|
||||
global_settings = WebEngineSettings(_SettingsWrapper())
|
||||
global_settings.init_settings()
|
||||
|
||||
|
||||
def shutdown():
|
||||
# FIXME:qtwebengine do we need to do something for a clean shutdown here?
|
||||
pass
|
||||
|
||||
|
||||
# Missing QtWebEngine attributes:
|
||||
# - ScreenCaptureEnabled
|
||||
# - Accelerated2dCanvasEnabled
|
||||
# - AutoLoadIconsForPage
|
||||
# - TouchIconsEnabled
|
||||
# - FocusOnNavigationEnabled (5.8)
|
||||
# - AllowRunningInsecureContent (5.8)
|
||||
#
|
||||
# Missing QtWebEngine fonts:
|
||||
# - PictographFont
|
||||
|
||||
|
||||
MAPPINGS = {
|
||||
'content.images':
|
||||
Attribute(QWebEngineSettings.AutoLoadImages),
|
||||
'content.javascript.enabled':
|
||||
Attribute(QWebEngineSettings.JavascriptEnabled),
|
||||
'content.javascript.can_open_tabs_automatically':
|
||||
Attribute(QWebEngineSettings.JavascriptCanOpenWindows),
|
||||
'content.javascript.can_access_clipboard':
|
||||
Attribute(QWebEngineSettings.JavascriptCanAccessClipboard),
|
||||
'content.plugins':
|
||||
Attribute(QWebEngineSettings.PluginsEnabled),
|
||||
'content.hyperlink_auditing':
|
||||
Attribute(QWebEngineSettings.HyperlinkAuditingEnabled),
|
||||
'content.local_content_can_access_remote_urls':
|
||||
Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
|
||||
'content.local_content_can_access_file_urls':
|
||||
Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls),
|
||||
'content.webgl':
|
||||
Attribute(QWebEngineSettings.WebGLEnabled),
|
||||
'content.local_storage':
|
||||
Attribute(QWebEngineSettings.LocalStorageEnabled),
|
||||
'content.cache.size':
|
||||
# 0: automatically managed by QtWebEngine
|
||||
DefaultProfileSetter('setHttpCacheMaximumSize', default=0,
|
||||
converter=lambda val:
|
||||
qtutils.check_overflow(val, 'int', fatal=False)),
|
||||
'content.xss_auditing':
|
||||
Attribute(QWebEngineSettings.XSSAuditingEnabled),
|
||||
'content.default_encoding':
|
||||
Setter(QWebEngineSettings.setDefaultTextEncoding),
|
||||
|
||||
'input.spatial_navigation':
|
||||
Attribute(QWebEngineSettings.SpatialNavigationEnabled),
|
||||
'input.links_included_in_focus_chain':
|
||||
Attribute(QWebEngineSettings.LinksIncludedInFocusChain),
|
||||
|
||||
'fonts.web.family.standard':
|
||||
FontFamilySetter(QWebEngineSettings.StandardFont),
|
||||
'fonts.web.family.fixed':
|
||||
FontFamilySetter(QWebEngineSettings.FixedFont),
|
||||
'fonts.web.family.serif':
|
||||
FontFamilySetter(QWebEngineSettings.SerifFont),
|
||||
'fonts.web.family.sans_serif':
|
||||
FontFamilySetter(QWebEngineSettings.SansSerifFont),
|
||||
'fonts.web.family.cursive':
|
||||
FontFamilySetter(QWebEngineSettings.CursiveFont),
|
||||
'fonts.web.family.fantasy':
|
||||
FontFamilySetter(QWebEngineSettings.FantasyFont),
|
||||
'fonts.web.size.minimum':
|
||||
Setter(QWebEngineSettings.setFontSize,
|
||||
args=[QWebEngineSettings.MinimumFontSize]),
|
||||
'fonts.web.size.minimum_logical':
|
||||
Setter(QWebEngineSettings.setFontSize,
|
||||
args=[QWebEngineSettings.MinimumLogicalFontSize]),
|
||||
'fonts.web.size.default':
|
||||
Setter(QWebEngineSettings.setFontSize,
|
||||
args=[QWebEngineSettings.DefaultFontSize]),
|
||||
'fonts.web.size.default_fixed':
|
||||
Setter(QWebEngineSettings.setFontSize,
|
||||
args=[QWebEngineSettings.DefaultFixedFontSize]),
|
||||
|
||||
'scrolling.smooth':
|
||||
Attribute(QWebEngineSettings.ScrollAnimatorEnabled),
|
||||
}
|
||||
|
||||
try:
|
||||
MAPPINGS['content.print_element_backgrounds'] = Attribute(
|
||||
QWebEngineSettings.PrintElementBackgrounds)
|
||||
except AttributeError:
|
||||
# Added in Qt 5.8
|
||||
pass
|
||||
|
||||
|
||||
if qtutils.version_check('5.8'):
|
||||
MAPPINGS['spellcheck.languages'] = DictionaryLanguageSetter()
|
||||
|
||||
|
||||
if qtutils.version_check('5.9', compiled=False):
|
||||
# https://bugreports.qt.io/browse/QTBUG-58650
|
||||
MAPPINGS['content.cookies.store'] = PersistentCookiePolicy()
|
||||
|
@ -22,11 +22,12 @@
|
||||
import math
|
||||
import functools
|
||||
import sys
|
||||
import re
|
||||
import html as html_utils
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
|
||||
QUrl)
|
||||
QUrl, QTimer)
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
from PyQt5.QtNetwork import QAuthenticator
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
@ -473,7 +474,8 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
||||
return self._history.itemAt(i)
|
||||
|
||||
def _go_to_item(self, item):
|
||||
return self._history.goToItem(item)
|
||||
self._tab.predicted_navigation.emit(item.url())
|
||||
self._history.goToItem(item)
|
||||
|
||||
def serialize(self):
|
||||
if not qtutils.version_check('5.9', compiled=False):
|
||||
@ -607,12 +609,15 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.printing = WebEnginePrinting()
|
||||
self.elements = WebEngineElements(tab=self)
|
||||
self.action = WebEngineAction(tab=self)
|
||||
# We're assigning settings in _set_widget
|
||||
self.settings = webenginesettings.WebEngineSettings(settings=None)
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
self.backend = usertypes.Backend.QtWebEngine
|
||||
self._init_js()
|
||||
self._child_event_filter = None
|
||||
self._saved_zoom = None
|
||||
self._reload_url = None
|
||||
|
||||
def _init_js(self):
|
||||
js_code = '\n'.join([
|
||||
@ -731,6 +736,16 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.send_event(press_evt)
|
||||
self.send_event(release_evt)
|
||||
|
||||
def _show_error_page(self, url, error):
|
||||
"""Show an error page in the tab."""
|
||||
log.misc.debug("Showing error page for {}".format(error))
|
||||
url_string = url.toDisplayString()
|
||||
error_page = jinja.render(
|
||||
'error.html',
|
||||
title="Error loading page: {}".format(url_string),
|
||||
url=url_string, error=error)
|
||||
self.set_html(error_page)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_history_trigger(self):
|
||||
try:
|
||||
@ -779,13 +794,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
sip.assign(authenticator, QAuthenticator())
|
||||
# pylint: enable=no-member, useless-suppression
|
||||
except AttributeError:
|
||||
url_string = url.toDisplayString()
|
||||
error_page = jinja.render(
|
||||
'error.html',
|
||||
title="Error loading page: {}".format(url_string),
|
||||
url=url_string, error="Proxy authentication required",
|
||||
icon='')
|
||||
self.set_html(error_page)
|
||||
self._show_error_page(url, "Proxy authentication required")
|
||||
|
||||
@pyqtSlot(QUrl, 'QAuthenticator*')
|
||||
def _on_authentication_required(self, url, authenticator):
|
||||
@ -805,12 +814,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
except AttributeError:
|
||||
# WORKAROUND for
|
||||
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
|
||||
url_string = url.toDisplayString()
|
||||
error_page = jinja.render(
|
||||
'error.html',
|
||||
title="Error loading page: {}".format(url_string),
|
||||
url=url_string, error="Authentication required")
|
||||
self.set_html(error_page)
|
||||
self._show_error_page(url, "Authentication required")
|
||||
|
||||
@pyqtSlot('QWebEngineFullScreenRequest')
|
||||
def _on_fullscreen_requested(self, request):
|
||||
@ -875,6 +879,54 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
if not ok:
|
||||
self._load_finished_fake.emit(False)
|
||||
|
||||
def _error_page_workaround(self, html):
|
||||
"""Check if we're displaying a Chromium error page.
|
||||
|
||||
This gets only called if we got loadFinished(False) without JavaScript,
|
||||
so we can display at least some error page.
|
||||
|
||||
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66643
|
||||
Needs to check the page content as a WORKAROUND for
|
||||
https://bugreports.qt.io/browse/QTBUG-66661
|
||||
"""
|
||||
match = re.search(r'"errorCode":"([^"]*)"', html)
|
||||
if match is None:
|
||||
return
|
||||
self._show_error_page(self.url(), error=match.group(1))
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _on_load_finished(self, ok):
|
||||
"""Display a static error page if JavaScript is disabled."""
|
||||
super()._on_load_finished(ok)
|
||||
js_enabled = self.settings.test_attribute('content.javascript.enabled')
|
||||
if not ok and not js_enabled:
|
||||
self.dump_async(self._error_page_workaround)
|
||||
|
||||
if ok and self._reload_url is not None:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
log.config.debug(
|
||||
"Reloading {} because of config change".format(
|
||||
self._reload_url.toDisplayString()))
|
||||
QTimer.singleShot(100, lambda url=self._reload_url:
|
||||
self.openurl(url))
|
||||
self._reload_url = None
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_predicted_navigation(self, url):
|
||||
"""If we know we're going to visit an URL soon, change the settings."""
|
||||
self.settings.update_for_url(url)
|
||||
|
||||
@pyqtSlot(usertypes.NavigationRequest)
|
||||
def _on_navigation_request(self, navigation):
|
||||
super()._on_navigation_request(navigation)
|
||||
if navigation.accepted and navigation.is_main_frame:
|
||||
changed = self.settings.update_for_url(navigation.url)
|
||||
needs_reload = {'content.plugins', 'content.javascript.enabled'}
|
||||
if (changed & needs_reload and navigation.navigation_type !=
|
||||
navigation.Type.link_clicked):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
self._reload_url = navigation.url
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
page = view.page()
|
||||
@ -889,6 +941,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._on_proxy_authentication_required)
|
||||
page.fullScreenRequested.connect(self._on_fullscreen_requested)
|
||||
page.contentsSizeChanged.connect(self.contents_size_changed)
|
||||
page.navigation_request.connect(self._on_navigation_request)
|
||||
|
||||
view.titleChanged.connect(self.title_changed)
|
||||
view.urlChanged.connect(self._on_url_changed)
|
||||
@ -909,5 +962,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
page.loadFinished.connect(self._restore_zoom)
|
||||
page.loadFinished.connect(self._on_load_finished)
|
||||
|
||||
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||
|
||||
def event_target(self):
|
||||
return self._widget.focusProxy()
|
||||
|
@ -29,8 +29,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webengine import certificateerror, webenginesettings
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
|
||||
objreg, qtutils)
|
||||
from qutebrowser.utils import log, debug, usertypes, jinja, objreg, qtutils
|
||||
|
||||
|
||||
class WebEngineView(QWebEngineView):
|
||||
@ -124,10 +123,12 @@ class WebEnginePage(QWebEnginePage):
|
||||
Signals:
|
||||
certificate_error: Emitted on certificate errors.
|
||||
shutting_down: Emitted when the page is shutting down.
|
||||
navigation_request: Emitted on acceptNavigationRequest.
|
||||
"""
|
||||
|
||||
certificate_error = pyqtSignal()
|
||||
shutting_down = pyqtSignal()
|
||||
navigation_request = pyqtSignal(usertypes.NavigationRequest)
|
||||
|
||||
def __init__(self, *, theme_color, profile, parent=None):
|
||||
super().__init__(profile, parent)
|
||||
@ -288,21 +289,26 @@ class WebEnginePage(QWebEnginePage):
|
||||
url: QUrl,
|
||||
typ: QWebEnginePage.NavigationType,
|
||||
is_main_frame: bool):
|
||||
"""Override acceptNavigationRequest to handle clicked links.
|
||||
|
||||
This only show an error on invalid links - everything else is handled
|
||||
in createWindow.
|
||||
"""
|
||||
log.webview.debug("navigation request: url {}, type {}, is_main_frame "
|
||||
"{}".format(url.toDisplayString(),
|
||||
debug.qenum_key(QWebEnginePage, typ),
|
||||
is_main_frame))
|
||||
if (typ == QWebEnginePage.NavigationTypeLinkClicked and
|
||||
not url.isValid()):
|
||||
msg = urlutils.get_errstring(url, "Invalid link clicked")
|
||||
message.error(msg)
|
||||
return False
|
||||
return True
|
||||
"""Override acceptNavigationRequest to forward it to the tab API."""
|
||||
type_map = {
|
||||
QWebEnginePage.NavigationTypeLinkClicked:
|
||||
usertypes.NavigationRequest.Type.link_clicked,
|
||||
QWebEnginePage.NavigationTypeTyped:
|
||||
usertypes.NavigationRequest.Type.typed,
|
||||
QWebEnginePage.NavigationTypeFormSubmitted:
|
||||
usertypes.NavigationRequest.Type.form_submitted,
|
||||
QWebEnginePage.NavigationTypeBackForward:
|
||||
usertypes.NavigationRequest.Type.back_forward,
|
||||
QWebEnginePage.NavigationTypeReload:
|
||||
usertypes.NavigationRequest.Type.reloaded,
|
||||
QWebEnginePage.NavigationTypeOther:
|
||||
usertypes.NavigationRequest.Type.other,
|
||||
}
|
||||
navigation = usertypes.NavigationRequest(url=url,
|
||||
navigation_type=type_map[typ],
|
||||
is_main_frame=is_main_frame)
|
||||
self.navigation_request.emit(navigation)
|
||||
return navigation.accepted
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
def _inject_userjs(self, url):
|
||||
|
@ -17,9 +17,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# We get various "abstract but not overridden" warnings
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
"""Bridge from QWebSettings to our own settings.
|
||||
|
||||
Module attributes:
|
||||
@ -37,85 +34,130 @@ from qutebrowser.utils import standarddir, urlutils
|
||||
from qutebrowser.browser import shared
|
||||
|
||||
|
||||
class Base(websettings.Base):
|
||||
|
||||
"""Base settings class with appropriate _get_global_settings."""
|
||||
|
||||
def _get_global_settings(self):
|
||||
return [QWebSettings.globalSettings()]
|
||||
# The global WebKitSettings object
|
||||
global_settings = None
|
||||
|
||||
|
||||
class Attribute(Base, websettings.Attribute):
|
||||
class WebKitSettings(websettings.AbstractSettings):
|
||||
|
||||
"""A setting set via QWebSettings::setAttribute."""
|
||||
"""A wrapper for the config for QWebSettings."""
|
||||
|
||||
ENUM_BASE = QWebSettings
|
||||
_ATTRIBUTES = {
|
||||
'content.images':
|
||||
[QWebSettings.AutoLoadImages],
|
||||
'content.javascript.enabled':
|
||||
[QWebSettings.JavascriptEnabled],
|
||||
'content.javascript.can_open_tabs_automatically':
|
||||
[QWebSettings.JavascriptCanOpenWindows],
|
||||
'content.javascript.can_close_tabs':
|
||||
[QWebSettings.JavascriptCanCloseWindows],
|
||||
'content.javascript.can_access_clipboard':
|
||||
[QWebSettings.JavascriptCanAccessClipboard],
|
||||
'content.plugins':
|
||||
[QWebSettings.PluginsEnabled],
|
||||
'content.webgl':
|
||||
[QWebSettings.WebGLEnabled],
|
||||
'content.hyperlink_auditing':
|
||||
[QWebSettings.HyperlinkAuditingEnabled],
|
||||
'content.local_content_can_access_remote_urls':
|
||||
[QWebSettings.LocalContentCanAccessRemoteUrls],
|
||||
'content.local_content_can_access_file_urls':
|
||||
[QWebSettings.LocalContentCanAccessFileUrls],
|
||||
'content.dns_prefetch':
|
||||
[QWebSettings.DnsPrefetchEnabled],
|
||||
'content.frame_flattening':
|
||||
[QWebSettings.FrameFlatteningEnabled],
|
||||
'content.cache.appcache':
|
||||
[QWebSettings.OfflineWebApplicationCacheEnabled],
|
||||
'content.local_storage':
|
||||
[QWebSettings.LocalStorageEnabled,
|
||||
QWebSettings.OfflineStorageDatabaseEnabled],
|
||||
'content.developer_extras':
|
||||
[QWebSettings.DeveloperExtrasEnabled],
|
||||
'content.print_element_backgrounds':
|
||||
[QWebSettings.PrintElementBackgrounds],
|
||||
'content.xss_auditing':
|
||||
[QWebSettings.XSSAuditingEnabled],
|
||||
|
||||
'input.spatial_navigation':
|
||||
[QWebSettings.SpatialNavigationEnabled],
|
||||
'input.links_included_in_focus_chain':
|
||||
[QWebSettings.LinksIncludedInFocusChain],
|
||||
|
||||
'zoom.text_only':
|
||||
[QWebSettings.ZoomTextOnly],
|
||||
'scrolling.smooth':
|
||||
[QWebSettings.ScrollAnimatorEnabled],
|
||||
}
|
||||
|
||||
_FONT_SIZES = {
|
||||
'fonts.web.size.minimum':
|
||||
QWebSettings.MinimumFontSize,
|
||||
'fonts.web.size.minimum_logical':
|
||||
QWebSettings.MinimumLogicalFontSize,
|
||||
'fonts.web.size.default':
|
||||
QWebSettings.DefaultFontSize,
|
||||
'fonts.web.size.default_fixed':
|
||||
QWebSettings.DefaultFixedFontSize,
|
||||
}
|
||||
|
||||
_FONT_FAMILIES = {
|
||||
'fonts.web.family.standard': QWebSettings.StandardFont,
|
||||
'fonts.web.family.fixed': QWebSettings.FixedFont,
|
||||
'fonts.web.family.serif': QWebSettings.SerifFont,
|
||||
'fonts.web.family.sans_serif': QWebSettings.SansSerifFont,
|
||||
'fonts.web.family.cursive': QWebSettings.CursiveFont,
|
||||
'fonts.web.family.fantasy': QWebSettings.FantasyFont,
|
||||
}
|
||||
|
||||
# Mapping from QWebSettings::QWebSettings() in
|
||||
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
|
||||
_FONT_TO_QFONT = {
|
||||
QWebSettings.StandardFont: QFont.Serif,
|
||||
QWebSettings.FixedFont: QFont.Monospace,
|
||||
QWebSettings.SerifFont: QFont.Serif,
|
||||
QWebSettings.SansSerifFont: QFont.SansSerif,
|
||||
QWebSettings.CursiveFont: QFont.Cursive,
|
||||
QWebSettings.FantasyFont: QFont.Fantasy,
|
||||
}
|
||||
|
||||
|
||||
class Setter(Base, websettings.Setter):
|
||||
|
||||
"""A setting set via a QWebSettings setter method."""
|
||||
|
||||
pass
|
||||
def _set_user_stylesheet(settings):
|
||||
"""Set the generated user-stylesheet."""
|
||||
stylesheet = shared.get_user_stylesheet().encode('utf-8')
|
||||
url = urlutils.data_url('text/css;charset=utf-8', stylesheet)
|
||||
settings.setUserStyleSheetUrl(url)
|
||||
|
||||
|
||||
class StaticSetter(Base, websettings.StaticSetter):
|
||||
|
||||
"""A setting set via a static QWebSettings setter method."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FontFamilySetter(Base, websettings.FontFamilySetter):
|
||||
|
||||
"""A setter for a font family.
|
||||
|
||||
Gets the default value from QFont.
|
||||
"""
|
||||
|
||||
def __init__(self, font):
|
||||
# Mapping from QWebSettings::QWebSettings() in
|
||||
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
|
||||
font_to_qfont = {
|
||||
QWebSettings.StandardFont: QFont.Serif,
|
||||
QWebSettings.FixedFont: QFont.Monospace,
|
||||
QWebSettings.SerifFont: QFont.Serif,
|
||||
QWebSettings.SansSerifFont: QFont.SansSerif,
|
||||
QWebSettings.CursiveFont: QFont.Cursive,
|
||||
QWebSettings.FantasyFont: QFont.Fantasy,
|
||||
}
|
||||
super().__init__(setter=QWebSettings.setFontFamily, font=font,
|
||||
qfont=font_to_qfont[font])
|
||||
|
||||
|
||||
class CookiePolicy(Base):
|
||||
|
||||
"""The ThirdPartyCookiePolicy setting is different from other settings."""
|
||||
|
||||
MAPPING = {
|
||||
def _set_cookie_accept_policy(settings):
|
||||
"""Update the content.cookies.accept setting."""
|
||||
mapping = {
|
||||
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
|
||||
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
|
||||
}
|
||||
|
||||
def _set(self, value, settings=None):
|
||||
for obj in self._get_settings(settings):
|
||||
obj.setThirdPartyCookiePolicy(self.MAPPING[value])
|
||||
value = config.val.content.cookies.accept
|
||||
settings.setThirdPartyCookiePolicy(mapping[value])
|
||||
|
||||
|
||||
def _set_user_stylesheet():
|
||||
"""Set the generated user-stylesheet."""
|
||||
stylesheet = shared.get_user_stylesheet().encode('utf-8')
|
||||
url = urlutils.data_url('text/css;charset=utf-8', stylesheet)
|
||||
QWebSettings.globalSettings().setUserStyleSheetUrl(url)
|
||||
def _set_cache_maximum_pages(settings):
|
||||
"""Update the content.cache.maximum_pages setting."""
|
||||
value = config.val.content.cache.maximum_pages
|
||||
settings.setMaximumPagesInCache(value)
|
||||
|
||||
|
||||
def _update_settings(option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
global_settings.update_setting(option)
|
||||
|
||||
settings = QWebSettings.globalSettings()
|
||||
if option in ['scrollbar.hide', 'content.user_stylesheets']:
|
||||
_set_user_stylesheet()
|
||||
websettings.update_mappings(MAPPINGS, option)
|
||||
_set_user_stylesheet(settings)
|
||||
elif option == 'content.cookies.accept':
|
||||
_set_cookie_accept_policy(settings)
|
||||
elif option == 'content.cache.maximum_pages':
|
||||
_set_cache_maximum_pages(settings)
|
||||
|
||||
|
||||
def init(_args):
|
||||
@ -131,92 +173,20 @@ def init(_args):
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(data_path, 'offline-storage'))
|
||||
|
||||
websettings.init_mappings(MAPPINGS)
|
||||
_set_user_stylesheet()
|
||||
settings = QWebSettings.globalSettings()
|
||||
_set_user_stylesheet(settings)
|
||||
_set_cookie_accept_policy(settings)
|
||||
_set_cache_maximum_pages(settings)
|
||||
|
||||
config.instance.changed.connect(_update_settings)
|
||||
|
||||
global global_settings
|
||||
global_settings = WebKitSettings(QWebSettings.globalSettings())
|
||||
global_settings.init_settings()
|
||||
|
||||
|
||||
def shutdown():
|
||||
"""Disable storage so removing tmpdir will work."""
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
QWebSettings.setOfflineWebApplicationCachePath('')
|
||||
QWebSettings.globalSettings().setLocalStoragePath('')
|
||||
|
||||
|
||||
MAPPINGS = {
|
||||
'content.images':
|
||||
Attribute(QWebSettings.AutoLoadImages),
|
||||
'content.javascript.enabled':
|
||||
Attribute(QWebSettings.JavascriptEnabled),
|
||||
'content.javascript.can_open_tabs_automatically':
|
||||
Attribute(QWebSettings.JavascriptCanOpenWindows),
|
||||
'content.javascript.can_close_tabs':
|
||||
Attribute(QWebSettings.JavascriptCanCloseWindows),
|
||||
'content.javascript.can_access_clipboard':
|
||||
Attribute(QWebSettings.JavascriptCanAccessClipboard),
|
||||
'content.plugins':
|
||||
Attribute(QWebSettings.PluginsEnabled),
|
||||
'content.webgl':
|
||||
Attribute(QWebSettings.WebGLEnabled),
|
||||
'content.hyperlink_auditing':
|
||||
Attribute(QWebSettings.HyperlinkAuditingEnabled),
|
||||
'content.local_content_can_access_remote_urls':
|
||||
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
'content.local_content_can_access_file_urls':
|
||||
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
|
||||
'content.cookies.accept':
|
||||
CookiePolicy(),
|
||||
'content.dns_prefetch':
|
||||
Attribute(QWebSettings.DnsPrefetchEnabled),
|
||||
'content.frame_flattening':
|
||||
Attribute(QWebSettings.FrameFlatteningEnabled),
|
||||
'content.cache.appcache':
|
||||
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
|
||||
'content.local_storage':
|
||||
Attribute(QWebSettings.LocalStorageEnabled,
|
||||
QWebSettings.OfflineStorageDatabaseEnabled),
|
||||
'content.cache.maximum_pages':
|
||||
StaticSetter(QWebSettings.setMaximumPagesInCache),
|
||||
'content.developer_extras':
|
||||
Attribute(QWebSettings.DeveloperExtrasEnabled),
|
||||
'content.print_element_backgrounds':
|
||||
Attribute(QWebSettings.PrintElementBackgrounds),
|
||||
'content.xss_auditing':
|
||||
Attribute(QWebSettings.XSSAuditingEnabled),
|
||||
'content.default_encoding':
|
||||
Setter(QWebSettings.setDefaultTextEncoding),
|
||||
# content.user_stylesheets is handled separately
|
||||
|
||||
'input.spatial_navigation':
|
||||
Attribute(QWebSettings.SpatialNavigationEnabled),
|
||||
'input.links_included_in_focus_chain':
|
||||
Attribute(QWebSettings.LinksIncludedInFocusChain),
|
||||
|
||||
'fonts.web.family.standard':
|
||||
FontFamilySetter(QWebSettings.StandardFont),
|
||||
'fonts.web.family.fixed':
|
||||
FontFamilySetter(QWebSettings.FixedFont),
|
||||
'fonts.web.family.serif':
|
||||
FontFamilySetter(QWebSettings.SerifFont),
|
||||
'fonts.web.family.sans_serif':
|
||||
FontFamilySetter(QWebSettings.SansSerifFont),
|
||||
'fonts.web.family.cursive':
|
||||
FontFamilySetter(QWebSettings.CursiveFont),
|
||||
'fonts.web.family.fantasy':
|
||||
FontFamilySetter(QWebSettings.FantasyFont),
|
||||
'fonts.web.size.minimum':
|
||||
Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumFontSize]),
|
||||
'fonts.web.size.minimum_logical':
|
||||
Setter(QWebSettings.setFontSize,
|
||||
args=[QWebSettings.MinimumLogicalFontSize]),
|
||||
'fonts.web.size.default':
|
||||
Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFontSize]),
|
||||
'fonts.web.size.default_fixed':
|
||||
Setter(QWebSettings.setFontSize,
|
||||
args=[QWebSettings.DefaultFixedFontSize]),
|
||||
|
||||
'zoom.text_only':
|
||||
Attribute(QWebSettings.ZoomTextOnly),
|
||||
'scrolling.smooth':
|
||||
Attribute(QWebSettings.ScrollAnimatorEnabled),
|
||||
}
|
||||
|
@ -35,8 +35,9 @@ from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtPrintSupport import QPrinter
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
|
||||
from qutebrowser.browser import browsertab, shared
|
||||
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
|
||||
webkitsettings)
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
|
||||
|
||||
|
||||
@ -517,7 +518,8 @@ class WebKitHistory(browsertab.AbstractHistory):
|
||||
return self._history.itemAt(i)
|
||||
|
||||
def _go_to_item(self, item):
|
||||
return self._history.goToItem(item)
|
||||
self._tab.predicted_navigation.emit(item.url())
|
||||
self._history.goToItem(item)
|
||||
|
||||
def serialize(self):
|
||||
return qtutils.serialize(self._history)
|
||||
@ -644,6 +646,8 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
self.printing = WebKitPrinting()
|
||||
self.elements = WebKitElements(tab=self)
|
||||
self.action = WebKitAction(tab=self)
|
||||
# We're assigning settings in _set_widget
|
||||
self.settings = webkitsettings.WebKitSettings(settings=None)
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
self.backend = usertypes.Backend.QtWebKit
|
||||
@ -761,6 +765,31 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
def _on_contents_size_changed(self, size):
|
||||
self.contents_size_changed.emit(QSizeF(size))
|
||||
|
||||
@pyqtSlot(usertypes.NavigationRequest)
|
||||
def _on_navigation_request(self, navigation):
|
||||
super()._on_navigation_request(navigation)
|
||||
if not navigation.accepted:
|
||||
return
|
||||
|
||||
log.webview.debug("target {} override {}".format(
|
||||
self.data.open_target, self.data.override_target))
|
||||
|
||||
if self.data.override_target is not None:
|
||||
target = self.data.override_target
|
||||
self.data.override_target = None
|
||||
else:
|
||||
target = self.data.open_target
|
||||
|
||||
if (navigation.navigation_type == navigation.Type.link_clicked and
|
||||
target != usertypes.ClickTarget.normal):
|
||||
tab = shared.get_tab(self.win_id, target)
|
||||
tab.openurl(navigation.url)
|
||||
self.data.open_target = usertypes.ClickTarget.normal
|
||||
navigation.accepted = False
|
||||
|
||||
if navigation.is_main_frame:
|
||||
self.settings.update_for_url(navigation.url)
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
page = view.page()
|
||||
@ -779,6 +808,7 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
page.frameCreated.connect(self._on_frame_created)
|
||||
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
|
||||
frame.initialLayoutCompleted.connect(self._on_history_trigger)
|
||||
page.navigation_request.connect(self._on_navigation_request)
|
||||
|
||||
def event_target(self):
|
||||
return self._widget
|
||||
|
@ -33,8 +33,7 @@ from qutebrowser.config import config
|
||||
from qutebrowser.browser import pdfjs, shared
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.utils import (message, usertypes, log, jinja, objreg, debug,
|
||||
urlutils)
|
||||
from qutebrowser.utils import message, usertypes, log, jinja, objreg
|
||||
|
||||
|
||||
class BrowserPage(QWebPage):
|
||||
@ -54,10 +53,12 @@ class BrowserPage(QWebPage):
|
||||
shutting_down: Emitted when the page is currently shutting down.
|
||||
reloading: Emitted before a web page reloads.
|
||||
arg: The URL which gets reloaded.
|
||||
navigation_request: Emitted on acceptNavigationRequest.
|
||||
"""
|
||||
|
||||
shutting_down = pyqtSignal()
|
||||
reloading = pyqtSignal(QUrl)
|
||||
navigation_request = pyqtSignal(usertypes.NavigationRequest)
|
||||
|
||||
def __init__(self, win_id, tab_id, tabdata, private, parent=None):
|
||||
super().__init__(parent)
|
||||
@ -70,7 +71,6 @@ class BrowserPage(QWebPage):
|
||||
}
|
||||
self._ignore_load_started = False
|
||||
self.error_occurred = False
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self._networkmanager = networkmanager.NetworkManager(
|
||||
win_id=win_id, tab_id=tab_id, private=private, parent=self)
|
||||
self.setNetworkAccessManager(self._networkmanager)
|
||||
@ -474,7 +474,7 @@ class BrowserPage(QWebPage):
|
||||
source, line, msg)
|
||||
|
||||
def acceptNavigationRequest(self,
|
||||
_frame: QWebFrame,
|
||||
frame: QWebFrame,
|
||||
request: QNetworkRequest,
|
||||
typ: QWebPage.NavigationType):
|
||||
"""Override acceptNavigationRequest to handle clicked links.
|
||||
@ -486,36 +486,27 @@ class BrowserPage(QWebPage):
|
||||
Checks if it should open it in a tab (middle-click or control) or not,
|
||||
and then conditionally opens the URL here or in another tab/window.
|
||||
"""
|
||||
url = request.url()
|
||||
log.webview.debug("navigation request: url {}, type {}, "
|
||||
"target {} override {}".format(
|
||||
url.toDisplayString(),
|
||||
debug.qenum_key(QWebPage, typ),
|
||||
self.open_target,
|
||||
self._tabdata.override_target))
|
||||
type_map = {
|
||||
QWebPage.NavigationTypeLinkClicked:
|
||||
usertypes.NavigationRequest.Type.link_clicked,
|
||||
QWebPage.NavigationTypeFormSubmitted:
|
||||
usertypes.NavigationRequest.Type.form_submitted,
|
||||
QWebPage.NavigationTypeFormResubmitted:
|
||||
usertypes.NavigationRequest.Type.form_resubmitted,
|
||||
QWebPage.NavigationTypeBackOrForward:
|
||||
usertypes.NavigationRequest.Type.back_forward,
|
||||
QWebPage.NavigationTypeReload:
|
||||
usertypes.NavigationRequest.Type.reloaded,
|
||||
QWebPage.NavigationTypeOther:
|
||||
usertypes.NavigationRequest.Type.other,
|
||||
}
|
||||
is_main_frame = frame is self.mainFrame()
|
||||
navigation = usertypes.NavigationRequest(url=request.url(),
|
||||
navigation_type=type_map[typ],
|
||||
is_main_frame=is_main_frame)
|
||||
|
||||
if self._tabdata.override_target is not None:
|
||||
target = self._tabdata.override_target
|
||||
self._tabdata.override_target = None
|
||||
else:
|
||||
target = self.open_target
|
||||
if navigation.navigation_type == navigation.Type.reloaded:
|
||||
self.reloading.emit(navigation.url)
|
||||
|
||||
if typ == QWebPage.NavigationTypeReload:
|
||||
self.reloading.emit(url)
|
||||
return True
|
||||
elif typ != QWebPage.NavigationTypeLinkClicked:
|
||||
return True
|
||||
|
||||
if not url.isValid():
|
||||
msg = urlutils.get_errstring(url, "Invalid link clicked")
|
||||
message.error(msg)
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
return False
|
||||
|
||||
if target == usertypes.ClickTarget.normal:
|
||||
return True
|
||||
|
||||
tab = shared.get_tab(self._win_id, target)
|
||||
tab.openurl(url)
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
return False
|
||||
self.navigation_request.emit(navigation)
|
||||
return navigation.accepted
|
||||
|
@ -262,10 +262,10 @@ class WebView(QWebView):
|
||||
target = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
target = usertypes.ClickTarget.tab
|
||||
self.page().open_target = target
|
||||
self._tabdata.open_target = target
|
||||
log.mouse.debug("Ctrl/Middle click, setting target: {}".format(
|
||||
target))
|
||||
else:
|
||||
self.page().open_target = usertypes.ClickTarget.normal
|
||||
self._tabdata.open_target = usertypes.ClickTarget.normal
|
||||
log.mouse.debug("Normal click, setting normal target")
|
||||
super().mousePressEvent(e)
|
||||
|
@ -63,6 +63,7 @@ def replace_variables(win_id, arglist):
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword),
|
||||
'url:pretty': lambda: _current_url(tabbed_browser).toString(
|
||||
QUrl.DecodeReserved | QUrl.RemovePassword),
|
||||
'url:host': lambda: _current_url(tabbed_browser).host(),
|
||||
'clipboard': utils.get_clipboard,
|
||||
'primary': lambda: utils.get_clipboard(selection=True),
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from qutebrowser.config import configdata, configexc
|
||||
from qutebrowser.config import configdata, configexc, configutils
|
||||
from qutebrowser.utils import utils, log, jinja
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
@ -37,6 +37,9 @@ key_instance = None
|
||||
# Keeping track of all change filters to validate them later.
|
||||
change_filters = []
|
||||
|
||||
# Sentinel
|
||||
UNSET = object()
|
||||
|
||||
|
||||
class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
@ -186,7 +189,7 @@ class KeyConfig:
|
||||
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
|
||||
key, command, mode))
|
||||
|
||||
bindings = self._config.get_obj('bindings.commands')
|
||||
bindings = self._config.get_mutable_obj('bindings.commands')
|
||||
if mode not in bindings:
|
||||
bindings[mode] = {}
|
||||
bindings[mode][key] = command
|
||||
@ -196,7 +199,7 @@ class KeyConfig:
|
||||
"""Restore a default keybinding."""
|
||||
key = self._prepare(key, mode)
|
||||
|
||||
bindings_commands = self._config.get_obj('bindings.commands')
|
||||
bindings_commands = self._config.get_mutable_obj('bindings.commands')
|
||||
try:
|
||||
del bindings_commands[mode][key]
|
||||
except KeyError:
|
||||
@ -208,7 +211,7 @@ class KeyConfig:
|
||||
"""Unbind the given key in the given mode."""
|
||||
key = self._prepare(key, mode)
|
||||
|
||||
bindings_commands = self._config.get_obj('bindings.commands')
|
||||
bindings_commands = self._config.get_mutable_obj('bindings.commands')
|
||||
|
||||
if val.bindings.commands[mode].get(key, None) is not None:
|
||||
# In custom bindings -> remove it
|
||||
@ -229,8 +232,12 @@ class Config(QObject):
|
||||
|
||||
"""Main config object.
|
||||
|
||||
Class attributes:
|
||||
MUTABLE_TYPES: Types returned from the config which could potentially
|
||||
be mutated.
|
||||
|
||||
Attributes:
|
||||
_values: A dict mapping setting names to their values.
|
||||
_values: A dict mapping setting names to configutils.Values objects.
|
||||
_mutables: A dictionary of mutable objects to be checked for changes.
|
||||
_yaml: A YamlConfig object or None.
|
||||
|
||||
@ -238,19 +245,25 @@ class Config(QObject):
|
||||
changed: Emitted with the option name when an option changed.
|
||||
"""
|
||||
|
||||
MUTABLE_TYPES = (dict, list)
|
||||
changed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, yaml_config, parent=None):
|
||||
super().__init__(parent)
|
||||
self.changed.connect(_render_stylesheet.cache_clear)
|
||||
self._values = {}
|
||||
self._mutables = {}
|
||||
self._yaml = yaml_config
|
||||
self._init_values()
|
||||
|
||||
def _init_values(self):
|
||||
"""Populate the self._values dict."""
|
||||
self._values = {}
|
||||
for name, opt in configdata.DATA.items():
|
||||
self._values[name] = configutils.Values(opt)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over Option, value tuples."""
|
||||
for name, value in sorted(self._values.items()):
|
||||
yield (self.get_opt(name), value)
|
||||
"""Iterate over configutils.Values items."""
|
||||
yield from self._values.values()
|
||||
|
||||
def init_save_manager(self, save_manager):
|
||||
"""Make sure the config gets saved properly.
|
||||
@ -260,14 +273,15 @@ class Config(QObject):
|
||||
"""
|
||||
self._yaml.init_save_manager(save_manager)
|
||||
|
||||
def _set_value(self, opt, value):
|
||||
def _set_value(self, opt, value, pattern=None):
|
||||
"""Set the given option to the given value."""
|
||||
if not isinstance(objects.backend, objects.NoBackend):
|
||||
if objects.backend not in opt.backends:
|
||||
raise configexc.BackendError(opt.name, objects.backend)
|
||||
|
||||
opt.typ.to_py(value) # for validation
|
||||
self._values[opt.name] = opt.typ.from_obj(value)
|
||||
|
||||
self._values[opt.name].add(opt.typ.from_obj(value), pattern)
|
||||
|
||||
self.changed.emit(opt.name)
|
||||
log.config.debug("Config option changed: {} = {}".format(
|
||||
@ -276,8 +290,10 @@ class Config(QObject):
|
||||
def read_yaml(self):
|
||||
"""Read the YAML settings from self._yaml."""
|
||||
self._yaml.load()
|
||||
for name, value in self._yaml:
|
||||
self._set_value(self.get_opt(name), value)
|
||||
for values in self._yaml:
|
||||
for scoped in values:
|
||||
self._set_value(values.opt, scoped.value,
|
||||
pattern=scoped.pattern)
|
||||
|
||||
def get_opt(self, name):
|
||||
"""Get a configdata.Option object for the given setting."""
|
||||
@ -290,53 +306,89 @@ class Config(QObject):
|
||||
name, deleted=deleted, renamed=renamed)
|
||||
raise exception from None
|
||||
|
||||
def get(self, name):
|
||||
def get(self, name, url=None):
|
||||
"""Get the given setting converted for Python code."""
|
||||
opt = self.get_opt(name)
|
||||
obj = self.get_obj(name, mutable=False)
|
||||
obj = self.get_obj(name, url=url)
|
||||
return opt.typ.to_py(obj)
|
||||
|
||||
def get_obj(self, name, *, mutable=True):
|
||||
def _maybe_copy(self, value):
|
||||
"""Copy the value if it could potentially be mutated."""
|
||||
if isinstance(value, self.MUTABLE_TYPES):
|
||||
# For mutable objects, create a copy so we don't accidentally
|
||||
# mutate the config's internal value.
|
||||
return copy.deepcopy(value)
|
||||
else:
|
||||
# Shouldn't be mutable (and thus hashable)
|
||||
assert value.__hash__ is not None, value
|
||||
return value
|
||||
|
||||
def get_obj(self, name, *, url=None):
|
||||
"""Get the given setting as object (for YAML/config.py).
|
||||
|
||||
If mutable=True is set, watch the returned object for mutations.
|
||||
Note that the returned values are not watched for mutation.
|
||||
If a URL is given, return the value which should be used for that URL.
|
||||
"""
|
||||
opt = self.get_opt(name)
|
||||
obj = None
|
||||
self.get_opt(name) # To make sure it exists
|
||||
value = self._values[name].get_for_url(url)
|
||||
return self._maybe_copy(value)
|
||||
|
||||
def get_obj_for_pattern(self, name, *, pattern):
|
||||
"""Get the given setting as object (for YAML/config.py).
|
||||
|
||||
This gets the overridden value for a given pattern, or
|
||||
configutils.UNSET if no such override exists.
|
||||
"""
|
||||
self.get_opt(name) # To make sure it exists
|
||||
value = self._values[name].get_for_pattern(pattern, fallback=False)
|
||||
return self._maybe_copy(value)
|
||||
|
||||
def get_mutable_obj(self, name, *, pattern=None):
|
||||
"""Get an object which can be mutated, e.g. in a config.py.
|
||||
|
||||
If a pattern is given, return the value for that pattern.
|
||||
Note that it's impossible to get a mutable object for an URL as we
|
||||
wouldn't know what pattern to apply.
|
||||
"""
|
||||
self.get_opt(name) # To make sure it exists
|
||||
|
||||
# If we allow mutation, there is a chance that prior mutations already
|
||||
# entered the mutable dictionary and thus further copies are unneeded
|
||||
# until update_mutables() is called
|
||||
if name in self._mutables and mutable:
|
||||
if name in self._mutables:
|
||||
_copy, obj = self._mutables[name]
|
||||
# Otherwise, we return a copy of the value stored internally, so the
|
||||
# internal value can never be changed by mutating the object returned.
|
||||
else:
|
||||
obj = copy.deepcopy(self._values.get(name, opt.default))
|
||||
# Then we watch the returned object for changes.
|
||||
if isinstance(obj, (dict, list)):
|
||||
if mutable:
|
||||
self._mutables[name] = (copy.deepcopy(obj), obj)
|
||||
else:
|
||||
# Shouldn't be mutable (and thus hashable)
|
||||
assert obj.__hash__ is not None, obj
|
||||
return obj
|
||||
return obj
|
||||
|
||||
def get_str(self, name):
|
||||
"""Get the given setting as string."""
|
||||
value = self._values[name].get_for_pattern(pattern)
|
||||
copy_value = self._maybe_copy(value)
|
||||
|
||||
# Watch the returned object for changes if it's mutable.
|
||||
if isinstance(copy_value, self.MUTABLE_TYPES):
|
||||
self._mutables[name] = (value, copy_value) # old, new
|
||||
|
||||
return copy_value
|
||||
|
||||
def get_str(self, name, *, pattern=None):
|
||||
"""Get the given setting as string.
|
||||
|
||||
If a pattern is given, get the setting for the given pattern or
|
||||
configutils.UNSET.
|
||||
"""
|
||||
opt = self.get_opt(name)
|
||||
value = self._values.get(name, opt.default)
|
||||
values = self._values[name]
|
||||
value = values.get_for_pattern(pattern)
|
||||
return opt.typ.to_str(value)
|
||||
|
||||
def set_obj(self, name, value, *, save_yaml=False):
|
||||
def set_obj(self, name, value, *, pattern=None, save_yaml=False):
|
||||
"""Set the given setting from a YAML/config.py object.
|
||||
|
||||
If save_yaml=True is given, store the new value to YAML.
|
||||
"""
|
||||
self._set_value(self.get_opt(name), value)
|
||||
self._set_value(self.get_opt(name), value, pattern=pattern)
|
||||
if save_yaml:
|
||||
self._yaml[name] = value
|
||||
self._yaml.set_obj(name, value, pattern=pattern)
|
||||
|
||||
def set_str(self, name, value, *, save_yaml=False):
|
||||
def set_str(self, name, value, *, pattern=None, save_yaml=False):
|
||||
"""Set the given setting from a string.
|
||||
|
||||
If save_yaml=True is given, store the new value to YAML.
|
||||
@ -346,21 +398,19 @@ class Config(QObject):
|
||||
log.config.debug("Setting {} (type {}) to {!r} (converted from {!r})"
|
||||
.format(name, opt.typ.__class__.__name__, converted,
|
||||
value))
|
||||
self._set_value(opt, converted)
|
||||
self._set_value(opt, converted, pattern=pattern)
|
||||
if save_yaml:
|
||||
self._yaml[name] = converted
|
||||
self._yaml.set_obj(name, converted, pattern=pattern)
|
||||
|
||||
def unset(self, name, *, save_yaml=False):
|
||||
def unset(self, name, *, save_yaml=False, pattern=None):
|
||||
"""Set the given setting back to its default."""
|
||||
self.get_opt(name)
|
||||
try:
|
||||
del self._values[name]
|
||||
except KeyError:
|
||||
return
|
||||
self.changed.emit(name)
|
||||
self.get_opt(name) # To check whether it exists
|
||||
changed = self._values[name].remove(pattern)
|
||||
if changed:
|
||||
self.changed.emit(name)
|
||||
|
||||
if save_yaml:
|
||||
self._yaml.unset(name)
|
||||
self._yaml.unset(name, pattern=pattern)
|
||||
|
||||
def clear(self, *, save_yaml=False):
|
||||
"""Clear all settings in the config.
|
||||
@ -368,10 +418,10 @@ class Config(QObject):
|
||||
If save_yaml=True is given, also remove all customization from the YAML
|
||||
file.
|
||||
"""
|
||||
old_values = self._values
|
||||
self._values = {}
|
||||
for name in old_values:
|
||||
self.changed.emit(name)
|
||||
for name, values in self._values.items():
|
||||
if values:
|
||||
values.clear()
|
||||
self.changed.emit(name)
|
||||
|
||||
if save_yaml:
|
||||
self._yaml.clear()
|
||||
@ -397,13 +447,15 @@ class Config(QObject):
|
||||
Return:
|
||||
The changed config part as string.
|
||||
"""
|
||||
lines = []
|
||||
for opt, value in self:
|
||||
str_value = opt.typ.to_str(value)
|
||||
lines.append('{} = {}'.format(opt.name, str_value))
|
||||
if not lines:
|
||||
lines = ['<Default configuration>']
|
||||
return '\n'.join(lines)
|
||||
blocks = []
|
||||
for values in sorted(self, key=lambda v: v.opt.name):
|
||||
if values:
|
||||
blocks.append(str(values))
|
||||
|
||||
if not blocks:
|
||||
return '<Default configuration>'
|
||||
|
||||
return '\n'.join(blocks)
|
||||
|
||||
|
||||
class ConfigContainer:
|
||||
@ -415,16 +467,21 @@ class ConfigContainer:
|
||||
_prefix: The __getattr__ chain leading up to this object.
|
||||
_configapi: If given, get values suitable for config.py and
|
||||
add errors to the given ConfigAPI object.
|
||||
_pattern: The URL pattern to be used.
|
||||
"""
|
||||
|
||||
def __init__(self, config, configapi=None, prefix=''):
|
||||
def __init__(self, config, configapi=None, prefix='', pattern=None):
|
||||
self._config = config
|
||||
self._prefix = prefix
|
||||
self._configapi = configapi
|
||||
self._pattern = pattern
|
||||
if configapi is None and pattern is not None:
|
||||
raise TypeError("Can't use pattern without configapi!")
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True, config=self._config,
|
||||
configapi=self._configapi, prefix=self._prefix)
|
||||
configapi=self._configapi, prefix=self._prefix,
|
||||
pattern=self._pattern)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_error(self, action, name):
|
||||
@ -452,7 +509,7 @@ class ConfigContainer:
|
||||
if configdata.is_valid_prefix(name):
|
||||
return ConfigContainer(config=self._config,
|
||||
configapi=self._configapi,
|
||||
prefix=name)
|
||||
prefix=name, pattern=self._pattern)
|
||||
|
||||
with self._handle_error('getting', name):
|
||||
if self._configapi is None:
|
||||
@ -460,7 +517,8 @@ class ConfigContainer:
|
||||
return self._config.get(name)
|
||||
else:
|
||||
# access from config.py
|
||||
return self._config.get_obj(name)
|
||||
return self._config.get_mutable_obj(
|
||||
name, pattern=self._pattern)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
"""Set the given option in the config."""
|
||||
@ -470,7 +528,7 @@ class ConfigContainer:
|
||||
|
||||
name = self._join(attr)
|
||||
with self._handle_error('setting', name):
|
||||
self._config.set_obj(name, value)
|
||||
self._config.set_obj(name, value, pattern=self._pattern)
|
||||
|
||||
def _join(self, attr):
|
||||
"""Get the prefix joined with the given attribute."""
|
||||
|
@ -26,7 +26,7 @@ from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.completion.models import configmodel
|
||||
from qutebrowser.utils import objreg, utils, message, standarddir
|
||||
from qutebrowser.utils import objreg, utils, message, standarddir, urlmatch
|
||||
from qutebrowser.config import configtypes, configexc, configfiles, configdata
|
||||
from qutebrowser.misc import editor
|
||||
|
||||
@ -47,17 +47,34 @@ class ConfigCommands:
|
||||
except configexc.Error as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
|
||||
def _print_value(self, option):
|
||||
def _parse_pattern(self, pattern):
|
||||
"""Parse a pattern string argument to a pattern."""
|
||||
if pattern is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return urlmatch.UrlPattern(pattern)
|
||||
except urlmatch.ParseError as e:
|
||||
raise cmdexc.CommandError("Error while parsing {}: {}"
|
||||
.format(pattern, str(e)))
|
||||
|
||||
def _print_value(self, option, pattern):
|
||||
"""Print the value of the given option."""
|
||||
with self._handle_config_error():
|
||||
value = self._config.get_str(option)
|
||||
message.info("{} = {}".format(option, value))
|
||||
value = self._config.get_str(option, pattern=pattern)
|
||||
|
||||
text = "{} = {}".format(option, value)
|
||||
if pattern is not None:
|
||||
text += " for {}".format(pattern)
|
||||
message.info(text)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.option)
|
||||
@cmdutils.argument('value', completion=configmodel.value)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def set(self, win_id, option=None, value=None, temp=False, print_=False):
|
||||
@cmdutils.argument('pattern', flag='u')
|
||||
def set(self, win_id, option=None, value=None, temp=False, print_=False,
|
||||
*, pattern=None):
|
||||
"""Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown
|
||||
@ -69,6 +86,7 @@ class ConfigCommands:
|
||||
Args:
|
||||
option: The name of the option.
|
||||
value: The value to set.
|
||||
pattern: The URL pattern to use.
|
||||
temp: Set value temporarily until qutebrowser is closed.
|
||||
print_: Print the value after setting.
|
||||
"""
|
||||
@ -82,8 +100,10 @@ class ConfigCommands:
|
||||
raise cmdexc.CommandError("Toggling values was moved to the "
|
||||
":config-cycle command")
|
||||
|
||||
pattern = self._parse_pattern(pattern)
|
||||
|
||||
if option.endswith('?') and option != '?':
|
||||
self._print_value(option[:-1])
|
||||
self._print_value(option[:-1], pattern=pattern)
|
||||
return
|
||||
|
||||
with self._handle_config_error():
|
||||
@ -91,10 +111,11 @@ class ConfigCommands:
|
||||
raise cmdexc.CommandError("set: The following arguments "
|
||||
"are required: value")
|
||||
else:
|
||||
self._config.set_str(option, value, save_yaml=not temp)
|
||||
self._config.set_str(option, value, pattern=pattern,
|
||||
save_yaml=not temp)
|
||||
|
||||
if print_:
|
||||
self._print_value(option)
|
||||
self._print_value(option, pattern=pattern)
|
||||
|
||||
@cmdutils.register(instance='config-commands', maxsplit=1,
|
||||
no_cmd_split=True, no_replace_variables=True)
|
||||
@ -161,18 +182,24 @@ class ConfigCommands:
|
||||
@cmdutils.register(instance='config-commands', star_args_optional=True)
|
||||
@cmdutils.argument('option', completion=configmodel.option)
|
||||
@cmdutils.argument('values', completion=configmodel.value)
|
||||
def config_cycle(self, option, *values, temp=False, print_=False):
|
||||
@cmdutils.argument('pattern', flag='u')
|
||||
def config_cycle(self, option, *values, pattern=None, temp=False,
|
||||
print_=False):
|
||||
"""Cycle an option between multiple values.
|
||||
|
||||
Args:
|
||||
option: The name of the option.
|
||||
values: The values to cycle through.
|
||||
pattern: The URL pattern to use.
|
||||
temp: Set value temporarily until qutebrowser is closed.
|
||||
print_: Print the value after setting.
|
||||
"""
|
||||
pattern = self._parse_pattern(pattern)
|
||||
|
||||
with self._handle_config_error():
|
||||
opt = self._config.get_opt(option)
|
||||
old_value = self._config.get_obj(option, mutable=False)
|
||||
old_value = self._config.get_obj_for_pattern(option,
|
||||
pattern=pattern)
|
||||
|
||||
if not values and isinstance(opt.typ, configtypes.Bool):
|
||||
values = ['true', 'false']
|
||||
@ -194,10 +221,11 @@ class ConfigCommands:
|
||||
value = values[0]
|
||||
|
||||
with self._handle_config_error():
|
||||
self._config.set_obj(option, value, save_yaml=not temp)
|
||||
self._config.set_obj(option, value, pattern=pattern,
|
||||
save_yaml=not temp)
|
||||
|
||||
if print_:
|
||||
self._print_value(option)
|
||||
self._print_value(option, pattern=pattern)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.customized_option)
|
||||
@ -291,13 +319,16 @@ class ConfigCommands:
|
||||
"overwrite!".format(filename))
|
||||
|
||||
if defaults:
|
||||
options = [(opt, opt.default)
|
||||
options = [(None, opt, opt.default)
|
||||
for _name, opt in sorted(configdata.DATA.items())]
|
||||
bindings = dict(configdata.DATA['bindings.default'].default)
|
||||
commented = True
|
||||
else:
|
||||
options = list(self._config)
|
||||
bindings = dict(self._config.get_obj('bindings.commands'))
|
||||
options = []
|
||||
for values in self._config:
|
||||
for scoped in values:
|
||||
options.append((scoped.pattern, values.opt, scoped.value))
|
||||
bindings = dict(self._config.get_mutable_obj('bindings.commands'))
|
||||
commented = False
|
||||
|
||||
writer = configfiles.ConfigPyWriter(options, bindings,
|
||||
|
@ -48,6 +48,7 @@ class Option:
|
||||
backends = attr.ib()
|
||||
raw_backends = attr.ib()
|
||||
description = attr.ib()
|
||||
supports_pattern = attr.ib(default=False)
|
||||
restart = attr.ib(default=False)
|
||||
|
||||
|
||||
@ -197,7 +198,8 @@ def _read_yaml(yaml_data):
|
||||
migrations = Migrations()
|
||||
data = utils.yaml_load(yaml_data)
|
||||
|
||||
keys = {'type', 'default', 'desc', 'backend', 'restart'}
|
||||
keys = {'type', 'default', 'desc', 'backend', 'restart',
|
||||
'supports_pattern'}
|
||||
|
||||
for name, option in data.items():
|
||||
if set(option.keys()) == {'renamed'}:
|
||||
@ -223,7 +225,9 @@ def _read_yaml(yaml_data):
|
||||
backends=_parse_yaml_backends(name, backends),
|
||||
raw_backends=backends if isinstance(backends, dict) else None,
|
||||
description=option['desc'],
|
||||
restart=option.get('restart', False))
|
||||
restart=option.get('restart', False),
|
||||
supports_pattern=option.get('supports_pattern', False),
|
||||
)
|
||||
|
||||
# Make sure no key shadows another.
|
||||
for key1 in parsed:
|
||||
|
@ -240,6 +240,7 @@ content.cache.appcache:
|
||||
default: true
|
||||
type: Bool
|
||||
backend: QtWebKit
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Enable support for the HTML 5 web application cache feature.
|
||||
|
||||
@ -298,12 +299,14 @@ content.dns_prefetch:
|
||||
default: true
|
||||
type: Bool
|
||||
backend: QtWebKit
|
||||
supports_pattern: true
|
||||
desc: Try to pre-fetch DNS entries to speed up browsing.
|
||||
|
||||
content.frame_flattening:
|
||||
default: false
|
||||
type: Bool
|
||||
backend: QtWebKit
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Expand each subframe to its contents.
|
||||
|
||||
@ -459,12 +462,14 @@ content.host_blocking.whitelist:
|
||||
content.hyperlink_auditing:
|
||||
default: false
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Enable hyperlink auditing (`<a ping>`).
|
||||
|
||||
content.images:
|
||||
default: true
|
||||
type: Bool
|
||||
desc: Load images automatically in web pages.
|
||||
supports_pattern: true
|
||||
|
||||
content.javascript.alert:
|
||||
default: true
|
||||
@ -474,6 +479,7 @@ content.javascript.alert:
|
||||
content.javascript.can_access_clipboard:
|
||||
default: false
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Allow JavaScript to read from or write to the clipboard.
|
||||
|
||||
@ -484,16 +490,19 @@ content.javascript.can_close_tabs:
|
||||
default: false
|
||||
type: Bool
|
||||
backend: QtWebKit
|
||||
supports_pattern: true
|
||||
desc: Allow JavaScript to close tabs.
|
||||
|
||||
content.javascript.can_open_tabs_automatically:
|
||||
default: false
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Allow JavaScript to open new tabs without user interaction.
|
||||
|
||||
content.javascript.enabled:
|
||||
default: true
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Enable JavaScript.
|
||||
|
||||
content.javascript.log:
|
||||
@ -536,16 +545,19 @@ content.javascript.prompt:
|
||||
content.local_content_can_access_remote_urls:
|
||||
default: false
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Allow locally loaded documents to access remote URLs.
|
||||
|
||||
content.local_content_can_access_file_urls:
|
||||
default: true
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Allow locally loaded documents to access other local URLs.
|
||||
|
||||
content.local_storage:
|
||||
default: true
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Enable support for HTML 5 local storage and Web SQL.
|
||||
|
||||
content.media_capture:
|
||||
@ -583,6 +595,7 @@ content.pdfjs:
|
||||
content.plugins:
|
||||
default: false
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Enable plugins in Web pages.
|
||||
|
||||
content.print_element_backgrounds:
|
||||
@ -591,6 +604,7 @@ content.print_element_backgrounds:
|
||||
backend:
|
||||
QtWebKit: true
|
||||
QtWebEngine: Qt 5.8
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Draw the background color and images also when the page is printed.
|
||||
|
||||
@ -631,11 +645,13 @@ content.user_stylesheets:
|
||||
content.webgl:
|
||||
default: true
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Enable WebGL.
|
||||
|
||||
content.xss_auditing:
|
||||
type: Bool
|
||||
default: false
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Monitor load requests for cross-site scripting attempts.
|
||||
|
||||
@ -978,6 +994,7 @@ input.insert_mode.plugins:
|
||||
input.links_included_in_focus_chain:
|
||||
default: true
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: Include hyperlinks in the keyboard focus chain when tabbing.
|
||||
|
||||
input.partial_timeout:
|
||||
@ -1003,6 +1020,7 @@ input.rocker_gestures:
|
||||
input.spatial_navigation:
|
||||
default: false
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Enable spatial navigation.
|
||||
|
||||
@ -1083,6 +1101,7 @@ scrolling.bar:
|
||||
scrolling.smooth:
|
||||
type: Bool
|
||||
default: false
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Enable smooth scrolling for web pages.
|
||||
|
||||
@ -1557,6 +1576,7 @@ zoom.text_only:
|
||||
type: Bool
|
||||
default: false
|
||||
backend: QtWebKit
|
||||
supports_pattern: true
|
||||
desc: Apply the zoom factor on a frame only to the text or to all content.
|
||||
|
||||
## colors
|
||||
@ -2309,6 +2329,12 @@ bindings.default:
|
||||
<Ctrl-p>: tab-pin
|
||||
q: record-macro
|
||||
"@": run-macro
|
||||
tsh: config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload
|
||||
tSh: config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload
|
||||
tsH: config-cycle -p -t -u *://*.{url:host}/* content.javascript.enabled ;; reload
|
||||
tSH: config-cycle -p -u *://*.{url:host}/* content.javascript.enabled ;; reload
|
||||
tsu: config-cycle -p -t -u {url} content.javascript.enabled ;; reload
|
||||
tSu: config-cycle -p -u {url} content.javascript.enabled ;; reload
|
||||
insert:
|
||||
<Ctrl-E>: open-editor
|
||||
<Shift-Ins>: insert-text {primary}
|
||||
|
@ -40,6 +40,15 @@ class BackendError(Error):
|
||||
"backend!".format(name, backend.name))
|
||||
|
||||
|
||||
class NoPatternError(Error):
|
||||
|
||||
"""Raised when the given setting does not support URL patterns."""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__("The {} setting does not support URL patterns!"
|
||||
.format(name))
|
||||
|
||||
|
||||
class ValidationError(Error):
|
||||
|
||||
"""Raised when a value for a config type was invalid.
|
||||
|
@ -32,8 +32,8 @@ import yaml
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QSettings
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.config import configexc, config, configdata
|
||||
from qutebrowser.utils import standarddir, utils, qtutils, log
|
||||
from qutebrowser.config import configexc, config, configdata, configutils
|
||||
from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch
|
||||
|
||||
|
||||
# The StateConfig instance
|
||||
@ -80,16 +80,19 @@ class YamlConfig(QObject):
|
||||
VERSION: The current version number of the config file.
|
||||
"""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._filename = os.path.join(standarddir.config(auto=True),
|
||||
'autoconfig.yml')
|
||||
self._values = {}
|
||||
self._dirty = None
|
||||
|
||||
self._values = {}
|
||||
for name, opt in configdata.DATA.items():
|
||||
self._values[name] = configutils.Values(opt)
|
||||
|
||||
def init_save_manager(self, save_manager):
|
||||
"""Make sure the config gets saved properly.
|
||||
|
||||
@ -98,18 +101,9 @@ class YamlConfig(QObject):
|
||||
"""
|
||||
save_manager.add_saveable('yaml-config', self._save, self.changed)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._values[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self._values[name] = value
|
||||
self._mark_changed()
|
||||
|
||||
def __contains__(self, name):
|
||||
return name in self._values
|
||||
|
||||
def __iter__(self):
|
||||
return iter(sorted(self._values.items()))
|
||||
"""Iterate over configutils.Values items."""
|
||||
yield from self._values.values()
|
||||
|
||||
def _mark_changed(self):
|
||||
"""Mark the YAML config as changed."""
|
||||
@ -121,7 +115,17 @@ class YamlConfig(QObject):
|
||||
if not self._dirty:
|
||||
return
|
||||
|
||||
data = {'config_version': self.VERSION, 'global': self._values}
|
||||
settings = {}
|
||||
for name, values in sorted(self._values.items()):
|
||||
if not values:
|
||||
continue
|
||||
settings[name] = {}
|
||||
for scoped in values:
|
||||
key = ('global' if scoped.pattern is None
|
||||
else str(scoped.pattern))
|
||||
settings[name][key] = scoped.value
|
||||
|
||||
data = {'config_version': self.VERSION, 'settings': settings}
|
||||
with qtutils.savefile_open(self._filename) as f:
|
||||
f.write(textwrap.dedent("""
|
||||
# DO NOT edit this file by hand, qutebrowser will overwrite it.
|
||||
@ -130,6 +134,29 @@ class YamlConfig(QObject):
|
||||
""".lstrip('\n')))
|
||||
utils.yaml_dump(data, f)
|
||||
|
||||
def _pop_object(self, yaml_data, key, typ):
|
||||
"""Get a global object from the given data."""
|
||||
if not isinstance(yaml_data, dict):
|
||||
desc = configexc.ConfigErrorDesc("While loading data",
|
||||
"Toplevel object is not a dict")
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
|
||||
if key not in yaml_data:
|
||||
desc = configexc.ConfigErrorDesc(
|
||||
"While loading data",
|
||||
"Toplevel object does not contain '{}' key".format(key))
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
|
||||
data = yaml_data.pop(key)
|
||||
|
||||
if not isinstance(data, typ):
|
||||
desc = configexc.ConfigErrorDesc(
|
||||
"While loading data",
|
||||
"'{}' object is not a {}".format(key, typ.__name__))
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
|
||||
return data
|
||||
|
||||
def load(self):
|
||||
"""Load configuration from the configured YAML file."""
|
||||
try:
|
||||
@ -144,76 +171,126 @@ class YamlConfig(QObject):
|
||||
desc = configexc.ConfigErrorDesc("While parsing", e)
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
|
||||
try:
|
||||
global_obj = yaml_data['global']
|
||||
except KeyError:
|
||||
config_version = self._pop_object(yaml_data, 'config_version', int)
|
||||
if config_version == 1:
|
||||
settings = self._load_legacy_settings_object(yaml_data)
|
||||
self._mark_changed()
|
||||
elif config_version > self.VERSION:
|
||||
desc = configexc.ConfigErrorDesc(
|
||||
"While loading data",
|
||||
"Toplevel object does not contain 'global' key")
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
except TypeError:
|
||||
desc = configexc.ConfigErrorDesc("While loading data",
|
||||
"Toplevel object is not a dict")
|
||||
"While reading",
|
||||
"Can't read config from incompatible newer version")
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
else:
|
||||
settings = self._load_settings_object(yaml_data)
|
||||
self._dirty = False
|
||||
|
||||
if not isinstance(global_obj, dict):
|
||||
desc = configexc.ConfigErrorDesc(
|
||||
"While loading data",
|
||||
"'global' object is not a dict")
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
settings = self._handle_migrations(settings)
|
||||
self._validate(settings)
|
||||
self._build_values(settings)
|
||||
|
||||
self._values = global_obj
|
||||
self._dirty = False
|
||||
def _load_settings_object(self, yaml_data):
|
||||
"""Load the settings from the settings: key."""
|
||||
return self._pop_object(yaml_data, 'settings', dict)
|
||||
|
||||
self._handle_migrations()
|
||||
self._validate()
|
||||
def _load_legacy_settings_object(self, yaml_data):
|
||||
data = self._pop_object(yaml_data, 'global', dict)
|
||||
settings = {}
|
||||
for name, value in data.items():
|
||||
settings[name] = {'global': value}
|
||||
return settings
|
||||
|
||||
def _handle_migrations(self):
|
||||
def _build_values(self, settings):
|
||||
"""Build up self._values from the values in the given dict."""
|
||||
errors = []
|
||||
for name, yaml_values in settings.items():
|
||||
if not isinstance(yaml_values, dict):
|
||||
errors.append(configexc.ConfigErrorDesc(
|
||||
"While parsing {!r}".format(name), "value is not a dict"))
|
||||
continue
|
||||
|
||||
values = configutils.Values(configdata.DATA[name])
|
||||
if 'global' in yaml_values:
|
||||
values.add(yaml_values.pop('global'))
|
||||
|
||||
for pattern, value in yaml_values.items():
|
||||
if not isinstance(pattern, str):
|
||||
errors.append(configexc.ConfigErrorDesc(
|
||||
"While parsing {!r}".format(name),
|
||||
"pattern is not of type string"))
|
||||
continue
|
||||
try:
|
||||
urlpattern = urlmatch.UrlPattern(pattern)
|
||||
except urlmatch.ParseError as e:
|
||||
errors.append(configexc.ConfigErrorDesc(
|
||||
"While parsing pattern {!r} for {!r}"
|
||||
.format(pattern, name), e))
|
||||
continue
|
||||
values.add(value, urlpattern)
|
||||
|
||||
self._values[name] = values
|
||||
|
||||
if errors:
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
|
||||
|
||||
def _handle_migrations(self, settings):
|
||||
"""Migrate older configs to the newest format."""
|
||||
# Simple renamed/deleted options
|
||||
for name in list(self._values):
|
||||
for name in list(settings):
|
||||
if name in configdata.MIGRATIONS.renamed:
|
||||
new_name = configdata.MIGRATIONS.renamed[name]
|
||||
log.config.debug("Renaming {} to {}".format(name, new_name))
|
||||
self._values[new_name] = self._values[name]
|
||||
del self._values[name]
|
||||
settings[new_name] = settings[name]
|
||||
del settings[name]
|
||||
self._mark_changed()
|
||||
elif name in configdata.MIGRATIONS.deleted:
|
||||
log.config.debug("Removing {}".format(name))
|
||||
del self._values[name]
|
||||
del settings[name]
|
||||
self._mark_changed()
|
||||
|
||||
# tabs.persist_mode_on_change got merged into tabs.mode_on_change
|
||||
old = 'tabs.persist_mode_on_change'
|
||||
new = 'tabs.mode_on_change'
|
||||
if old in self._values:
|
||||
if self._values[old]:
|
||||
self._values[new] = 'persist'
|
||||
else:
|
||||
self._values[new] = 'normal'
|
||||
del self._values[old]
|
||||
if old in settings:
|
||||
settings[new] = {}
|
||||
for scope, val in settings[old].items():
|
||||
if val:
|
||||
settings[new][scope] = 'persist'
|
||||
else:
|
||||
settings[new][scope] = 'normal'
|
||||
|
||||
del settings[old]
|
||||
self._mark_changed()
|
||||
|
||||
def _validate(self):
|
||||
return settings
|
||||
|
||||
def _validate(self, settings):
|
||||
"""Make sure all settings exist."""
|
||||
unknown = set(self._values) - set(configdata.DATA)
|
||||
unknown = []
|
||||
for name in settings:
|
||||
if name not in configdata.DATA:
|
||||
unknown.append(name)
|
||||
|
||||
if unknown:
|
||||
errors = [configexc.ConfigErrorDesc("While loading options",
|
||||
"Unknown option {}".format(e))
|
||||
for e in sorted(unknown)]
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
|
||||
|
||||
def unset(self, name):
|
||||
"""Remove the given option name if it's configured."""
|
||||
try:
|
||||
del self._values[name]
|
||||
except KeyError:
|
||||
return
|
||||
def set_obj(self, name, value, *, pattern=None):
|
||||
"""Set the given setting to the given value."""
|
||||
self._values[name].add(value, pattern)
|
||||
self._mark_changed()
|
||||
|
||||
def unset(self, name, *, pattern=None):
|
||||
"""Remove the given option name if it's configured."""
|
||||
changed = self._values[name].remove(pattern)
|
||||
if changed:
|
||||
self._mark_changed()
|
||||
|
||||
def clear(self):
|
||||
"""Clear all values from the YAML file."""
|
||||
self._values = []
|
||||
for values in self._values.values():
|
||||
values.clear()
|
||||
self._mark_changed()
|
||||
|
||||
|
||||
@ -242,6 +319,7 @@ class ConfigAPI:
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_error(self, action, name):
|
||||
"""Catch config-related exceptions and save them in self.errors."""
|
||||
try:
|
||||
yield
|
||||
except configexc.ConfigFileErrors as e:
|
||||
@ -251,28 +329,38 @@ class ConfigAPI:
|
||||
except configexc.Error as e:
|
||||
text = "While {} '{}'".format(action, name)
|
||||
self.errors.append(configexc.ConfigErrorDesc(text, e))
|
||||
except urlmatch.ParseError as e:
|
||||
text = "While {} '{}' and parsing pattern".format(action, name)
|
||||
self.errors.append(configexc.ConfigErrorDesc(text, e))
|
||||
|
||||
def finalize(self):
|
||||
"""Do work which needs to be done after reading config.py."""
|
||||
self._config.update_mutables()
|
||||
|
||||
def load_autoconfig(self):
|
||||
"""Load the autoconfig.yml file which is used for :set/:bind/etc."""
|
||||
with self._handle_error('reading', 'autoconfig.yml'):
|
||||
read_autoconfig()
|
||||
|
||||
def get(self, name):
|
||||
def get(self, name, pattern=None):
|
||||
"""Get a setting value from the config, optionally with a pattern."""
|
||||
with self._handle_error('getting', name):
|
||||
return self._config.get_obj(name)
|
||||
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
|
||||
return self._config.get_mutable_obj(name, pattern=urlpattern)
|
||||
|
||||
def set(self, name, value):
|
||||
def set(self, name, value, pattern=None):
|
||||
"""Set a setting value in the config, optionally with a pattern."""
|
||||
with self._handle_error('setting', name):
|
||||
self._config.set_obj(name, value)
|
||||
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
|
||||
self._config.set_obj(name, value, pattern=urlpattern)
|
||||
|
||||
def bind(self, key, command, mode='normal'):
|
||||
"""Bind a key to a command, with an optional key mode."""
|
||||
with self._handle_error('binding', key):
|
||||
self._keyconfig.bind(key, command, mode=mode)
|
||||
|
||||
def unbind(self, key, mode='normal'):
|
||||
"""Unbind a key from a command, with an optional key mode."""
|
||||
with self._handle_error('unbinding', key):
|
||||
self._keyconfig.unbind(key, mode=mode)
|
||||
|
||||
@ -286,6 +374,16 @@ class ConfigAPI:
|
||||
except configexc.ConfigFileErrors as e:
|
||||
self.errors += e.errors
|
||||
|
||||
@contextlib.contextmanager
|
||||
def pattern(self, pattern):
|
||||
"""Get a ConfigContainer for the given pattern."""
|
||||
# We need to propagate the exception so we don't need to return
|
||||
# something.
|
||||
urlpattern = urlmatch.UrlPattern(pattern)
|
||||
container = config.ConfigContainer(config=self._config, configapi=self,
|
||||
pattern=urlpattern)
|
||||
yield container
|
||||
|
||||
|
||||
class ConfigPyWriter:
|
||||
|
||||
@ -344,7 +442,7 @@ class ConfigPyWriter:
|
||||
|
||||
def _gen_options(self):
|
||||
"""Generate the options part of the config."""
|
||||
for opt, value in self._options:
|
||||
for pattern, opt, value in self._options:
|
||||
if opt.name in ['bindings.commands', 'bindings.default']:
|
||||
continue
|
||||
|
||||
@ -363,7 +461,11 @@ class ConfigPyWriter:
|
||||
except KeyError:
|
||||
yield self._line("# - {}".format(val))
|
||||
|
||||
yield self._line('c.{} = {!r}'.format(opt.name, value))
|
||||
if pattern is None:
|
||||
yield self._line('c.{} = {!r}'.format(opt.name, value))
|
||||
else:
|
||||
yield self._line('config.set({!r}, {!r}, {!r})'.format(
|
||||
opt.name, value, str(pattern)))
|
||||
yield ''
|
||||
|
||||
def _gen_bindings(self):
|
||||
|
186
qutebrowser/config/configutils.py
Normal file
186
qutebrowser/config/configutils.py
Normal file
@ -0,0 +1,186 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Utilities and data structures used by various config code."""
|
||||
|
||||
|
||||
import attr
|
||||
|
||||
from qutebrowser.utils import utils
|
||||
from qutebrowser.config import configexc
|
||||
|
||||
|
||||
class _UnsetObject:
|
||||
|
||||
"""Sentinel object."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self):
|
||||
return '<UNSET>'
|
||||
|
||||
|
||||
UNSET = _UnsetObject()
|
||||
|
||||
|
||||
@attr.s
|
||||
class ScopedValue:
|
||||
|
||||
"""A configuration value which is valid for a UrlPattern.
|
||||
|
||||
Attributes:
|
||||
value: The value itself.
|
||||
pattern: The UrlPattern for the value, or None for global values.
|
||||
"""
|
||||
|
||||
value = attr.ib()
|
||||
pattern = attr.ib()
|
||||
|
||||
|
||||
class Values:
|
||||
|
||||
"""A collection of values for a single setting.
|
||||
|
||||
Currently, this is a list and iterates through all possible ScopedValues to
|
||||
find matching ones.
|
||||
|
||||
In the future, it should be possible to optimize this by doing
|
||||
pre-selection based on hosts, by making this a dict mapping the
|
||||
non-wildcard part of the host to a list of matching ScopedValues.
|
||||
|
||||
That way, when searching for a setting for sub.example.com, we only have to
|
||||
check 'sub.example.com', 'example.com', '.com' and '' instead of checking
|
||||
all ScopedValues for the given setting.
|
||||
|
||||
Attributes:
|
||||
opt: The Option being customized.
|
||||
"""
|
||||
|
||||
def __init__(self, opt, values=None):
|
||||
self.opt = opt
|
||||
self._values = values or []
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, opt=self.opt, values=self._values,
|
||||
constructor=True)
|
||||
|
||||
def __str__(self):
|
||||
"""Get the values as human-readable string."""
|
||||
if not self:
|
||||
return '{}: <unchanged>'.format(self.opt.name)
|
||||
|
||||
lines = []
|
||||
for scoped in self._values:
|
||||
str_value = self.opt.typ.to_str(scoped.value)
|
||||
if scoped.pattern is None:
|
||||
lines.append('{} = {}'.format(self.opt.name, str_value))
|
||||
else:
|
||||
lines.append('{}: {} = {}'.format(
|
||||
scoped.pattern, self.opt.name, str_value))
|
||||
return '\n'.join(lines)
|
||||
|
||||
def __iter__(self):
|
||||
"""Yield ScopedValue elements.
|
||||
|
||||
This yields in "normal" order, i.e. global and then first-set settings
|
||||
first.
|
||||
"""
|
||||
yield from self._values
|
||||
|
||||
def __bool__(self):
|
||||
"""Check whether this value is customized."""
|
||||
return bool(self._values)
|
||||
|
||||
def _check_pattern_support(self, arg):
|
||||
"""Make sure patterns are supported if one was given."""
|
||||
if arg is not None and not self.opt.supports_pattern:
|
||||
raise configexc.NoPatternError(self.opt.name)
|
||||
|
||||
def add(self, value, pattern=None):
|
||||
"""Add a value with the given pattern to the list of values."""
|
||||
self._check_pattern_support(pattern)
|
||||
self.remove(pattern)
|
||||
scoped = ScopedValue(value, pattern)
|
||||
self._values.append(scoped)
|
||||
|
||||
def remove(self, pattern=None):
|
||||
"""Remove the value with the given pattern.
|
||||
|
||||
If a matching pattern was removed, True is returned.
|
||||
If no matching pattern was found, False is returned.
|
||||
"""
|
||||
self._check_pattern_support(pattern)
|
||||
old_len = len(self._values)
|
||||
self._values = [v for v in self._values if v.pattern != pattern]
|
||||
return old_len != len(self._values)
|
||||
|
||||
def clear(self):
|
||||
"""Clear all customization for this value."""
|
||||
self._values = []
|
||||
|
||||
def _get_fallback(self, fallback):
|
||||
"""Get the fallback global/default value."""
|
||||
for scoped in self._values:
|
||||
if scoped.pattern is None:
|
||||
return scoped.value
|
||||
|
||||
if fallback:
|
||||
return self.opt.default
|
||||
else:
|
||||
return UNSET
|
||||
|
||||
def get_for_url(self, url=None, *, fallback=True):
|
||||
"""Get a config value, falling back when needed.
|
||||
|
||||
This first tries to find a value matching the URL (if given).
|
||||
If there's no match:
|
||||
With fallback=True, the global/default setting is returned.
|
||||
With fallback=False, UNSET is returned.
|
||||
"""
|
||||
self._check_pattern_support(url)
|
||||
if url is not None:
|
||||
for scoped in reversed(self._values):
|
||||
if scoped.pattern is not None and scoped.pattern.matches(url):
|
||||
return scoped.value
|
||||
|
||||
if not fallback:
|
||||
return UNSET
|
||||
|
||||
return self._get_fallback(fallback)
|
||||
|
||||
def get_for_pattern(self, pattern, *, fallback=True):
|
||||
"""Get a value only if it's been overridden for the given pattern.
|
||||
|
||||
This is useful when showing values to the user.
|
||||
|
||||
If there's no match:
|
||||
With fallback=True, the global/default setting is returned.
|
||||
With fallback=False, UNSET is returned.
|
||||
"""
|
||||
self._check_pattern_support(pattern)
|
||||
if pattern is not None:
|
||||
for scoped in reversed(self._values):
|
||||
if scoped.pattern == pattern:
|
||||
return scoped.value
|
||||
|
||||
if not fallback:
|
||||
return UNSET
|
||||
|
||||
return self._get_fallback(fallback)
|
@ -17,195 +17,150 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# We get various "abstract but not overridden" warnings
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
"""Bridge from QWeb(Engine)Settings to our own settings."""
|
||||
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, utils, debug, usertypes
|
||||
from qutebrowser.config import config, configutils
|
||||
from qutebrowser.utils import log, usertypes
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
UNSET = object()
|
||||
|
||||
|
||||
class Base:
|
||||
class AbstractSettings:
|
||||
|
||||
"""Base class for QWeb(Engine)Settings wrappers."""
|
||||
"""Abstract base class for settings set via QWeb(Engine)Settings."""
|
||||
|
||||
def __init__(self, default=UNSET):
|
||||
self._default = default
|
||||
_ATTRIBUTES = None
|
||||
_FONT_SIZES = None
|
||||
_FONT_FAMILIES = None
|
||||
_FONT_TO_QFONT = None
|
||||
|
||||
def _get_global_settings(self):
|
||||
"""Get a list of global QWeb(Engine)Settings to use."""
|
||||
raise NotImplementedError
|
||||
def __init__(self, settings):
|
||||
self._settings = settings
|
||||
|
||||
def _get_settings(self, settings):
|
||||
"""Get a list of QWeb(Engine)Settings objects to use.
|
||||
def set_attribute(self, name, value):
|
||||
"""Set the given QWebSettings/QWebEngineSettings attribute.
|
||||
|
||||
Args:
|
||||
settings: The QWeb(Engine)Settings instance to use, or None to use
|
||||
the global instance.
|
||||
If the value is configutils.UNSET, the value is reset instead.
|
||||
|
||||
Return:
|
||||
A list of QWeb(Engine)Settings objects. The first one should be
|
||||
used for reading.
|
||||
True if there was a change, False otherwise.
|
||||
"""
|
||||
if settings is None:
|
||||
return self._get_global_settings()
|
||||
else:
|
||||
return [settings]
|
||||
old_value = self.test_attribute(name)
|
||||
|
||||
def set(self, value, settings=None):
|
||||
"""Set the value of this setting.
|
||||
|
||||
Args:
|
||||
value: The value to set, or None to restore the default.
|
||||
settings: The QWeb(Engine)Settings instance to use, or None to use
|
||||
the global instance.
|
||||
"""
|
||||
if value is None:
|
||||
self.set_default(settings=settings)
|
||||
else:
|
||||
self._set(value, settings=settings)
|
||||
|
||||
def set_default(self, settings=None):
|
||||
"""Set the default value for this setting.
|
||||
|
||||
Not implemented for most settings.
|
||||
"""
|
||||
if self._default is UNSET:
|
||||
raise ValueError("No default set for {!r}".format(self))
|
||||
else:
|
||||
self._set(self._default, settings=settings)
|
||||
|
||||
def _set(self, value, settings):
|
||||
"""Inner function to set the value of this setting.
|
||||
|
||||
Must be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
value: The value to set.
|
||||
settings: The QWeb(Engine)Settings instance to use, or None to use
|
||||
the global instance.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Attribute(Base):
|
||||
|
||||
"""A setting set via QWeb(Engine)Settings::setAttribute.
|
||||
|
||||
Attributes:
|
||||
self._attributes: A list of QWeb(Engine)Settings::WebAttribute members.
|
||||
"""
|
||||
|
||||
ENUM_BASE = None
|
||||
|
||||
def __init__(self, *attributes, default=UNSET):
|
||||
super().__init__(default=default)
|
||||
self._attributes = list(attributes)
|
||||
|
||||
def __repr__(self):
|
||||
attributes = [debug.qenum_key(self.ENUM_BASE, attr)
|
||||
for attr in self._attributes]
|
||||
return utils.get_repr(self, attributes=attributes, constructor=True)
|
||||
|
||||
def _set(self, value, settings=None):
|
||||
for obj in self._get_settings(settings):
|
||||
for attribute in self._attributes:
|
||||
obj.setAttribute(attribute, value)
|
||||
|
||||
|
||||
class Setter(Base):
|
||||
|
||||
"""A setting set via a QWeb(Engine)Settings setter method.
|
||||
|
||||
This will pass the QWeb(Engine)Settings instance ("self") as first argument
|
||||
to the methods, so self._setter is the *unbound* method.
|
||||
|
||||
Attributes:
|
||||
_setter: The unbound QWeb(Engine)Settings method to set this value.
|
||||
_args: An iterable of the arguments to pass to the setter (before the
|
||||
value).
|
||||
_unpack: Whether to unpack args (True) or pass them directly (False).
|
||||
"""
|
||||
|
||||
def __init__(self, setter, args=(), unpack=False, default=UNSET):
|
||||
super().__init__(default=default)
|
||||
self._setter = setter
|
||||
self._args = args
|
||||
self._unpack = unpack
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, setter=self._setter, args=self._args,
|
||||
unpack=self._unpack, constructor=True)
|
||||
|
||||
def _set(self, value, settings=None):
|
||||
for obj in self._get_settings(settings):
|
||||
args = [obj]
|
||||
args.extend(self._args)
|
||||
if self._unpack:
|
||||
args.extend(value)
|
||||
for attribute in self._ATTRIBUTES[name]:
|
||||
if value is configutils.UNSET:
|
||||
self._settings.resetAttribute(attribute)
|
||||
new_value = self.test_attribute(name)
|
||||
else:
|
||||
args.append(value)
|
||||
self._setter(*args)
|
||||
self._settings.setAttribute(attribute, value)
|
||||
new_value = value
|
||||
|
||||
return old_value != new_value
|
||||
|
||||
class StaticSetter(Setter):
|
||||
def test_attribute(self, name):
|
||||
"""Get the value for the given attribute.
|
||||
|
||||
"""A setting set via a static QWeb(Engine)Settings method.
|
||||
If the setting resolves to a list of attributes, only the first
|
||||
attribute is tested.
|
||||
"""
|
||||
return self._settings.testAttribute(self._ATTRIBUTES[name][0])
|
||||
|
||||
self._setter is the *bound* method.
|
||||
"""
|
||||
def set_font_size(self, name, value):
|
||||
"""Set the given QWebSettings/QWebEngineSettings font size.
|
||||
|
||||
def _set(self, value, settings=None):
|
||||
if settings is not None:
|
||||
raise ValueError("'settings' may not be set with StaticSetters!")
|
||||
args = list(self._args)
|
||||
if self._unpack:
|
||||
args.extend(value)
|
||||
else:
|
||||
args.append(value)
|
||||
self._setter(*args)
|
||||
Return:
|
||||
True if there was a change, False otherwise.
|
||||
"""
|
||||
assert value is not configutils.UNSET
|
||||
family = self._FONT_SIZES[name]
|
||||
old_value = self._settings.fontSize(family)
|
||||
self._settings.setFontSize(family, value)
|
||||
return old_value != value
|
||||
|
||||
def set_font_family(self, name, value):
|
||||
"""Set the given QWebSettings/QWebEngineSettings font family.
|
||||
|
||||
class FontFamilySetter(Setter):
|
||||
With None (the default), QFont is used to get the default font for the
|
||||
family.
|
||||
|
||||
"""A setter for a font family.
|
||||
Return:
|
||||
True if there was a change, False otherwise.
|
||||
"""
|
||||
assert value is not configutils.UNSET
|
||||
family = self._FONT_FAMILIES[name]
|
||||
if value is None:
|
||||
font = QFont()
|
||||
font.setStyleHint(self._FONT_TO_QFONT[family])
|
||||
value = font.defaultFamily()
|
||||
|
||||
Gets the default value from QFont.
|
||||
"""
|
||||
old_value = self._settings.fontFamily(family)
|
||||
self._settings.setFontFamily(family, value)
|
||||
|
||||
def __init__(self, setter, font, qfont):
|
||||
super().__init__(setter=setter, args=[font])
|
||||
self._qfont = qfont
|
||||
return value != old_value
|
||||
|
||||
def set_default(self, settings=None):
|
||||
font = QFont()
|
||||
font.setStyleHint(self._qfont)
|
||||
value = font.defaultFamily()
|
||||
self._set(value, settings=settings)
|
||||
def set_default_text_encoding(self, encoding):
|
||||
"""Set the default text encoding to use.
|
||||
|
||||
Return:
|
||||
True if there was a change, False otherwise.
|
||||
"""
|
||||
assert encoding is not configutils.UNSET
|
||||
old_value = self._settings.defaultTextEncoding()
|
||||
self._settings.setDefaultTextEncoding(encoding)
|
||||
return old_value != encoding
|
||||
|
||||
def init_mappings(mappings):
|
||||
"""Initialize all settings based on a settings mapping."""
|
||||
for option, mapping in mappings.items():
|
||||
value = config.instance.get(option)
|
||||
log.config.vdebug("Setting {} to {!r}".format(option, value))
|
||||
mapping.set(value)
|
||||
def _update_setting(self, setting, value):
|
||||
"""Update the given setting/value.
|
||||
|
||||
Unknown settings are ignored.
|
||||
|
||||
def update_mappings(mappings, option):
|
||||
"""Update global settings when QWeb(Engine)Settings changed."""
|
||||
try:
|
||||
mapping = mappings[option]
|
||||
except KeyError:
|
||||
return
|
||||
value = config.instance.get(option)
|
||||
mapping.set(value)
|
||||
Return:
|
||||
True if there was a change, False otherwise.
|
||||
"""
|
||||
if setting in self._ATTRIBUTES:
|
||||
return self.set_attribute(setting, value)
|
||||
elif setting in self._FONT_SIZES:
|
||||
return self.set_font_size(setting, value)
|
||||
elif setting in self._FONT_FAMILIES:
|
||||
return self.set_font_family(setting, value)
|
||||
elif setting == 'content.default_encoding':
|
||||
return self.set_default_text_encoding(value)
|
||||
return False
|
||||
|
||||
def update_setting(self, setting):
|
||||
"""Update the given setting."""
|
||||
value = config.instance.get(setting)
|
||||
self._update_setting(setting, value)
|
||||
|
||||
def update_for_url(self, url):
|
||||
"""Update settings customized for the given tab.
|
||||
|
||||
Return:
|
||||
A set of settings which actually changed.
|
||||
"""
|
||||
changed_settings = set()
|
||||
for values in config.instance:
|
||||
if not values.opt.supports_pattern:
|
||||
continue
|
||||
|
||||
value = values.get_for_url(url, fallback=False)
|
||||
|
||||
changed = self._update_setting(values.opt.name, value)
|
||||
if changed:
|
||||
log.config.debug("Changed for {}: {} = {}".format(
|
||||
url.toDisplayString(), values.opt.name, value))
|
||||
changed_settings.add(values.opt.name)
|
||||
|
||||
return changed_settings
|
||||
|
||||
def init_settings(self):
|
||||
"""Set all supported settings correctly."""
|
||||
for setting in (list(self._ATTRIBUTES) + list(self._FONT_SIZES) +
|
||||
list(self._FONT_FAMILIES)):
|
||||
self.update_setting(setting)
|
||||
|
||||
|
||||
def init(args):
|
||||
|
277
qutebrowser/utils/urlmatch.py
Normal file
277
qutebrowser/utils/urlmatch.py
Normal file
@ -0,0 +1,277 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""A Chromium-like URL matching pattern.
|
||||
|
||||
See:
|
||||
https://developer.chrome.com/apps/match_patterns
|
||||
https://cs.chromium.org/chromium/src/extensions/common/url_pattern.cc
|
||||
https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import fnmatch
|
||||
import urllib.parse
|
||||
|
||||
from qutebrowser.utils import utils, qtutils
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
|
||||
"""Raised when a pattern could not be parsed."""
|
||||
|
||||
|
||||
class UrlPattern:
|
||||
|
||||
"""A Chromium-like URL matching pattern.
|
||||
|
||||
Class attributes:
|
||||
DEFAULT_PORTS: The default ports used for schemes which support ports.
|
||||
|
||||
Attributes:
|
||||
_pattern: The given pattern as string.
|
||||
_match_all: Whether the pattern should match all URLs.
|
||||
_match_subdomains: Whether the pattern should match subdomains of the
|
||||
given host.
|
||||
_scheme: The scheme to match to, or None to match any scheme.
|
||||
Note that with Chromium, '*'/None only matches http/https and
|
||||
not file/ftp. We deviate from that as per-URL settings aren't
|
||||
security relevant.
|
||||
_host: The host to match to, or None for any host.
|
||||
_path: The path to match to, or None for any path.
|
||||
_port: The port to match to as integer, or None for any port.
|
||||
"""
|
||||
|
||||
DEFAULT_PORTS = {'https': 443, 'http': 80, 'ftp': 21}
|
||||
|
||||
def __init__(self, pattern):
|
||||
# Make sure all attributes are initialized if we exit early.
|
||||
self._pattern = pattern
|
||||
self._match_all = False
|
||||
self._match_subdomains = False
|
||||
self._scheme = None
|
||||
self._host = None
|
||||
self._path = None
|
||||
self._port = None
|
||||
|
||||
# > The special pattern <all_urls> matches any URL that starts with a
|
||||
# > permitted scheme.
|
||||
if pattern == '<all_urls>':
|
||||
self._match_all = True
|
||||
return
|
||||
|
||||
if '\0' in pattern:
|
||||
raise ParseError("May not contain NUL byte")
|
||||
|
||||
pattern = self._fixup_pattern(pattern)
|
||||
|
||||
# We use urllib.parse instead of QUrl here because it can handle
|
||||
# hosts with * in them.
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(pattern)
|
||||
except ValueError as e:
|
||||
raise ParseError(str(e))
|
||||
|
||||
assert parsed is not None
|
||||
|
||||
self._init_scheme(parsed)
|
||||
self._init_host(parsed)
|
||||
self._init_path(parsed)
|
||||
self._init_port(parsed)
|
||||
|
||||
def _to_tuple(self):
|
||||
"""Get a pattern with information used for __eq__/__hash__."""
|
||||
return (self._match_all, self._match_subdomains, self._scheme,
|
||||
self._host, self._path, self._port)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._to_tuple())
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, UrlPattern):
|
||||
return NotImplemented
|
||||
# pylint: disable=protected-access
|
||||
return self._to_tuple() == other._to_tuple()
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, pattern=self._pattern, constructor=True)
|
||||
|
||||
def __str__(self):
|
||||
return self._pattern
|
||||
|
||||
def _fixup_pattern(self, pattern):
|
||||
"""Make sure the given pattern is parseable by urllib.parse."""
|
||||
if pattern.startswith('*:'): # Any scheme, but *:// is unparseable
|
||||
pattern = 'any:' + pattern[2:]
|
||||
|
||||
# Chromium handles file://foo like file:///foo
|
||||
# FIXME This doesn't actually strip the hostname correctly.
|
||||
if (pattern.startswith('file://') and
|
||||
not pattern.startswith('file:///')):
|
||||
pattern = 'file:///' + pattern[len("file://"):]
|
||||
|
||||
return pattern
|
||||
|
||||
def _init_scheme(self, parsed):
|
||||
if not parsed.scheme:
|
||||
raise ParseError("No scheme given")
|
||||
elif parsed.scheme == 'any':
|
||||
self._scheme = None
|
||||
return
|
||||
|
||||
self._scheme = parsed.scheme
|
||||
|
||||
def _init_path(self, parsed):
|
||||
if self._scheme == 'about' and not parsed.path.strip():
|
||||
raise ParseError("Pattern without path")
|
||||
|
||||
if parsed.path == '/*':
|
||||
self._path = None
|
||||
elif parsed.path == '':
|
||||
# We want to make it possible to leave off a trailing slash.
|
||||
self._path = '/'
|
||||
else:
|
||||
self._path = parsed.path
|
||||
|
||||
def _init_host(self, parsed):
|
||||
"""Parse the host from the given URL.
|
||||
|
||||
Deviation from Chromium:
|
||||
- http://:1234/ is not a valid URL because it has no host.
|
||||
"""
|
||||
if parsed.hostname is None or not parsed.hostname.strip():
|
||||
if self._scheme not in ['about', 'file', 'data', 'javascript']:
|
||||
raise ParseError("Pattern without host")
|
||||
assert self._host is None
|
||||
return
|
||||
|
||||
# FIXME what about multiple dots?
|
||||
host_parts = parsed.hostname.rstrip('.').split('.')
|
||||
if host_parts[0] == '*':
|
||||
host_parts = host_parts[1:]
|
||||
self._match_subdomains = True
|
||||
|
||||
if not host_parts:
|
||||
self._host = None
|
||||
return
|
||||
|
||||
self._host = '.'.join(host_parts)
|
||||
|
||||
if self._host.endswith('.*'):
|
||||
# Special case to have a nicer error
|
||||
raise ParseError("TLD wildcards are not implemented yet")
|
||||
elif '*' in self._host:
|
||||
# Only * or *.foo is allowed as host.
|
||||
raise ParseError("Invalid host wildcard")
|
||||
|
||||
def _init_port(self, parsed):
|
||||
"""Parse the port from the given URL.
|
||||
|
||||
Deviation from Chromium:
|
||||
- We use None instead of "*" if there's no port filter.
|
||||
"""
|
||||
if parsed.netloc.endswith(':*'):
|
||||
# We can't access parsed.port as it tries to run int()
|
||||
self._port = None
|
||||
elif parsed.netloc.endswith(':'):
|
||||
raise ParseError("Invalid port: Port is empty")
|
||||
else:
|
||||
try:
|
||||
self._port = parsed.port
|
||||
except ValueError as e:
|
||||
raise ParseError("Invalid port: {}".format(e))
|
||||
|
||||
if (self._scheme not in list(self.DEFAULT_PORTS) + [None] and
|
||||
self._port is not None):
|
||||
raise ParseError("Ports are unsupported with {} scheme".format(
|
||||
self._scheme))
|
||||
|
||||
def _matches_scheme(self, scheme):
|
||||
return self._scheme is None or self._scheme == scheme
|
||||
|
||||
def _matches_host(self, host):
|
||||
# FIXME what about multiple dots?
|
||||
host = host.rstrip('.')
|
||||
|
||||
# If we have no host in the match pattern, that means that we're
|
||||
# matching all hosts, which means we have a match no matter what the
|
||||
# test host is.
|
||||
# Contrary to Chromium, we don't need to check for
|
||||
# self._match_subdomains, as we want to return True here for e.g.
|
||||
# file:// as well.
|
||||
if self._host is None:
|
||||
return True
|
||||
|
||||
# If the hosts are exactly equal, we have a match.
|
||||
if host == self._host:
|
||||
return True
|
||||
|
||||
# Otherwise, we can only match if our match pattern matches subdomains.
|
||||
if not self._match_subdomains:
|
||||
return False
|
||||
|
||||
# We don't do subdomain matching against IP addresses, so we can give
|
||||
# up now if the test host is an IP address.
|
||||
if not utils.raises(ValueError, ipaddress.ip_address, host):
|
||||
return False
|
||||
|
||||
# Check if the test host is a subdomain of our host.
|
||||
if len(host) <= (len(self._host) + 1):
|
||||
return False
|
||||
|
||||
if not host.endswith(self._host):
|
||||
return False
|
||||
|
||||
return host[len(host) - len(self._host) - 1] == '.'
|
||||
|
||||
def _matches_port(self, scheme, port):
|
||||
if port == -1 and scheme in self.DEFAULT_PORTS:
|
||||
port = self.DEFAULT_PORTS[scheme]
|
||||
return self._port is None or self._port == port
|
||||
|
||||
def _matches_path(self, path):
|
||||
if self._path is None:
|
||||
return True
|
||||
|
||||
# Match 'google.com' with 'google.com/'
|
||||
if path + '/*' == self._path:
|
||||
return True
|
||||
|
||||
# FIXME Chromium seems to have a more optimized glob matching which
|
||||
# doesn't rely on regexes. Do we need that too?
|
||||
return fnmatch.fnmatchcase(path, self._path)
|
||||
|
||||
def matches(self, qurl):
|
||||
"""Check if the pattern matches the given QUrl."""
|
||||
qtutils.ensure_valid(qurl)
|
||||
|
||||
if self._match_all:
|
||||
return True
|
||||
|
||||
if not self._matches_scheme(qurl.scheme()):
|
||||
return False
|
||||
# FIXME ignore for file:// like Chromium?
|
||||
if not self._matches_host(qurl.host()):
|
||||
return False
|
||||
if not self._matches_port(qurl.scheme(), qurl.port()):
|
||||
return False
|
||||
if not self._matches_path(qurl.path()):
|
||||
return False
|
||||
|
||||
return True
|
@ -27,6 +27,7 @@ import operator
|
||||
import collections.abc
|
||||
import enum
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
|
||||
from qutebrowser.utils import log, qtutils, utils
|
||||
@ -394,3 +395,24 @@ class AbstractCertificateErrorWrapper:
|
||||
|
||||
def is_overridable(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@attr.s
|
||||
class NavigationRequest:
|
||||
|
||||
"""A request to navigate to the given URL."""
|
||||
|
||||
Type = enum.Enum('Type', [
|
||||
'link_clicked',
|
||||
'typed', # QtWebEngine only
|
||||
'form_submitted',
|
||||
'form_resubmitted', # QtWebKit only
|
||||
'back_forward',
|
||||
'reloaded',
|
||||
'other'
|
||||
])
|
||||
|
||||
url = attr.ib()
|
||||
navigation_type = attr.ib()
|
||||
is_main_frame = attr.ib()
|
||||
accepted = attr.ib(default=True)
|
||||
|
@ -143,6 +143,8 @@ PERFECT_FILES = [
|
||||
'config/configinit.py'),
|
||||
('tests/unit/config/test_configcommands.py',
|
||||
'config/configcommands.py'),
|
||||
('tests/unit/config/test_configutils.py',
|
||||
'config/configutils.py'),
|
||||
|
||||
('tests/unit/utils/test_qtutils.py',
|
||||
'utils/qtutils.py'),
|
||||
@ -164,6 +166,8 @@ PERFECT_FILES = [
|
||||
'utils/error.py'),
|
||||
('tests/unit/utils/test_javascript.py',
|
||||
'utils/javascript.py'),
|
||||
('tests/unit/utils/test_urlmatch.py',
|
||||
'utils/urlmatch.py'),
|
||||
|
||||
(None,
|
||||
'completion/models/util.py'),
|
||||
|
@ -419,6 +419,8 @@ def _generate_setting_option(f, opt):
|
||||
f.write(opt.description + "\n")
|
||||
if opt.restart:
|
||||
f.write("This setting requires a restart.\n")
|
||||
if opt.supports_pattern:
|
||||
f.write("\nThis setting supports URL patterns.\n")
|
||||
f.write("\n")
|
||||
typ = opt.typ.get_name().replace(',', ',')
|
||||
f.write('Type: <<types,{typ}>>\n'.format(typ=typ))
|
||||
|
16
tests/end2end/data/javascript/enabled.html
Normal file
16
tests/end2end/data/javascript/enabled.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
function check_enabled() {
|
||||
const elem = document.getElementById("status");
|
||||
elem.innerHTML = "enabled";
|
||||
console.log("JavaScript is enabled");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="check_enabled()">
|
||||
<p>JavaScript is <span id="status">disabled</span></p>
|
||||
<noscript>noscript tag</noscript>
|
||||
</body>
|
||||
</html>
|
@ -7,8 +7,10 @@
|
||||
try {
|
||||
localStorage.qute_test = "foo";
|
||||
elem.innerHTML = "working";
|
||||
console.log("local storage is working");
|
||||
} catch (e) {
|
||||
elem.innerHTML = "not working";
|
||||
console.log("local storage is not working");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -207,7 +207,7 @@ Feature: Using hints
|
||||
Scenario: Using :follow-hint inside an iframe
|
||||
When I open data/hints/iframe.html
|
||||
And I hint with args "links normal" and follow a
|
||||
Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
Then "navigation request: url http://localhost:*/data/hello.txt, type Type.link_clicked, *" should be logged
|
||||
|
||||
Scenario: Using :follow-hint inside an iframe button
|
||||
When I open data/hints/iframe_button.html
|
||||
@ -228,12 +228,12 @@ Feature: Using hints
|
||||
And I hint with args "all normal" and follow a
|
||||
And I run :scroll bottom
|
||||
And I hint with args "links normal" and follow a
|
||||
Then "navigation request: url http://localhost:*/data/hello2.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
Then "navigation request: url http://localhost:*/data/hello2.txt, type Type.link_clicked, *" should be logged
|
||||
|
||||
Scenario: Opening a link inside a specific iframe
|
||||
When I open data/hints/iframe_target.html
|
||||
And I hint with args "links normal" and follow a
|
||||
Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
Then "navigation request: url http://localhost:*/data/hello.txt, type Type.link_clicked, *" should be logged
|
||||
|
||||
Scenario: Opening a link with specific target frame in a new tab
|
||||
When I open data/hints/iframe_target.html
|
||||
|
@ -151,3 +151,25 @@ Feature: Javascript stuff
|
||||
And I run :greasemonkey-reload
|
||||
And I open data/hints/iframe.html
|
||||
Then the javascript message "Script is running on /data/hints/html/wrapped.html" should not be logged
|
||||
|
||||
Scenario: Per-URL localstorage setting
|
||||
When I set content.local_storage to false
|
||||
And I run :set -u http://localhost:*/data2/* content.local_storage true
|
||||
And I open data/javascript/localstorage.html
|
||||
And I wait for "[*] local storage is not working" in the log
|
||||
And I open data2/javascript/localstorage.html
|
||||
Then the javascript message "local storage is working" should be logged
|
||||
|
||||
Scenario: Per-URL JavaScript setting
|
||||
When I set content.javascript.enabled to false
|
||||
And I run :set -u http://localhost:*/data2/* content.javascript.enabled true
|
||||
And I open data2/javascript/enabled.html
|
||||
And I wait for "[*] JavaScript is enabled" in the log
|
||||
And I open data/javascript/enabled.html
|
||||
Then the page should contain the plaintext "JavaScript is disabled"
|
||||
|
||||
@qtwebkit_skip
|
||||
Scenario: Error pages without JS enabled
|
||||
When I set content.javascript.enabled to false
|
||||
And I open 500 without waiting
|
||||
Then "Showing error page for* 500" should be logged
|
||||
|
@ -250,7 +250,7 @@ Feature: Searching on a page
|
||||
And I run :search follow
|
||||
And I wait for "search found follow" in the log
|
||||
And I run :follow-selected
|
||||
Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, is_main_frame False" should be logged
|
||||
Then "navigation request: url http://localhost:*/data/hello.txt, type Type.link_clicked, is_main_frame False" should be logged
|
||||
|
||||
@qtwebkit_skip: Not supported in qtwebkit
|
||||
Scenario: Follow a tabbed searched link in an iframe
|
||||
|
@ -79,6 +79,7 @@ class Request(testprocess.Line):
|
||||
'/cookies/set': [HTTPStatus.FOUND],
|
||||
|
||||
'/500-inline': [HTTPStatus.INTERNAL_SERVER_ERROR],
|
||||
'/500': [HTTPStatus.INTERNAL_SERVER_ERROR],
|
||||
}
|
||||
for i in range(15):
|
||||
path_to_statuses['/redirect/{}'.format(i)] = [HTTPStatus.FOUND]
|
||||
|
@ -49,6 +49,7 @@ def root():
|
||||
|
||||
|
||||
@app.route('/data/<path:path>')
|
||||
@app.route('/data2/<path:path>') # for per-URL settings
|
||||
def send_data(path):
|
||||
"""Send a given data file to qutebrowser.
|
||||
|
||||
@ -179,6 +180,14 @@ def internal_error_attachment():
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/500')
|
||||
def internal_error():
|
||||
"""A normal 500 error."""
|
||||
r = flask.make_response()
|
||||
r.status_code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
return r
|
||||
|
||||
|
||||
@app.route('/cookies')
|
||||
def view_cookies():
|
||||
"""Show cookies."""
|
||||
|
@ -42,7 +42,8 @@ from PyQt5.QtNetwork import QNetworkCookieJar
|
||||
|
||||
import helpers.stubs as stubsmod
|
||||
import helpers.utils
|
||||
from qutebrowser.config import config, configdata, configtypes, configexc
|
||||
from qutebrowser.config import (config, configdata, configtypes, configexc,
|
||||
configfiles)
|
||||
from qutebrowser.utils import objreg, standarddir
|
||||
from qutebrowser.browser.webkit import cookies
|
||||
from qutebrowser.misc import savemanager, sql
|
||||
@ -193,9 +194,9 @@ def configdata_init():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_stub(stubs, monkeypatch, configdata_init):
|
||||
def config_stub(stubs, monkeypatch, configdata_init, config_tmpdir):
|
||||
"""Fixture which provides a fake config object."""
|
||||
yaml_config = stubs.FakeYamlConfig()
|
||||
yaml_config = configfiles.YamlConfig()
|
||||
|
||||
conf = config.Config(yaml_config=yaml_config)
|
||||
monkeypatch.setattr(config, 'instance', conf)
|
||||
|
@ -406,33 +406,6 @@ class InstaTimer(QObject):
|
||||
fun()
|
||||
|
||||
|
||||
class FakeYamlConfig:
|
||||
|
||||
"""Fake configfiles.YamlConfig object."""
|
||||
|
||||
def __init__(self):
|
||||
self.loaded = False
|
||||
self._values = {}
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self._values
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._values.items())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._values[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._values[key]
|
||||
|
||||
def unset(self, name):
|
||||
self._values.pop(name, None)
|
||||
|
||||
def clear(self):
|
||||
self._values = []
|
||||
|
||||
|
||||
class StatusBarCommandStub(QLineEdit):
|
||||
|
||||
"""Stub for the statusbar command prompt."""
|
||||
|
@ -32,7 +32,8 @@ def init_profiles(qapp, config_stub, cache_tmpdir, data_tmpdir):
|
||||
def test_big_cache_size(config_stub):
|
||||
"""Make sure a too big cache size is handled correctly."""
|
||||
config_stub.val.content.cache.size = 2 ** 63 - 1
|
||||
webenginesettings._update_settings('content.cache.size')
|
||||
profile = webenginesettings.default_profile
|
||||
|
||||
size = webenginesettings.default_profile.httpCacheMaximumSize()
|
||||
assert size == 2 ** 31 - 1
|
||||
webenginesettings._set_http_cache_size(profile)
|
||||
|
||||
assert profile.httpCacheMaximumSize() == 2 ** 31 - 1
|
||||
|
@ -80,9 +80,9 @@ def cmdutils_stub(monkeypatch, stubs):
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def configdata_stub(monkeypatch, configdata_init):
|
||||
def configdata_stub(config_stub, monkeypatch, configdata_init):
|
||||
"""Patch the configdata module to provide fake data."""
|
||||
return monkeypatch.setattr(configdata, 'DATA', collections.OrderedDict([
|
||||
monkeypatch.setattr(configdata, 'DATA', collections.OrderedDict([
|
||||
('aliases', configdata.Option(
|
||||
name='aliases',
|
||||
description='Aliases for commands.',
|
||||
@ -132,6 +132,7 @@ def configdata_stub(monkeypatch, configdata_init):
|
||||
backends=[],
|
||||
raw_backends=None)),
|
||||
]))
|
||||
config_stub._init_values()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -23,11 +23,11 @@ import types
|
||||
import unittest.mock
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtGui import QColor
|
||||
|
||||
from qutebrowser.config import config, configdata, configexc, configfiles
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.config import config, configdata, configexc, configutils
|
||||
from qutebrowser.utils import usertypes, urlmatch
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
|
||||
@ -303,9 +303,15 @@ class TestKeyConfig:
|
||||
class TestConfig:
|
||||
|
||||
@pytest.fixture
|
||||
def conf(self, config_tmpdir):
|
||||
yaml_config = configfiles.YamlConfig()
|
||||
return config.Config(yaml_config)
|
||||
def conf(self, config_stub):
|
||||
return config_stub
|
||||
|
||||
@pytest.fixture
|
||||
def yaml_value(self, conf):
|
||||
"""Fixture which provides a getter for a YAML value."""
|
||||
def getter(option):
|
||||
return conf._yaml._values[option].get_for_url(fallback=False)
|
||||
return getter
|
||||
|
||||
def test_init_save_manager(self, conf, fake_save_manager):
|
||||
conf.init_save_manager(fake_save_manager)
|
||||
@ -327,10 +333,10 @@ class TestConfig:
|
||||
monkeypatch.setattr(config.objects, 'backend', objects.NoBackend())
|
||||
opt = conf.get_opt('tabs.show')
|
||||
conf._set_value(opt, 'never')
|
||||
assert conf._values['tabs.show'] == 'never'
|
||||
assert conf.get_obj('tabs.show') == 'never'
|
||||
|
||||
@pytest.mark.parametrize('save_yaml', [True, False])
|
||||
def test_unset(self, conf, qtbot, save_yaml):
|
||||
def test_unset(self, conf, qtbot, yaml_value, save_yaml):
|
||||
name = 'tabs.show'
|
||||
conf.set_obj(name, 'never', save_yaml=True)
|
||||
assert conf.get(name) == 'never'
|
||||
@ -340,9 +346,9 @@ class TestConfig:
|
||||
|
||||
assert conf.get(name) == 'always'
|
||||
if save_yaml:
|
||||
assert name not in conf._yaml
|
||||
assert yaml_value(name) is configutils.UNSET
|
||||
else:
|
||||
assert conf._yaml[name] == 'never'
|
||||
assert yaml_value(name) == 'never'
|
||||
|
||||
def test_unset_never_set(self, conf, qtbot):
|
||||
name = 'tabs.show'
|
||||
@ -353,18 +359,14 @@ class TestConfig:
|
||||
|
||||
assert conf.get(name) == 'always'
|
||||
|
||||
def test_unset_unknown(self, conf):
|
||||
with pytest.raises(configexc.NoOptionError):
|
||||
conf.unset('tabs')
|
||||
|
||||
@pytest.mark.parametrize('save_yaml', [True, False])
|
||||
def test_clear(self, conf, qtbot, save_yaml):
|
||||
def test_clear(self, conf, qtbot, yaml_value, save_yaml):
|
||||
name1 = 'tabs.show'
|
||||
name2 = 'content.plugins'
|
||||
conf.set_obj(name1, 'never', save_yaml=True)
|
||||
conf.set_obj(name2, True, save_yaml=True)
|
||||
assert conf._values[name1] == 'never'
|
||||
assert conf._values[name2] is True
|
||||
assert conf.get_obj(name1) == 'never'
|
||||
assert conf.get_obj(name2) is True
|
||||
|
||||
with qtbot.waitSignals([conf.changed, conf.changed]) as blocker:
|
||||
conf.clear(save_yaml=save_yaml)
|
||||
@ -373,33 +375,52 @@ class TestConfig:
|
||||
assert options == {name1, name2}
|
||||
|
||||
if save_yaml:
|
||||
assert name1 not in conf._yaml
|
||||
assert name2 not in conf._yaml
|
||||
assert yaml_value(name1) is configutils.UNSET
|
||||
assert yaml_value(name2) is configutils.UNSET
|
||||
else:
|
||||
assert conf._yaml[name1] == 'never'
|
||||
assert conf._yaml[name2] is True
|
||||
assert yaml_value(name1) == 'never'
|
||||
assert yaml_value(name2) is True
|
||||
|
||||
def test_read_yaml(self, conf):
|
||||
conf._yaml['content.plugins'] = True
|
||||
def test_read_yaml(self, conf, yaml_value):
|
||||
conf._yaml.set_obj('content.plugins', True)
|
||||
conf.read_yaml()
|
||||
assert conf._values['content.plugins'] is True
|
||||
assert conf.get_obj('content.plugins') is True
|
||||
|
||||
def test_get_opt_valid(self, conf):
|
||||
assert conf.get_opt('tabs.show') == configdata.DATA['tabs.show']
|
||||
|
||||
def test_get_opt_invalid(self, conf):
|
||||
@pytest.mark.parametrize('code', [
|
||||
lambda c: c.get_opt('tabs'),
|
||||
lambda c: c.get('tabs'),
|
||||
lambda c: c.get_obj('tabs'),
|
||||
lambda c: c.get_obj_for_pattern('tabs', pattern=None),
|
||||
lambda c: c.get_mutable_obj('tabs'),
|
||||
lambda c: c.get_str('tabs'),
|
||||
|
||||
lambda c: c.set_obj('tabs', 42),
|
||||
lambda c: c.set_str('tabs', '42'),
|
||||
lambda c: c.unset('tabs'),
|
||||
])
|
||||
def test_no_option_error(self, conf, code):
|
||||
with pytest.raises(configexc.NoOptionError):
|
||||
conf.get_opt('tabs')
|
||||
code(conf)
|
||||
|
||||
def test_get(self, conf):
|
||||
"""Test conf.get() with a QColor (where get/get_obj is different)."""
|
||||
assert conf.get('colors.completion.category.fg') == QColor('white')
|
||||
|
||||
def test_get_for_url(self, conf):
|
||||
"""Test conf.get() with an URL/pattern."""
|
||||
pattern = urlmatch.UrlPattern('*://example.com/')
|
||||
name = 'content.javascript.enabled'
|
||||
conf.set_obj(name, False, pattern=pattern)
|
||||
assert conf.get(name, url=QUrl('https://example.com/')) is False
|
||||
|
||||
@pytest.mark.parametrize('value', [{}, {'normal': {'a': 'nop'}}])
|
||||
def test_get_bindings(self, config_stub, conf, value):
|
||||
"""Test conf.get() with bindings which have missing keys."""
|
||||
config_stub.val.aliases = {}
|
||||
conf._values['bindings.commands'] = value
|
||||
conf.set_obj('bindings.commands', value)
|
||||
assert conf.get('bindings.commands')['prompt'] == {}
|
||||
|
||||
def test_get_mutable(self, conf):
|
||||
@ -415,7 +436,7 @@ class TestConfig:
|
||||
'bindings.commands'])
|
||||
@pytest.mark.parametrize('mutable', [True, False])
|
||||
@pytest.mark.parametrize('mutated', [True, False])
|
||||
def test_get_obj_mutable(self, conf, config_stub, qtbot, caplog,
|
||||
def test_get_obj_mutable(self, conf, qtbot, caplog,
|
||||
option, mutable, mutated):
|
||||
"""Make sure mutables are handled correctly.
|
||||
|
||||
@ -432,7 +453,7 @@ class TestConfig:
|
||||
(keyhint.blacklist).
|
||||
"""
|
||||
# Setting new value
|
||||
obj = conf.get_obj(option, mutable=mutable)
|
||||
obj = conf.get_mutable_obj(option) if mutable else conf.get_obj(option)
|
||||
with qtbot.assert_not_emitted(conf.changed):
|
||||
if option == 'content.headers.custom':
|
||||
old = {}
|
||||
@ -454,7 +475,6 @@ class TestConfig:
|
||||
assert obj == new
|
||||
else:
|
||||
assert option == 'bindings.commands'
|
||||
config_stub.val.aliases = {}
|
||||
old = {}
|
||||
new = {}
|
||||
assert obj == old
|
||||
@ -485,9 +505,9 @@ class TestConfig:
|
||||
def test_get_mutable_twice(self, conf):
|
||||
"""Get a mutable value twice."""
|
||||
option = 'content.headers.custom'
|
||||
obj = conf.get_obj(option, mutable=True)
|
||||
obj = conf.get_mutable_obj(option)
|
||||
obj['X-Foo'] = 'fooval'
|
||||
obj2 = conf.get_obj(option, mutable=True)
|
||||
obj2 = conf.get_mutable_obj(option)
|
||||
obj2['X-Bar'] = 'barval'
|
||||
|
||||
conf.update_mutables()
|
||||
@ -497,9 +517,32 @@ class TestConfig:
|
||||
|
||||
def test_get_obj_unknown_mutable(self, conf):
|
||||
"""Make sure we don't have unknown mutable types."""
|
||||
conf._values['aliases'] = set() # This would never happen
|
||||
with pytest.raises(AssertionError):
|
||||
conf.get_obj('aliases')
|
||||
conf._maybe_copy(set())
|
||||
|
||||
def test_copy_non_mutable(self, conf, mocker):
|
||||
"""Make sure no copies are done for non-mutable types."""
|
||||
spy = mocker.spy(config.copy, 'deepcopy')
|
||||
conf.get_mutable_obj('content.plugins')
|
||||
assert not spy.called
|
||||
|
||||
def test_copy_mutable(self, conf, mocker):
|
||||
"""Make sure mutable types are only copied once."""
|
||||
spy = mocker.spy(config.copy, 'deepcopy')
|
||||
conf.get_mutable_obj('bindings.commands')
|
||||
spy.assert_called_once_with(mocker.ANY)
|
||||
|
||||
def test_get_obj_for_pattern(self, conf):
|
||||
pattern = urlmatch.UrlPattern('*://example.com')
|
||||
name = 'content.javascript.enabled'
|
||||
conf.set_obj(name, False, pattern=pattern)
|
||||
assert conf.get_obj_for_pattern(name, pattern=pattern) is False
|
||||
|
||||
def test_get_obj_for_pattern_no_match(self, conf):
|
||||
pattern = urlmatch.UrlPattern('*://example.com')
|
||||
name = 'content.javascript.enabled'
|
||||
value = conf.get_obj_for_pattern(name, pattern=pattern)
|
||||
assert value is configutils.UNSET
|
||||
|
||||
def test_get_str(self, conf):
|
||||
assert conf.get_str('content.plugins') == 'false'
|
||||
@ -509,16 +552,17 @@ class TestConfig:
|
||||
('set_obj', True),
|
||||
('set_str', 'true'),
|
||||
])
|
||||
def test_set_valid(self, conf, qtbot, save_yaml, method, value):
|
||||
def test_set_valid(self, conf, qtbot, yaml_value,
|
||||
save_yaml, method, value):
|
||||
option = 'content.plugins'
|
||||
meth = getattr(conf, method)
|
||||
with qtbot.wait_signal(conf.changed):
|
||||
meth(option, value, save_yaml=save_yaml)
|
||||
assert conf._values[option] is True
|
||||
assert conf.get_obj(option) is True
|
||||
if save_yaml:
|
||||
assert conf._yaml[option] is True
|
||||
assert yaml_value(option) is True
|
||||
else:
|
||||
assert option not in conf._yaml
|
||||
assert yaml_value(option) is configutils.UNSET
|
||||
|
||||
@pytest.mark.parametrize('method', ['set_obj', 'set_str'])
|
||||
def test_set_invalid(self, conf, qtbot, method):
|
||||
@ -526,7 +570,7 @@ class TestConfig:
|
||||
with pytest.raises(configexc.ValidationError):
|
||||
with qtbot.assert_not_emitted(conf.changed):
|
||||
meth('content.plugins', '42')
|
||||
assert 'content.plugins' not in conf._values
|
||||
assert not conf._values['content.plugins']
|
||||
|
||||
@pytest.mark.parametrize('method', ['set_obj', 'set_str'])
|
||||
def test_set_wrong_backend(self, conf, qtbot, monkeypatch, method):
|
||||
@ -535,7 +579,15 @@ class TestConfig:
|
||||
with pytest.raises(configexc.BackendError):
|
||||
with qtbot.assert_not_emitted(conf.changed):
|
||||
meth('content.cookies.accept', 'all')
|
||||
assert 'content.cookies.accept' not in conf._values
|
||||
assert not conf._values['content.cookies.accept']
|
||||
|
||||
@pytest.mark.parametrize('method', ['set_obj', 'set_str'])
|
||||
def test_set_no_pattern(self, conf, method, qtbot):
|
||||
meth = getattr(conf, method)
|
||||
pattern = urlmatch.UrlPattern('https://www.example.com/')
|
||||
with pytest.raises(configexc.NoPatternError):
|
||||
with qtbot.assert_not_emitted(conf.changed):
|
||||
meth('colors.statusbar.normal.bg', '#abcdef', pattern=pattern)
|
||||
|
||||
def test_dump_userconfig(self, conf):
|
||||
conf.set_obj('content.plugins', True)
|
||||
@ -581,7 +633,7 @@ class TestContainer:
|
||||
|
||||
def test_setattr_option(self, config_stub, container):
|
||||
container.content.cookies.store = False
|
||||
assert config_stub._values['content.cookies.store'] is False
|
||||
assert config_stub.get_obj('content.cookies.store') is False
|
||||
|
||||
def test_confapi_errors(self, container):
|
||||
configapi = types.SimpleNamespace(errors=[])
|
||||
@ -593,6 +645,12 @@ class TestContainer:
|
||||
assert error.text == "While getting 'tabs.foobar'"
|
||||
assert str(error.exception) == "No option 'tabs.foobar'"
|
||||
|
||||
def test_pattern_no_configapi(self, config_stub):
|
||||
pattern = urlmatch.UrlPattern('https://example.com/')
|
||||
with pytest.raises(TypeError,
|
||||
match="Can't use pattern without configapi!"):
|
||||
config.ConfigContainer(config_stub, pattern=pattern)
|
||||
|
||||
|
||||
class StyleObj(QObject):
|
||||
|
||||
|
@ -24,9 +24,9 @@ import unittest.mock
|
||||
import pytest
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.config import configcommands
|
||||
from qutebrowser.config import configcommands, configutils
|
||||
from qutebrowser.commands import cmdexc
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.utils import usertypes, urlmatch
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
|
||||
@ -35,6 +35,14 @@ def commands(config_stub, key_config_stub):
|
||||
return configcommands.ConfigCommands(config_stub, key_config_stub)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def yaml_value(config_stub):
|
||||
"""Fixture which provides a getter for a YAML value."""
|
||||
def getter(option):
|
||||
return config_stub._yaml._values[option].get_for_url(fallback=False)
|
||||
return getter
|
||||
|
||||
|
||||
class TestSet:
|
||||
|
||||
"""Tests for :set."""
|
||||
@ -64,7 +72,7 @@ class TestSet:
|
||||
['gvim', '-f', '{file}', '-c', 'normal {line}G{column0}l'],
|
||||
'[emacs, "{}"]', ['emacs', '{}']),
|
||||
])
|
||||
def test_set_simple(self, monkeypatch, commands, config_stub,
|
||||
def test_set_simple(self, monkeypatch, commands, config_stub, yaml_value,
|
||||
temp, option, old_value, inp, new_value):
|
||||
"""Run ':set [-t] option value'.
|
||||
|
||||
@ -76,14 +84,38 @@ class TestSet:
|
||||
commands.set(0, option, inp, temp=temp)
|
||||
|
||||
assert config_stub.get(option) == new_value
|
||||
assert yaml_value(option) == (configutils.UNSET if temp else new_value)
|
||||
|
||||
if temp:
|
||||
assert option not in config_stub._yaml
|
||||
else:
|
||||
assert config_stub._yaml[option] == new_value
|
||||
def test_set_with_pattern(self, monkeypatch, commands, config_stub):
|
||||
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
|
||||
option = 'content.javascript.enabled'
|
||||
|
||||
commands.set(0, option, 'false', pattern='*://example.com')
|
||||
pattern = urlmatch.UrlPattern('*://example.com')
|
||||
|
||||
assert config_stub.get(option)
|
||||
assert not config_stub.get_obj_for_pattern(option, pattern=pattern)
|
||||
|
||||
def test_set_invalid_pattern(self, monkeypatch, commands):
|
||||
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
|
||||
option = 'content.javascript.enabled'
|
||||
|
||||
with pytest.raises(cmdexc.CommandError,
|
||||
match='Error while parsing :/: No scheme given'):
|
||||
commands.set(0, option, 'false', pattern=':/')
|
||||
|
||||
def test_set_no_pattern(self, monkeypatch, commands):
|
||||
"""Run ':set --pattern=*://* colors.statusbar.normal.bg #abcdef.
|
||||
|
||||
Should show an error as patterns are unsupported.
|
||||
"""
|
||||
with pytest.raises(cmdexc.CommandError,
|
||||
match='does not support URL patterns'):
|
||||
commands.set(0, 'colors.statusbar.normal.bg', '#abcdef',
|
||||
pattern='*://*')
|
||||
|
||||
@pytest.mark.parametrize('temp', [True, False])
|
||||
def test_set_temp_override(self, commands, config_stub, temp):
|
||||
def test_set_temp_override(self, commands, config_stub, yaml_value, temp):
|
||||
"""Invoking :set twice.
|
||||
|
||||
:set url.auto_search dns
|
||||
@ -96,19 +128,28 @@ class TestSet:
|
||||
commands.set(0, 'url.auto_search', 'never', temp=True)
|
||||
|
||||
assert config_stub.val.url.auto_search == 'never'
|
||||
assert config_stub._yaml['url.auto_search'] == 'dns'
|
||||
assert yaml_value('url.auto_search') == 'dns'
|
||||
|
||||
def test_set_print(self, config_stub, commands, message_mock):
|
||||
"""Run ':set -p url.auto_search never'.
|
||||
@pytest.mark.parametrize('pattern', [None, '*://example.com'])
|
||||
def test_set_print(self, config_stub, commands, message_mock, pattern):
|
||||
"""Run ':set -p [-u *://example.com] content.javascript.enabled false'.
|
||||
|
||||
Should set show the value.
|
||||
"""
|
||||
assert config_stub.val.url.auto_search == 'naive'
|
||||
commands.set(0, 'url.auto_search', 'dns', print_=True)
|
||||
assert config_stub.val.content.javascript.enabled
|
||||
commands.set(0, 'content.javascript.enabled', 'false', print_=True,
|
||||
pattern=pattern)
|
||||
|
||||
assert config_stub.val.url.auto_search == 'dns'
|
||||
value = config_stub.get_obj_for_pattern(
|
||||
'content.javascript.enabled',
|
||||
pattern=None if pattern is None else urlmatch.UrlPattern(pattern))
|
||||
assert not value
|
||||
|
||||
expected = 'content.javascript.enabled = false'
|
||||
if pattern is not None:
|
||||
expected += ' for {}'.format(pattern)
|
||||
msg = message_mock.getmsg(usertypes.MessageLevel.info)
|
||||
assert msg.text == 'url.auto_search = dns'
|
||||
assert msg.text == expected
|
||||
|
||||
def test_set_invalid_option(self, commands):
|
||||
"""Run ':set foo bar'.
|
||||
@ -177,13 +218,14 @@ class TestCycle:
|
||||
# Value which is not in the list
|
||||
('red', 'green'),
|
||||
])
|
||||
def test_cycling(self, commands, config_stub, initial, expected):
|
||||
def test_cycling(self, commands, config_stub, yaml_value,
|
||||
initial, expected):
|
||||
"""Run ':set' with multiple values."""
|
||||
opt = 'colors.statusbar.normal.bg'
|
||||
config_stub.set_obj(opt, initial)
|
||||
commands.config_cycle(opt, 'green', 'magenta', 'blue', 'yellow')
|
||||
assert config_stub.get(opt) == expected
|
||||
assert config_stub._yaml[opt] == expected
|
||||
assert yaml_value(opt) == expected
|
||||
|
||||
def test_different_representation(self, commands, config_stub):
|
||||
"""When using a different representation, cycling should work.
|
||||
@ -205,7 +247,7 @@ class TestCycle:
|
||||
assert not config_stub.val.auto_save.session
|
||||
commands.config_cycle('auto_save.session')
|
||||
assert config_stub.val.auto_save.session
|
||||
assert config_stub._yaml['auto_save.session']
|
||||
assert yaml_value('auto_save.session')
|
||||
|
||||
@pytest.mark.parametrize('args', [
|
||||
['url.auto_search'], ['url.auto_search', 'foo']
|
||||
@ -239,34 +281,28 @@ class TestUnsetAndClear:
|
||||
"""Test :config-unset and :config-clear."""
|
||||
|
||||
@pytest.mark.parametrize('temp', [True, False])
|
||||
def test_unset(self, commands, config_stub, temp):
|
||||
def test_unset(self, commands, config_stub, yaml_value, temp):
|
||||
name = 'tabs.show'
|
||||
config_stub.set_obj(name, 'never', save_yaml=True)
|
||||
|
||||
commands.config_unset(name, temp=temp)
|
||||
|
||||
assert config_stub.get(name) == 'always'
|
||||
if temp:
|
||||
assert config_stub._yaml[name] == 'never'
|
||||
else:
|
||||
assert name not in config_stub._yaml
|
||||
assert yaml_value(name) == ('never' if temp else configutils.UNSET)
|
||||
|
||||
def test_unset_unknown_option(self, commands):
|
||||
with pytest.raises(cmdexc.CommandError, match="No option 'tabs'"):
|
||||
commands.config_unset('tabs')
|
||||
|
||||
@pytest.mark.parametrize('save', [True, False])
|
||||
def test_clear(self, commands, config_stub, save):
|
||||
def test_clear(self, commands, config_stub, yaml_value, save):
|
||||
name = 'tabs.show'
|
||||
config_stub.set_obj(name, 'never', save_yaml=True)
|
||||
|
||||
commands.config_clear(save=save)
|
||||
|
||||
assert config_stub.get(name) == 'always'
|
||||
if save:
|
||||
assert name not in config_stub._yaml
|
||||
else:
|
||||
assert config_stub._yaml[name] == 'never'
|
||||
assert yaml_value(name) == (configutils.UNSET if save else 'never')
|
||||
|
||||
|
||||
class TestSource:
|
||||
@ -453,7 +489,7 @@ class TestBind:
|
||||
|
||||
@pytest.mark.parametrize('command', ['nop', 'nope'])
|
||||
def test_bind(self, commands, config_stub, no_bindings, key_config_stub,
|
||||
command):
|
||||
yaml_value, command):
|
||||
"""Simple :bind test (and aliases)."""
|
||||
config_stub.val.aliases = {'nope': 'nop'}
|
||||
config_stub.val.bindings.default = no_bindings
|
||||
@ -461,7 +497,7 @@ class TestBind:
|
||||
|
||||
commands.bind(0, 'a', command)
|
||||
assert key_config_stub.get_command('a', 'normal') == command
|
||||
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
|
||||
yaml_bindings = yaml_value('bindings.commands')['normal']
|
||||
assert yaml_bindings['a'] == command
|
||||
|
||||
@pytest.mark.parametrize('key, mode, expected', [
|
||||
@ -573,7 +609,7 @@ class TestBind:
|
||||
('c', 'c'), # :bind then :unbind
|
||||
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
|
||||
])
|
||||
def test_unbind(self, commands, key_config_stub, config_stub,
|
||||
def test_unbind(self, commands, key_config_stub, config_stub, yaml_value,
|
||||
key, normalized):
|
||||
config_stub.val.bindings.default = {
|
||||
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
|
||||
@ -590,7 +626,7 @@ class TestBind:
|
||||
commands.unbind(key)
|
||||
assert key_config_stub.get_command(key, 'normal') is None
|
||||
|
||||
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
|
||||
yaml_bindings = yaml_value('bindings.commands')['normal']
|
||||
if key in 'bc':
|
||||
# Custom binding
|
||||
assert normalized not in yaml_bindings
|
||||
|
@ -54,6 +54,12 @@ def test_backend_error():
|
||||
assert str(e) == expected
|
||||
|
||||
|
||||
def test_no_pattern_error():
|
||||
e = configexc.NoPatternError('foo')
|
||||
expected = "The foo setting does not support URL patterns!"
|
||||
assert str(e) == expected
|
||||
|
||||
|
||||
def test_desc_with_text():
|
||||
"""Test ConfigErrorDesc.with_text."""
|
||||
old = configexc.ConfigErrorDesc("Error text", Exception("Exception text"))
|
||||
|
@ -28,7 +28,7 @@ from PyQt5.QtCore import QSettings
|
||||
|
||||
from qutebrowser.config import (config, configfiles, configexc, configdata,
|
||||
configtypes)
|
||||
from qutebrowser.utils import utils, usertypes
|
||||
from qutebrowser.utils import utils, usertypes, urlmatch
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -38,6 +38,42 @@ def configdata_init():
|
||||
configdata.init()
|
||||
|
||||
|
||||
class AutoConfigHelper:
|
||||
|
||||
"""A helper to easily create/validate autoconfig.yml files."""
|
||||
|
||||
def __init__(self, config_tmpdir):
|
||||
self.fobj = config_tmpdir / 'autoconfig.yml'
|
||||
|
||||
def write_toplevel(self, data):
|
||||
with self.fobj.open('w', encoding='utf-8') as f:
|
||||
utils.yaml_dump(data, f)
|
||||
|
||||
def write(self, values):
|
||||
data = {'config_version': 2, 'settings': values}
|
||||
self.write_toplevel(data)
|
||||
|
||||
def write_raw(self, text):
|
||||
self.fobj.write_text(text, encoding='utf-8', ensure=True)
|
||||
|
||||
def read_toplevel(self):
|
||||
with self.fobj.open('r', encoding='utf-8') as f:
|
||||
data = utils.yaml_load(f)
|
||||
assert data['config_version'] == 2
|
||||
return data
|
||||
|
||||
def read(self):
|
||||
return self.read_toplevel()['settings']
|
||||
|
||||
def read_raw(self):
|
||||
return self.fobj.read_text('utf-8')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def autoconfig(config_tmpdir):
|
||||
return AutoConfigHelper(config_tmpdir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('old_data, insert, new_data', [
|
||||
(None, False, '[general]\n\n[geometry]\n\n'),
|
||||
('[general]\nfooled = true', False, '[general]\n\n[geometry]\n\n'),
|
||||
@ -75,49 +111,58 @@ class TestYaml:
|
||||
|
||||
@pytest.mark.parametrize('old_config', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
# Only global
|
||||
{'colors.hints.fg': {'global': 'magenta'}},
|
||||
# Global and for pattern
|
||||
{'content.javascript.enabled':
|
||||
{'global': True, 'https://example.com/': False}},
|
||||
# Only for pattern
|
||||
{'content.images': {'https://example.com/': False}},
|
||||
])
|
||||
@pytest.mark.parametrize('insert', [True, False])
|
||||
def test_yaml_config(self, yaml, config_tmpdir, old_config, insert):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
def test_yaml_config(self, yaml, autoconfig, old_config, insert):
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
autoconfig.write(old_config)
|
||||
|
||||
yaml.load()
|
||||
|
||||
if insert:
|
||||
yaml['tabs.show'] = 'never'
|
||||
yaml.set_obj('tabs.show', 'never')
|
||||
|
||||
yaml._save()
|
||||
|
||||
if not insert and old_config is None:
|
||||
lines = []
|
||||
else:
|
||||
text = autoconfig.read_text('utf-8')
|
||||
lines = text.splitlines()
|
||||
data = autoconfig.read()
|
||||
lines = autoconfig.read_raw().splitlines()
|
||||
|
||||
if insert:
|
||||
assert lines[0].startswith('# DO NOT edit this file by hand,')
|
||||
assert 'config_version: {}'.format(yaml.VERSION) in lines
|
||||
|
||||
assert 'global:' in lines
|
||||
|
||||
print(lines)
|
||||
|
||||
if 'magenta' in (old_config or ''):
|
||||
assert ' colors.hints.fg: magenta' in lines
|
||||
if old_config is None:
|
||||
pass
|
||||
elif 'colors.hints.fg' in old_config:
|
||||
assert data['colors.hints.fg'] == {'global': 'magenta'}
|
||||
elif 'content.javascript.enabled' in old_config:
|
||||
expected = {'global': True, 'https://example.com/': False}
|
||||
assert data['content.javascript.enabled'] == expected
|
||||
elif 'content.images' in old_config:
|
||||
assert data['content.images'] == {'https://example.com/': False}
|
||||
|
||||
if insert:
|
||||
assert ' tabs.show: never' in lines
|
||||
assert data['tabs.show'] == {'global': 'never'}
|
||||
|
||||
def test_init_save_manager(self, yaml, fake_save_manager):
|
||||
yaml.init_save_manager(fake_save_manager)
|
||||
fake_save_manager.add_saveable.assert_called_with(
|
||||
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
|
||||
|
||||
def test_unknown_key(self, yaml, config_tmpdir):
|
||||
def test_unknown_key(self, yaml, autoconfig):
|
||||
"""An unknown setting should show an error."""
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text('global:\n hello: world', encoding='utf-8')
|
||||
autoconfig.write({'hello': {'global': 'world'}})
|
||||
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
yaml.load()
|
||||
@ -127,10 +172,9 @@ class TestYaml:
|
||||
assert error.text == "While loading options"
|
||||
assert str(error.exception) == "Unknown option hello"
|
||||
|
||||
def test_multiple_unknown_keys(self, yaml, config_tmpdir):
|
||||
def test_multiple_unknown_keys(self, yaml, autoconfig):
|
||||
"""With multiple unknown settings, all should be shown."""
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text('global:\n one: 1\n two: 2', encoding='utf-8')
|
||||
autoconfig.write({'one': {'global': 1}, 'two': {'global': 2}})
|
||||
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
yaml.load()
|
||||
@ -141,23 +185,21 @@ class TestYaml:
|
||||
assert str(error1.exception) == "Unknown option one"
|
||||
assert str(error2.exception) == "Unknown option two"
|
||||
|
||||
def test_deleted_key(self, monkeypatch, yaml, config_tmpdir):
|
||||
def test_deleted_key(self, monkeypatch, yaml, autoconfig):
|
||||
"""A key marked as deleted should be removed."""
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text('global:\n hello: world', encoding='utf-8')
|
||||
autoconfig.write({'hello': {'global': 'world'}})
|
||||
|
||||
monkeypatch.setattr(configdata.MIGRATIONS, 'deleted', ['hello'])
|
||||
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
lines = autoconfig.read_text('utf-8').splitlines()
|
||||
assert ' hello: world' not in lines
|
||||
data = autoconfig.read()
|
||||
assert not data
|
||||
|
||||
def test_renamed_key(self, monkeypatch, yaml, config_tmpdir):
|
||||
def test_renamed_key(self, monkeypatch, yaml, autoconfig):
|
||||
"""A key marked as renamed should be renamed properly."""
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text('global:\n old: value', encoding='utf-8')
|
||||
autoconfig.write({'old': {'global': 'value'}})
|
||||
|
||||
monkeypatch.setattr(configdata.MIGRATIONS, 'renamed',
|
||||
{'old': 'tabs.show'})
|
||||
@ -165,29 +207,25 @@ class TestYaml:
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
lines = autoconfig.read_text('utf-8').splitlines()
|
||||
assert ' old: value' not in lines
|
||||
assert ' tabs.show: value' in lines
|
||||
data = autoconfig.read()
|
||||
assert data == {'tabs.show': {'global': 'value'}}
|
||||
|
||||
@pytest.mark.parametrize('persist', [True, False])
|
||||
def test_merge_persist(self, yaml, config_tmpdir, persist):
|
||||
def test_merge_persist(self, yaml, autoconfig, persist):
|
||||
"""Tests for migration of tabs.persist_mode_on_change."""
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text('global:\n tabs.persist_mode_on_change: {}'.
|
||||
format(persist), encoding='utf-8')
|
||||
autoconfig.write({'tabs.persist_mode_on_change': {'global': persist}})
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
lines = autoconfig.read_text('utf-8').splitlines()
|
||||
data = autoconfig.read()
|
||||
assert 'tabs.persist_mode_on_change' not in data
|
||||
mode = 'persist' if persist else 'normal'
|
||||
assert ' tabs.persist_mode_on_change:' not in lines
|
||||
assert ' tabs.mode_on_change: {}'.format(mode) in lines
|
||||
assert data['tabs.mode_on_change']['global'] == mode
|
||||
|
||||
def test_renamed_key_unknown_target(self, monkeypatch, yaml,
|
||||
config_tmpdir):
|
||||
autoconfig):
|
||||
"""A key marked as renamed with invalid name should raise an error."""
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text('global:\n old: value', encoding='utf-8')
|
||||
autoconfig.write({'old': {'global': 'value'}})
|
||||
|
||||
monkeypatch.setattr(configdata.MIGRATIONS, 'renamed',
|
||||
{'old': 'new'})
|
||||
@ -202,7 +240,7 @@ class TestYaml:
|
||||
|
||||
@pytest.mark.parametrize('old_config', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
{'colors.hints.fg': {'global': 'magenta'}},
|
||||
])
|
||||
@pytest.mark.parametrize('key, value', [
|
||||
('colors.hints.fg', 'green'),
|
||||
@ -210,61 +248,64 @@ class TestYaml:
|
||||
('confirm_quit', True),
|
||||
('confirm_quit', False),
|
||||
])
|
||||
def test_changed(self, yaml, qtbot, config_tmpdir, old_config, key, value):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
def test_changed(self, yaml, qtbot, autoconfig,
|
||||
old_config, key, value):
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
autoconfig.write(old_config)
|
||||
|
||||
yaml.load()
|
||||
|
||||
with qtbot.wait_signal(yaml.changed):
|
||||
yaml[key] = value
|
||||
yaml.set_obj(key, value)
|
||||
|
||||
assert key in yaml
|
||||
assert yaml[key] == value
|
||||
assert yaml._values[key].get_for_url(fallback=False) == value
|
||||
|
||||
yaml._save()
|
||||
|
||||
yaml = configfiles.YamlConfig()
|
||||
yaml.load()
|
||||
|
||||
assert key in yaml
|
||||
assert yaml[key] == value
|
||||
assert yaml._values[key].get_for_url(fallback=False) == value
|
||||
|
||||
def test_iter(self, yaml):
|
||||
yaml['foo'] = 23
|
||||
yaml['bar'] = 42
|
||||
assert list(iter(yaml)) == [('bar', 42), ('foo', 23)]
|
||||
assert list(iter(yaml)) == list(iter(yaml._values.values()))
|
||||
|
||||
@pytest.mark.parametrize('old_config', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
{'colors.hints.fg': {'global': 'magenta'}},
|
||||
])
|
||||
def test_unchanged(self, yaml, config_tmpdir, old_config):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
def test_unchanged(self, yaml, autoconfig, old_config):
|
||||
mtime = None
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
mtime = autoconfig.stat().mtime
|
||||
autoconfig.write(old_config)
|
||||
mtime = autoconfig.fobj.stat().mtime
|
||||
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
if old_config is None:
|
||||
assert not autoconfig.exists()
|
||||
assert not autoconfig.fobj.exists()
|
||||
else:
|
||||
assert autoconfig.stat().mtime == mtime
|
||||
assert autoconfig.fobj.stat().mtime == mtime
|
||||
|
||||
@pytest.mark.parametrize('line, text, exception', [
|
||||
('%', 'While parsing', 'while scanning a directive'),
|
||||
('global: 42', 'While loading data', "'global' object is not a dict"),
|
||||
('foo: 42', 'While loading data',
|
||||
"Toplevel object does not contain 'global' key"),
|
||||
('settings: 42\nconfig_version: 2',
|
||||
'While loading data', "'settings' object is not a dict"),
|
||||
('foo: 42\nconfig_version: 2', 'While loading data',
|
||||
"Toplevel object does not contain 'settings' key"),
|
||||
('settings: {}', 'While loading data',
|
||||
"Toplevel object does not contain 'config_version' key"),
|
||||
('42', 'While loading data', "Toplevel object is not a dict"),
|
||||
('settings: {"content.images": 42}\nconfig_version: 2',
|
||||
"While parsing 'content.images'", "value is not a dict"),
|
||||
('settings: {"content.images": {"https://": true}}\nconfig_version: 2',
|
||||
"While parsing pattern 'https://' for 'content.images'", "Pattern without host"),
|
||||
('settings: {"content.images": {true: true}}\nconfig_version: 2',
|
||||
"While parsing 'content.images'", "pattern is not of type string"),
|
||||
])
|
||||
def test_invalid(self, yaml, config_tmpdir, line, text, exception):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text(line, 'utf-8', ensure=True)
|
||||
def test_invalid(self, yaml, autoconfig, line, text, exception):
|
||||
autoconfig.write_raw(line)
|
||||
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
yaml.load()
|
||||
@ -275,11 +316,44 @@ class TestYaml:
|
||||
assert str(error.exception).splitlines()[0] == exception
|
||||
assert error.traceback is None
|
||||
|
||||
def test_oserror(self, yaml, config_tmpdir):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.ensure()
|
||||
autoconfig.chmod(0)
|
||||
if os.access(str(autoconfig), os.R_OK):
|
||||
def test_legacy_migration(self, yaml, autoconfig, qtbot):
|
||||
autoconfig.write_toplevel({
|
||||
'config_version': 1,
|
||||
'global': {'content.javascript.enabled': True},
|
||||
})
|
||||
with qtbot.wait_signal(yaml.changed):
|
||||
yaml.load()
|
||||
|
||||
yaml._save()
|
||||
|
||||
data = autoconfig.read_toplevel()
|
||||
assert data == {
|
||||
'config_version': 2,
|
||||
'settings': {
|
||||
'content.javascript.enabled': {
|
||||
'global': True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_read_newer_version(self, yaml, autoconfig):
|
||||
autoconfig.write_toplevel({
|
||||
'config_version': 999,
|
||||
'settings': {},
|
||||
})
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
yaml.load()
|
||||
|
||||
assert len(excinfo.value.errors) == 1
|
||||
error = excinfo.value.errors[0]
|
||||
assert error.text == "While reading"
|
||||
msg = "Can't read config from incompatible newer version"
|
||||
assert error.exception == msg
|
||||
|
||||
def test_oserror(self, yaml, autoconfig):
|
||||
autoconfig.fobj.ensure()
|
||||
autoconfig.fobj.chmod(0)
|
||||
if os.access(str(autoconfig.fobj), os.R_OK):
|
||||
# Docker container or similar
|
||||
pytest.skip("File was still readable")
|
||||
|
||||
@ -292,22 +366,22 @@ class TestYaml:
|
||||
assert isinstance(error.exception, OSError)
|
||||
assert error.traceback is None
|
||||
|
||||
def test_unset(self, yaml, qtbot, config_tmpdir):
|
||||
def test_unset(self, yaml, qtbot):
|
||||
name = 'tabs.show'
|
||||
yaml[name] = 'never'
|
||||
yaml.set_obj(name, 'never')
|
||||
|
||||
with qtbot.wait_signal(yaml.changed):
|
||||
yaml.unset(name)
|
||||
|
||||
assert name not in yaml
|
||||
|
||||
def test_unset_never_set(self, yaml, qtbot, config_tmpdir):
|
||||
def test_unset_never_set(self, yaml, qtbot):
|
||||
with qtbot.assert_not_emitted(yaml.changed):
|
||||
yaml.unset('tabs.show')
|
||||
|
||||
def test_clear(self, yaml, qtbot, config_tmpdir):
|
||||
def test_clear(self, yaml, qtbot):
|
||||
name = 'tabs.show'
|
||||
yaml[name] = 'never'
|
||||
yaml.set_obj(name, 'never')
|
||||
|
||||
with qtbot.wait_signal(yaml.changed):
|
||||
yaml.clear()
|
||||
@ -370,7 +444,7 @@ class TestConfigPyModules:
|
||||
confpy.write_qbmodule()
|
||||
confpy.read()
|
||||
expected = {'normal': {',a': 'message-info foo'}}
|
||||
assert config.instance._values['bindings.commands'] == expected
|
||||
assert config.instance.get_obj('bindings.commands') == expected
|
||||
assert "qbmodule" not in sys.modules.keys()
|
||||
assert tmpdir not in sys.path
|
||||
|
||||
@ -432,16 +506,41 @@ class TestConfigPy:
|
||||
@pytest.mark.parametrize('line', [
|
||||
'c.colors.hints.bg = "red"',
|
||||
'config.set("colors.hints.bg", "red")',
|
||||
'config.set("colors.hints.bg", "red", pattern=None)',
|
||||
])
|
||||
def test_set(self, confpy, line):
|
||||
confpy.write(line)
|
||||
confpy.read()
|
||||
assert config.instance._values['colors.hints.bg'] == 'red'
|
||||
assert config.instance.get_obj('colors.hints.bg') == 'red'
|
||||
|
||||
@pytest.mark.parametrize('template', [
|
||||
"config.set({opt!r}, False, {pattern!r})",
|
||||
"with config.pattern({pattern!r}) as p: p.{opt} = False",
|
||||
])
|
||||
def test_set_with_pattern(self, confpy, template):
|
||||
option = 'content.javascript.enabled'
|
||||
pattern = 'https://www.example.com/'
|
||||
|
||||
confpy.write(template.format(opt=option, pattern=pattern))
|
||||
confpy.read()
|
||||
|
||||
assert config.instance.get_obj(option)
|
||||
assert not config.instance.get_obj_for_pattern(
|
||||
option, pattern=urlmatch.UrlPattern(pattern))
|
||||
|
||||
def test_set_context_manager_global(self, confpy):
|
||||
"""When "with config.pattern" is used, "c." should still be global."""
|
||||
option = 'content.javascript.enabled'
|
||||
confpy.write('with config.pattern("https://www.example.com/") as p:'
|
||||
' c.{} = False'.format(option))
|
||||
confpy.read()
|
||||
assert not config.instance.get_obj(option)
|
||||
|
||||
@pytest.mark.parametrize('set_first', [True, False])
|
||||
@pytest.mark.parametrize('get_line', [
|
||||
'c.colors.hints.fg',
|
||||
'config.get("colors.hints.fg")',
|
||||
'config.get("colors.hints.fg", pattern=None)',
|
||||
])
|
||||
def test_get(self, confpy, set_first, get_line):
|
||||
"""Test whether getting options works correctly."""
|
||||
@ -455,6 +554,24 @@ class TestConfigPy:
|
||||
confpy.write('assert {} == "green"'.format(get_line))
|
||||
confpy.read()
|
||||
|
||||
def test_get_with_pattern(self, confpy):
|
||||
"""Test whether we get a matching value with a pattern."""
|
||||
option = 'content.javascript.enabled'
|
||||
pattern = 'https://www.example.com/'
|
||||
config.instance.set_obj(option, False,
|
||||
pattern=urlmatch.UrlPattern(pattern))
|
||||
confpy.write('assert config.get({!r})'.format(option),
|
||||
'assert not config.get({!r}, pattern={!r})'
|
||||
.format(option, pattern))
|
||||
confpy.read()
|
||||
|
||||
def test_get_with_pattern_no_match(self, confpy):
|
||||
confpy.write(
|
||||
'val = config.get("content.images", "https://www.example.com")',
|
||||
'assert val is True',
|
||||
)
|
||||
confpy.read()
|
||||
|
||||
@pytest.mark.parametrize('line, mode', [
|
||||
('config.bind(",a", "message-info foo")', 'normal'),
|
||||
('config.bind(",a", "message-info foo", "prompt")', 'prompt'),
|
||||
@ -463,7 +580,7 @@ class TestConfigPy:
|
||||
confpy.write(line)
|
||||
confpy.read()
|
||||
expected = {mode: {',a': 'message-info foo'}}
|
||||
assert config.instance._values['bindings.commands'] == expected
|
||||
assert config.instance.get_obj('bindings.commands') == expected
|
||||
|
||||
def test_bind_freshly_defined_alias(self, confpy):
|
||||
"""Make sure we can bind to a new alias.
|
||||
@ -479,14 +596,14 @@ class TestConfigPy:
|
||||
confpy.write("config.bind('H', 'message-info back')")
|
||||
confpy.read()
|
||||
expected = {'normal': {'H': 'message-info back'}}
|
||||
assert config.instance._values['bindings.commands'] == expected
|
||||
assert config.instance.get_obj('bindings.commands') == expected
|
||||
|
||||
def test_bind_none(self, confpy):
|
||||
confpy.write("c.bindings.commands = None",
|
||||
"config.bind(',x', 'nop')")
|
||||
confpy.read()
|
||||
expected = {'normal': {',x': 'nop'}}
|
||||
assert config.instance._values['bindings.commands'] == expected
|
||||
assert config.instance.get_obj('bindings.commands') == expected
|
||||
|
||||
@pytest.mark.parametrize('line, key, mode', [
|
||||
('config.unbind("o")', 'o', 'normal'),
|
||||
@ -496,14 +613,14 @@ class TestConfigPy:
|
||||
confpy.write(line)
|
||||
confpy.read()
|
||||
expected = {mode: {key: None}}
|
||||
assert config.instance._values['bindings.commands'] == expected
|
||||
assert config.instance.get_obj('bindings.commands') == expected
|
||||
|
||||
def test_mutating(self, confpy):
|
||||
confpy.write('c.aliases["foo"] = "message-info foo"',
|
||||
'c.aliases["bar"] = "message-info bar"')
|
||||
confpy.read()
|
||||
assert config.instance._values['aliases']['foo'] == 'message-info foo'
|
||||
assert config.instance._values['aliases']['bar'] == 'message-info bar'
|
||||
assert config.instance.get_obj('aliases')['foo'] == 'message-info foo'
|
||||
assert config.instance.get_obj('aliases')['bar'] == 'message-info bar'
|
||||
|
||||
@pytest.mark.parametrize('option, value', [
|
||||
('content.user_stylesheets', 'style.css'),
|
||||
@ -517,7 +634,7 @@ class TestConfigPy:
|
||||
(config_tmpdir / 'style.css').ensure()
|
||||
confpy.write('c.{}.append("{}")'.format(option, value))
|
||||
confpy.read()
|
||||
assert config.instance._values[option][-1] == value
|
||||
assert config.instance.get_obj(option)[-1] == value
|
||||
|
||||
def test_oserror(self, tmpdir, data_tmpdir, config_tmpdir):
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
@ -604,6 +721,22 @@ class TestConfigPy:
|
||||
"'qt.args')")
|
||||
assert str(error.exception) == expected
|
||||
|
||||
@pytest.mark.parametrize('line, text', [
|
||||
('config.get("content.images", "://")',
|
||||
"While getting 'content.images' and parsing pattern"),
|
||||
('config.set("content.images", False, "://")',
|
||||
"While setting 'content.images' and parsing pattern"),
|
||||
('with config.pattern("://"): pass',
|
||||
"Unhandled exception"),
|
||||
])
|
||||
def test_invalid_pattern(self, confpy, line, text):
|
||||
confpy.write(line)
|
||||
error = confpy.read(error=True)
|
||||
|
||||
assert error.text == text
|
||||
assert isinstance(error.exception, urlmatch.ParseError)
|
||||
assert str(error.exception) == "No scheme given"
|
||||
|
||||
def test_multiple_errors(self, confpy):
|
||||
confpy.write("c.foo = 42", "config.set('foo', 42)", "1/0")
|
||||
|
||||
@ -638,7 +771,7 @@ class TestConfigPy:
|
||||
confpy.write("config.source({!r})".format(arg))
|
||||
confpy.read()
|
||||
|
||||
assert not config.instance._values['content.javascript.enabled']
|
||||
assert not config.instance.get_obj('content.javascript.enabled')
|
||||
|
||||
def test_source_errors(self, tmpdir, confpy):
|
||||
subfile = tmpdir / 'config' / 'subfile.py'
|
||||
@ -682,7 +815,7 @@ class TestConfigPyWriter:
|
||||
name='opt', typ=configtypes.Int(), default='def',
|
||||
backends=[usertypes.Backend.QtWebEngine], raw_backends=None,
|
||||
description=desc)
|
||||
options = [(opt, 'val')]
|
||||
options = [(None, opt, 'val')]
|
||||
bindings = {'normal': {',x': 'message-info normal'},
|
||||
'caret': {',y': 'message-info caret'}}
|
||||
|
||||
@ -714,8 +847,8 @@ class TestConfigPyWriter:
|
||||
def test_binding_options_hidden(self):
|
||||
opt1 = configdata.DATA['bindings.default']
|
||||
opt2 = configdata.DATA['bindings.commands']
|
||||
options = [(opt1, {'normal': {'x': 'message-info x'}}),
|
||||
(opt2, {})]
|
||||
options = [(None, opt1, {'normal': {'x': 'message-info x'}}),
|
||||
(None, opt2, {})]
|
||||
writer = configfiles.ConfigPyWriter(options, bindings={},
|
||||
commented=False)
|
||||
text = '\n'.join(writer._gen_lines())
|
||||
@ -727,7 +860,7 @@ class TestConfigPyWriter:
|
||||
name='opt', typ=configtypes.Int(), default='def',
|
||||
backends=[usertypes.Backend.QtWebEngine], raw_backends=None,
|
||||
description='Hello World')
|
||||
options = [(opt, 'val')]
|
||||
options = [(None, opt, 'val')]
|
||||
bindings = {'normal': {',x': 'message-info normal'},
|
||||
'caret': {',y': 'message-info caret'}}
|
||||
|
||||
@ -753,7 +886,7 @@ class TestConfigPyWriter:
|
||||
backends=[usertypes.Backend.QtWebEngine], raw_backends=None,
|
||||
description='All colors are beautiful!')
|
||||
|
||||
options = [(opt1, 'ask'), (opt2, 'rgb')]
|
||||
options = [(None, opt1, 'ask'), (None, opt2, 'rgb')]
|
||||
|
||||
writer = configfiles.ConfigPyWriter(options, bindings={},
|
||||
commented=False)
|
||||
@ -794,6 +927,20 @@ class TestConfigPyWriter:
|
||||
""").lstrip()
|
||||
assert text == expected
|
||||
|
||||
def test_pattern(self):
|
||||
opt = configdata.Option(
|
||||
name='opt', typ=configtypes.BoolAsk(), default='ask',
|
||||
backends=[usertypes.Backend.QtWebEngine], raw_backends=None,
|
||||
description='Hello World')
|
||||
options = [
|
||||
(urlmatch.UrlPattern('https://www.example.com/'), opt, 'ask'),
|
||||
]
|
||||
writer = configfiles.ConfigPyWriter(options=options, bindings={},
|
||||
commented=False)
|
||||
text = '\n'.join(writer._gen_lines())
|
||||
expected = "config.set('opt', 'ask', 'https://www.example.com/')"
|
||||
assert expected in text
|
||||
|
||||
def test_write(self, tmpdir):
|
||||
pyfile = tmpdir / 'config.py'
|
||||
writer = configfiles.ConfigPyWriter(options=[], bindings={},
|
||||
@ -804,7 +951,7 @@ class TestConfigPyWriter:
|
||||
|
||||
def test_defaults_work(self, confpy):
|
||||
"""Get a config.py with default values and run it."""
|
||||
options = [(opt, opt.default)
|
||||
options = [(None, opt, opt.default)
|
||||
for _name, opt in sorted(configdata.DATA.items())]
|
||||
bindings = dict(configdata.DATA['bindings.default'].default)
|
||||
writer = configfiles.ConfigPyWriter(options, bindings, commented=False)
|
||||
|
@ -100,14 +100,15 @@ class TestEarlyInit:
|
||||
|
||||
# Check config values
|
||||
if config_py:
|
||||
assert config.instance._values == {'colors.hints.bg': 'red'}
|
||||
expected = 'colors.hints.bg = red'
|
||||
else:
|
||||
assert config.instance._values == {}
|
||||
expected = '<Default configuration>'
|
||||
assert config.instance.dump_userconfig() == expected
|
||||
|
||||
@pytest.mark.parametrize('load_autoconfig', [True, False]) # noqa
|
||||
@pytest.mark.parametrize('config_py', [True, 'error', False])
|
||||
@pytest.mark.parametrize('invalid_yaml', ['42', 'unknown', 'wrong-type',
|
||||
False])
|
||||
@pytest.mark.parametrize('invalid_yaml', ['42', 'list', 'unknown',
|
||||
'wrong-type', False])
|
||||
def test_autoconfig_yml(self, init_patch, config_tmpdir, caplog, args,
|
||||
load_autoconfig, config_py, invalid_yaml):
|
||||
"""Test interaction between config.py and autoconfig.yml."""
|
||||
@ -115,14 +116,30 @@ class TestEarlyInit:
|
||||
autoconfig_file = config_tmpdir / 'autoconfig.yml'
|
||||
config_py_file = config_tmpdir / 'config.py'
|
||||
|
||||
yaml_text = {
|
||||
yaml_lines = {
|
||||
'42': '42',
|
||||
'unknown': 'global:\n colors.foobar: magenta\n',
|
||||
'wrong-type': 'global:\n tabs.position: true\n',
|
||||
False: 'global:\n colors.hints.fg: magenta\n',
|
||||
'list': '[1, 2]',
|
||||
'unknown': [
|
||||
'settings:',
|
||||
' colors.foobar:',
|
||||
' global: magenta',
|
||||
'config_version: 2',
|
||||
],
|
||||
'wrong-type': [
|
||||
'settings:',
|
||||
' tabs.position:',
|
||||
' global: true',
|
||||
'config_version: 2',
|
||||
],
|
||||
False: [
|
||||
'settings:',
|
||||
' colors.hints.fg:',
|
||||
' global: magenta',
|
||||
'config_version: 2',
|
||||
],
|
||||
}
|
||||
autoconfig_file.write_text(yaml_text[invalid_yaml], 'utf-8',
|
||||
ensure=True)
|
||||
text = '\n'.join(yaml_lines[invalid_yaml])
|
||||
autoconfig_file.write_text(text, 'utf-8', ensure=True)
|
||||
|
||||
if config_py:
|
||||
config_py_lines = ['c.colors.hints.bg = "red"']
|
||||
@ -141,7 +158,7 @@ class TestEarlyInit:
|
||||
|
||||
if load_autoconfig or not config_py:
|
||||
suffix = ' (autoconfig.yml)' if config_py else ''
|
||||
if invalid_yaml == '42':
|
||||
if invalid_yaml in ['42', 'list']:
|
||||
error = ("While loading data{}: Toplevel object is not a dict"
|
||||
.format(suffix))
|
||||
expected_errors.append(error)
|
||||
@ -165,17 +182,21 @@ class TestEarlyInit:
|
||||
assert actual_errors == expected_errors
|
||||
|
||||
# Check config values
|
||||
dump = config.instance.dump_userconfig()
|
||||
|
||||
if config_py and load_autoconfig and not invalid_yaml:
|
||||
assert config.instance._values == {
|
||||
'colors.hints.bg': 'red',
|
||||
'colors.hints.fg': 'magenta',
|
||||
}
|
||||
expected = [
|
||||
'colors.hints.bg = red',
|
||||
'colors.hints.fg = magenta',
|
||||
]
|
||||
elif config_py:
|
||||
assert config.instance._values == {'colors.hints.bg': 'red'}
|
||||
expected = ['colors.hints.bg = red']
|
||||
elif invalid_yaml:
|
||||
assert config.instance._values == {}
|
||||
expected = ['<Default configuration>']
|
||||
else:
|
||||
assert config.instance._values == {'colors.hints.fg': 'magenta'}
|
||||
expected = ['colors.hints.fg = magenta']
|
||||
|
||||
assert dump == '\n'.join(expected)
|
||||
|
||||
def test_invalid_change_filter(self, init_patch, args):
|
||||
config.change_filter('foobar')
|
||||
@ -185,7 +206,7 @@ class TestEarlyInit:
|
||||
def test_temp_settings_valid(self, init_patch, args):
|
||||
args.temp_settings = [('colors.completion.fg', 'magenta')]
|
||||
configinit.early_init(args)
|
||||
assert config.instance._values['colors.completion.fg'] == 'magenta'
|
||||
assert config.instance.get_obj('colors.completion.fg') == 'magenta'
|
||||
|
||||
def test_temp_settings_invalid(self, caplog, init_patch, message_mock,
|
||||
args):
|
||||
@ -198,7 +219,6 @@ class TestEarlyInit:
|
||||
msg = message_mock.getmsg()
|
||||
assert msg.level == usertypes.MessageLevel.error
|
||||
assert msg.text == "set: NoOptionError - No option 'foo'"
|
||||
assert 'colors.completion.fg' not in config.instance._values
|
||||
|
||||
@pytest.mark.parametrize('settings, size, family', [
|
||||
# Only fonts.monospace customized
|
||||
@ -220,8 +240,9 @@ class TestEarlyInit:
|
||||
args.temp_settings = settings
|
||||
elif method == 'auto':
|
||||
autoconfig_file = config_tmpdir / 'autoconfig.yml'
|
||||
lines = ["global:"] + [" {}: '{}'".format(k, v)
|
||||
for k, v in settings]
|
||||
lines = (["config_version: 2", "settings:"] +
|
||||
[" {}:\n global:\n '{}'".format(k, v)
|
||||
for k, v in settings])
|
||||
autoconfig_file.write_text('\n'.join(lines), 'utf-8', ensure=True)
|
||||
elif method == 'py':
|
||||
config_py_file = config_tmpdir / 'config.py'
|
||||
|
210
tests/unit/config/test_configutils.py
Normal file
210
tests/unit/config/test_configutils.py
Normal file
@ -0,0 +1,210 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.config import configutils, configdata, configtypes
|
||||
from qutebrowser.utils import urlmatch
|
||||
|
||||
|
||||
def test_unset_object_identity():
|
||||
assert configutils._UnsetObject() is not configutils._UnsetObject()
|
||||
assert configutils.UNSET is configutils.UNSET
|
||||
|
||||
|
||||
def test_unset_object_repr():
|
||||
assert repr(configutils.UNSET) == '<UNSET>'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def opt():
|
||||
return configdata.Option(name='example.option', typ=configtypes.String(),
|
||||
default='default value', backends=None,
|
||||
raw_backends=None, description=None,
|
||||
supports_pattern=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pattern():
|
||||
return urlmatch.UrlPattern('*://www.example.com/')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_pattern():
|
||||
return urlmatch.UrlPattern('https://www.example.org/')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def values(opt, pattern):
|
||||
scoped_values = [configutils.ScopedValue('global value', None),
|
||||
configutils.ScopedValue('example value', pattern)]
|
||||
return configutils.Values(opt, scoped_values)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def empty_values(opt):
|
||||
return configutils.Values(opt)
|
||||
|
||||
|
||||
def test_repr(opt, values):
|
||||
expected = ("qutebrowser.config.configutils.Values(opt={!r}, "
|
||||
"values=[ScopedValue(value='global value', pattern=None), "
|
||||
"ScopedValue(value='example value', pattern=qutebrowser.utils."
|
||||
"urlmatch.UrlPattern(pattern='*://www.example.com/'))])"
|
||||
.format(opt))
|
||||
assert repr(values) == expected
|
||||
|
||||
|
||||
def test_str(values):
|
||||
expected = [
|
||||
'example.option = global value',
|
||||
'*://www.example.com/: example.option = example value',
|
||||
]
|
||||
assert str(values) == '\n'.join(expected)
|
||||
|
||||
|
||||
def test_str_empty(empty_values):
|
||||
assert str(empty_values) == 'example.option: <unchanged>'
|
||||
|
||||
|
||||
def test_bool(values, empty_values):
|
||||
assert values
|
||||
assert not empty_values
|
||||
|
||||
|
||||
def test_iter(values):
|
||||
assert list(iter(values)) == list(iter(values._values))
|
||||
|
||||
|
||||
def test_add_existing(values):
|
||||
values.add('new global value')
|
||||
assert values.get_for_url() == 'new global value'
|
||||
|
||||
|
||||
def test_add_new(values, other_pattern):
|
||||
values.add('example.org value', other_pattern)
|
||||
assert values.get_for_url() == 'global value'
|
||||
example_com = QUrl('https://www.example.com/')
|
||||
example_org = QUrl('https://www.example.org/')
|
||||
assert values.get_for_url(example_com) == 'example value'
|
||||
assert values.get_for_url(example_org) == 'example.org value'
|
||||
|
||||
|
||||
def test_remove_existing(values, pattern):
|
||||
removed = values.remove(pattern)
|
||||
assert removed
|
||||
|
||||
url = QUrl('https://www.example.com/')
|
||||
assert values.get_for_url(url) == 'global value'
|
||||
|
||||
|
||||
def test_remove_non_existing(values, other_pattern):
|
||||
removed = values.remove(other_pattern)
|
||||
assert not removed
|
||||
|
||||
url = QUrl('https://www.example.com/')
|
||||
assert values.get_for_url(url) == 'example value'
|
||||
|
||||
|
||||
def test_clear(values):
|
||||
assert values
|
||||
values.clear()
|
||||
assert not values
|
||||
assert values.get_for_url(fallback=False) is configutils.UNSET
|
||||
|
||||
|
||||
def test_get_matching(values):
|
||||
url = QUrl('https://www.example.com/')
|
||||
assert values.get_for_url(url, fallback=False) == 'example value'
|
||||
|
||||
|
||||
def test_get_unset(empty_values):
|
||||
assert empty_values.get_for_url(fallback=False) is configutils.UNSET
|
||||
|
||||
|
||||
def test_get_no_global(empty_values, other_pattern):
|
||||
empty_values.add('example.org value', pattern)
|
||||
assert empty_values.get_for_url(fallback=False) is configutils.UNSET
|
||||
|
||||
|
||||
def test_get_unset_fallback(empty_values):
|
||||
assert empty_values.get_for_url() == 'default value'
|
||||
|
||||
|
||||
def test_get_non_matching(values):
|
||||
url = QUrl('https://www.example.ch/')
|
||||
assert values.get_for_url(url, fallback=False) is configutils.UNSET
|
||||
|
||||
|
||||
def test_get_non_matching_fallback(values):
|
||||
url = QUrl('https://www.example.ch/')
|
||||
assert values.get_for_url(url) == 'global value'
|
||||
|
||||
|
||||
def test_get_multiple_matches(values):
|
||||
"""With multiple matching pattern, the last added should win."""
|
||||
all_pattern = urlmatch.UrlPattern('*://*/')
|
||||
values.add('new value', all_pattern)
|
||||
url = QUrl('https://www.example.com/')
|
||||
assert values.get_for_url(url) == 'new value'
|
||||
|
||||
|
||||
def test_get_matching_pattern(values, pattern):
|
||||
assert values.get_for_pattern(pattern, fallback=False) == 'example value'
|
||||
|
||||
|
||||
def test_get_pattern_none(values, pattern):
|
||||
assert values.get_for_pattern(None, fallback=False) == 'global value'
|
||||
|
||||
|
||||
def test_get_unset_pattern(empty_values, pattern):
|
||||
value = empty_values.get_for_pattern(pattern, fallback=False)
|
||||
assert value is configutils.UNSET
|
||||
|
||||
|
||||
def test_get_no_global_pattern(empty_values, pattern, other_pattern):
|
||||
empty_values.add('example.org value', other_pattern)
|
||||
value = empty_values.get_for_pattern(pattern, fallback=False)
|
||||
assert value is configutils.UNSET
|
||||
|
||||
|
||||
def test_get_unset_fallback_pattern(empty_values, pattern):
|
||||
assert empty_values.get_for_pattern(pattern) == 'default value'
|
||||
|
||||
|
||||
def test_get_non_matching_pattern(values, other_pattern):
|
||||
value = values.get_for_pattern(other_pattern, fallback=False)
|
||||
assert value is configutils.UNSET
|
||||
|
||||
|
||||
def test_get_non_matching_fallback_pattern(values, other_pattern):
|
||||
assert values.get_for_pattern(other_pattern) == 'global value'
|
||||
|
||||
|
||||
def test_get_equivalent_patterns(empty_values):
|
||||
"""With multiple matching pattern, the last added should win."""
|
||||
pat1 = urlmatch.UrlPattern('https://www.example.com/')
|
||||
pat2 = urlmatch.UrlPattern('*://www.example.com/')
|
||||
empty_values.add('pat1 value', pat1)
|
||||
empty_values.add('pat2 value', pat2)
|
||||
|
||||
assert empty_values.get_for_pattern(pat1) == 'pat1 value'
|
||||
assert empty_values.get_for_pattern(pat2) == 'pat2 value'
|
@ -56,22 +56,25 @@ class TestFileCompletion:
|
||||
def test_simple_completion(self, tmpdir, get_prompt, steps, where,
|
||||
subfolder):
|
||||
"""Simply trying to tab through items."""
|
||||
testdir = tmpdir / 'test'
|
||||
for directory in 'abc':
|
||||
(tmpdir / directory).ensure(dir=True)
|
||||
(testdir / directory).ensure(dir=True)
|
||||
|
||||
prompt = get_prompt(str(tmpdir) + os.sep)
|
||||
prompt = get_prompt(str(testdir) + os.sep)
|
||||
|
||||
for _ in range(steps):
|
||||
prompt.item_focus(where)
|
||||
|
||||
assert prompt._lineedit.text() == str(tmpdir / subfolder)
|
||||
assert prompt._lineedit.text() == str(testdir / subfolder)
|
||||
|
||||
def test_backspacing_path(self, qtbot, tmpdir, get_prompt):
|
||||
"""When we start deleting a path we want to see the subdir."""
|
||||
for directory in ['bar', 'foo']:
|
||||
(tmpdir / directory).ensure(dir=True)
|
||||
testdir = tmpdir / 'test'
|
||||
|
||||
prompt = get_prompt(str(tmpdir / 'foo') + os.sep)
|
||||
for directory in ['bar', 'foo']:
|
||||
(testdir / directory).ensure(dir=True)
|
||||
|
||||
prompt = get_prompt(str(testdir / 'foo') + os.sep)
|
||||
|
||||
# Deleting /f[oo/]
|
||||
with qtbot.wait_signal(prompt._file_model.directoryLoaded):
|
||||
@ -81,7 +84,7 @@ class TestFileCompletion:
|
||||
# We should now show / again, so tabbing twice gives us .. -> bar
|
||||
prompt.item_focus('next')
|
||||
prompt.item_focus('next')
|
||||
assert prompt._lineedit.text() == str(tmpdir / 'bar')
|
||||
assert prompt._lineedit.text() == str(testdir / 'bar')
|
||||
|
||||
@pytest.mark.linux
|
||||
def test_root_path(self, get_prompt):
|
||||
|
544
tests/unit/utils/test_urlmatch.py
Normal file
544
tests/unit/utils/test_urlmatch.py
Normal file
@ -0,0 +1,544 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Tests for qutebrowser.utils.urlmatch.
|
||||
|
||||
The tests are mostly inspired by Chromium's:
|
||||
https://cs.chromium.org/chromium/src/extensions/common/url_pattern_unittest.cc
|
||||
|
||||
Currently not tested:
|
||||
- The match_effective_tld attribute as it doesn't exist yet.
|
||||
- Nested filesystem:// URLs as we don't have those.
|
||||
- Unicode matching because QUrl doesn't like those URLs.
|
||||
- Any other features we don't need, such as .GetAsString() or set operations.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import string
|
||||
|
||||
import pytest
|
||||
import hypothesis
|
||||
import hypothesis.strategies as hst
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.utils import urlmatch
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, error', [
|
||||
# Chromium: PARSE_ERROR_MISSING_SCHEME_SEPARATOR
|
||||
("http", "No scheme given"),
|
||||
("http:", "Pattern without host"),
|
||||
("http:/", "Pattern without host"),
|
||||
("about://", "Pattern without path"),
|
||||
("http:/bar", "Pattern without host"),
|
||||
|
||||
# Chromium: PARSE_ERROR_EMPTY_HOST
|
||||
("http://", "Pattern without host"),
|
||||
("http:///", "Pattern without host"),
|
||||
("http:// /", "Pattern without host"),
|
||||
("http://:1234/", "Pattern without host"),
|
||||
|
||||
# Chromium: PARSE_ERROR_EMPTY_PATH
|
||||
# We deviate from Chromium and allow this for ease of use
|
||||
# ("http://bar", "..."),
|
||||
|
||||
# Chromium: PARSE_ERROR_INVALID_HOST
|
||||
("http://\0www/", "May not contain NUL byte"),
|
||||
|
||||
# Chromium: PARSE_ERROR_INVALID_HOST_WILDCARD
|
||||
("http://*foo/bar", "Invalid host wildcard"),
|
||||
("http://foo.*.bar/baz", "Invalid host wildcard"),
|
||||
("http://fo.*.ba:123/baz", "Invalid host wildcard"),
|
||||
("http://foo.*/bar", "TLD wildcards are not implemented yet"),
|
||||
|
||||
# Chromium: PARSE_ERROR_INVALID_PORT
|
||||
("http://foo:/", "Invalid port: Port is empty"),
|
||||
("http://*.foo:/", "Invalid port: Port is empty"),
|
||||
("http://foo:com/",
|
||||
"Invalid port: invalid literal for int() with base 10: 'com'"),
|
||||
pytest.param("http://foo:123456/",
|
||||
"Invalid port: Port out of range 0-65535",
|
||||
marks=pytest.mark.skipif(
|
||||
sys.hexversion < 0x03060000,
|
||||
reason="Doesn't show an error on Python 3.5")),
|
||||
("http://foo:80:80/monkey",
|
||||
"Invalid port: invalid literal for int() with base 10: '80:80'"),
|
||||
("chrome://foo:1234/bar", "Ports are unsupported with chrome scheme"),
|
||||
|
||||
# Additional tests
|
||||
("http://[", "Invalid IPv6 URL"),
|
||||
])
|
||||
def test_invalid_patterns(pattern, error):
|
||||
with pytest.raises(urlmatch.ParseError, match=re.escape(error)):
|
||||
urlmatch.UrlPattern(pattern)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, port', [
|
||||
("http://foo:1234/", 1234),
|
||||
("http://foo:1234/bar", 1234),
|
||||
("http://*.foo:1234/", 1234),
|
||||
("http://*.foo:1234/bar", 1234),
|
||||
("http://*:1234/", 1234),
|
||||
("http://*:*/", None),
|
||||
("http://foo:*/", None),
|
||||
("file://foo:1234/bar", None),
|
||||
|
||||
# Port-like strings in the path should not trigger a warning.
|
||||
("http://*/:1234", None),
|
||||
("http://*.foo/bar:1234", None),
|
||||
("http://foo/bar:1234/path", None),
|
||||
# We don't implement ALLOW_WILDCARD_FOR_EFFECTIVE_TLD yet.
|
||||
# ("http://*.foo.*/:1234", None),
|
||||
])
|
||||
def test_port(pattern, port):
|
||||
up = urlmatch.UrlPattern(pattern)
|
||||
assert up._port == port
|
||||
|
||||
|
||||
class TestMatchAllPagesForGivenScheme:
|
||||
|
||||
@pytest.fixture
|
||||
def up(self):
|
||||
return urlmatch.UrlPattern("http://*/*")
|
||||
|
||||
def test_attrs(self, up):
|
||||
assert up._scheme == 'http'
|
||||
assert up._host is None
|
||||
assert up._match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path is None
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
("http://google.com", True),
|
||||
("http://yahoo.com", True),
|
||||
("http://google.com/foo", True),
|
||||
("https://google.com", False),
|
||||
("http://74.125.127.100/search", True),
|
||||
])
|
||||
def test_urls(self, up, url, expected):
|
||||
assert up.matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
class TestMatchAllDomains:
|
||||
|
||||
@pytest.fixture
|
||||
def up(self):
|
||||
return urlmatch.UrlPattern("https://*/foo*")
|
||||
|
||||
def test_attrs(self, up):
|
||||
assert up._scheme == 'https'
|
||||
assert up._host is None
|
||||
assert up._match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path == '/foo*'
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
("https://google.com/foo", True),
|
||||
("https://google.com/foobar", True),
|
||||
("http://google.com/foo", False),
|
||||
("https://google.com/", False),
|
||||
])
|
||||
def test_urls(self, up, url, expected):
|
||||
assert up.matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
class TestMatchSubdomains:
|
||||
|
||||
@pytest.fixture
|
||||
def up(self):
|
||||
return urlmatch.UrlPattern("http://*.google.com/foo*bar")
|
||||
|
||||
def test_attrs(self, up):
|
||||
assert up._scheme == 'http'
|
||||
assert up._host == 'google.com'
|
||||
assert up._match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path == '/foo*bar'
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
("http://google.com/foobar", True),
|
||||
# FIXME The ?bar seems to be treated as path by GURL but as query by
|
||||
# QUrl.
|
||||
# ("http://www.google.com/foo?bar", True),
|
||||
("http://monkey.images.google.com/foooobar", True),
|
||||
("http://yahoo.com/foobar", False),
|
||||
])
|
||||
def test_urls(self, up, url, expected):
|
||||
assert up.matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
class TestMatchGlobEscaping:
|
||||
|
||||
@pytest.fixture
|
||||
def up(self):
|
||||
return urlmatch.UrlPattern(r"file:///foo-bar\*baz")
|
||||
|
||||
def test_attrs(self, up):
|
||||
assert up._scheme == 'file'
|
||||
assert up._host is None
|
||||
assert not up._match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path == r'/foo-bar\*baz'
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
# We use - instead of ? so it doesn't get treated as query
|
||||
(r"file:///foo-bar\hellobaz", True),
|
||||
(r"file:///fooXbar\hellobaz", False),
|
||||
])
|
||||
def test_urls(self, up, url, expected):
|
||||
assert up.matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
class TestMatchIpAddresses:
|
||||
|
||||
@pytest.mark.parametrize('pattern, host, match_subdomains', [
|
||||
("http://127.0.0.1/*", "127.0.0.1", False),
|
||||
("http://*.0.0.1/*", "0.0.1", True),
|
||||
])
|
||||
def test_attrs(self, pattern, host, match_subdomains):
|
||||
up = urlmatch.UrlPattern(pattern)
|
||||
assert up._scheme == 'http'
|
||||
assert up._host == host
|
||||
assert up._match_subdomains == match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path is None
|
||||
|
||||
@pytest.mark.parametrize('pattern, expected', [
|
||||
("http://127.0.0.1/*", True),
|
||||
# No subdomain matching is done with IPs
|
||||
("http://*.0.0.1/*", False),
|
||||
])
|
||||
def test_urls(self, pattern, expected):
|
||||
up = urlmatch.UrlPattern(pattern)
|
||||
assert up.matches(QUrl("http://127.0.0.1")) == expected
|
||||
|
||||
|
||||
class TestMatchChromeUrls:
|
||||
|
||||
@pytest.fixture
|
||||
def up(self):
|
||||
return urlmatch.UrlPattern("chrome://favicon/*")
|
||||
|
||||
def test_attrs(self, up):
|
||||
assert up._scheme == 'chrome'
|
||||
assert up._host == 'favicon'
|
||||
assert not up._match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path is None
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
("chrome://favicon/http://google.com", True),
|
||||
("chrome://favicon/https://google.com", True),
|
||||
("chrome://history", False),
|
||||
])
|
||||
def test_urls(self, up, url, expected):
|
||||
assert up.matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
class TestMatchAnything:
|
||||
|
||||
@pytest.fixture(params=['*://*/*', '*://*:*/*', '<all_urls>'])
|
||||
def up(self, request):
|
||||
return urlmatch.UrlPattern(request.param)
|
||||
|
||||
def test_attrs_common(self, up):
|
||||
assert up._scheme is None
|
||||
assert up._host is None
|
||||
assert up._path is None
|
||||
|
||||
def test_attrs_wildcard(self):
|
||||
up = urlmatch.UrlPattern('*://*/*')
|
||||
assert up._match_subdomains
|
||||
assert not up._match_all
|
||||
|
||||
def test_attrs_all(self):
|
||||
up = urlmatch.UrlPattern('<all_urls>')
|
||||
assert not up._match_subdomains
|
||||
assert up._match_all
|
||||
|
||||
@pytest.mark.parametrize('url', [
|
||||
"http://127.0.0.1",
|
||||
# We deviate from Chromium as we allow other schemes as well
|
||||
"chrome://favicon/http://google.com",
|
||||
"file:///foo/bar",
|
||||
"file://localhost/foo/bar",
|
||||
"qute://version",
|
||||
"about:blank",
|
||||
"data:text/html;charset=utf-8,<html>asdf</html>",
|
||||
])
|
||||
def test_urls(self, up, url):
|
||||
assert up.matches(QUrl(url))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, url, expected', [
|
||||
("about:*", "about:blank", True),
|
||||
("about:blank", "about:blank", True),
|
||||
("about:*", "about:version", True),
|
||||
("data:*", "data:monkey", True),
|
||||
("javascript:*", "javascript:atemyhomework", True),
|
||||
("data:*", "about:blank", False),
|
||||
])
|
||||
def test_special_schemes(pattern, url, expected):
|
||||
assert urlmatch.UrlPattern(pattern).matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
class TestFileScheme:
|
||||
|
||||
@pytest.fixture(params=[
|
||||
'file:///foo*',
|
||||
'file://foo*',
|
||||
# FIXME This doesn't pass all tests
|
||||
pytest.param('file://localhost/foo*', marks=pytest.mark.skip(
|
||||
reason="We're not handling this correctly in all cases"))
|
||||
])
|
||||
def up(self, request):
|
||||
return urlmatch.UrlPattern(request.param)
|
||||
|
||||
def test_attrs(self, up):
|
||||
assert up._scheme == 'file'
|
||||
assert up._host is None
|
||||
assert not up._match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path == '/foo*'
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
("file://foo", False),
|
||||
("file://foobar", False),
|
||||
("file:///foo", True),
|
||||
("file:///foobar", True),
|
||||
("file://localhost/foo", True),
|
||||
])
|
||||
def test_urls(self, up, url, expected):
|
||||
assert up.matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
class TestMatchSpecificPort:
|
||||
|
||||
@pytest.fixture
|
||||
def up(self):
|
||||
return urlmatch.UrlPattern("http://www.example.com:80/foo")
|
||||
|
||||
def test_attrs(self, up):
|
||||
assert up._scheme == 'http'
|
||||
assert up._host == 'www.example.com'
|
||||
assert not up._match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path == '/foo'
|
||||
assert up._port == 80
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
("http://www.example.com:80/foo", True),
|
||||
("http://www.example.com/foo", True),
|
||||
("http://www.example.com:8080/foo", False),
|
||||
])
|
||||
def test_urls(self, up, url, expected):
|
||||
assert up.matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
class TestExplicitPortWildcard:
|
||||
|
||||
@pytest.fixture
|
||||
def up(self):
|
||||
return urlmatch.UrlPattern("http://www.example.com:*/foo")
|
||||
|
||||
def test_attrs(self, up):
|
||||
assert up._scheme == 'http'
|
||||
assert up._host == 'www.example.com'
|
||||
assert not up._match_subdomains
|
||||
assert not up._match_all
|
||||
assert up._path == '/foo'
|
||||
assert up._port is None
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
("http://www.example.com:80/foo", True),
|
||||
("http://www.example.com/foo", True),
|
||||
("http://www.example.com:8080/foo", True),
|
||||
])
|
||||
def test_urls(self, up, url, expected):
|
||||
assert up.matches(QUrl(url)) == expected
|
||||
|
||||
|
||||
def test_ignore_missing_slashes():
|
||||
pattern1 = urlmatch.UrlPattern("http://www.example.com/example")
|
||||
pattern2 = urlmatch.UrlPattern("http://www.example.com/example/*")
|
||||
url1 = QUrl('http://www.example.com/example')
|
||||
url2 = QUrl('http://www.example.com/example/')
|
||||
|
||||
# Same patterns should match same URLs.
|
||||
assert pattern1.matches(url1)
|
||||
assert pattern2.matches(url1)
|
||||
# The not terminated path should match the terminated pattern.
|
||||
assert pattern2.matches(url1)
|
||||
# The terminated path however should not match the unterminated pattern.
|
||||
assert not pattern1.matches(url2)
|
||||
|
||||
|
||||
def test_trailing_slash():
|
||||
"""Contrary to Chromium, we allow to leave off a trailing slash."""
|
||||
url = QUrl('http://www.example.com/')
|
||||
pattern = urlmatch.UrlPattern('http://www.example.com')
|
||||
assert pattern.matches(url)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern', ['*://example.com/*',
|
||||
'*://example.com./*'])
|
||||
@pytest.mark.parametrize('url', ['http://example.com/',
|
||||
'http://example.com./'])
|
||||
def test_trailing_dot_domain(pattern, url):
|
||||
"""Both patterns should match trailing dot and non trailing dot domains.
|
||||
|
||||
More information about this not obvious behavior can be found in [1].
|
||||
|
||||
RFC 1738 [2] specifies clearly that the <host> part of a URL is supposed to
|
||||
contain a fully qualified domain name:
|
||||
|
||||
3.1. Common Internet Scheme Syntax
|
||||
//<user>:<password>@<host>:<port>/<url-path>
|
||||
|
||||
host
|
||||
The fully qualified domain name of a network host
|
||||
|
||||
[1] http://www.dns-sd.org./TrailingDotsInDomainNames.html
|
||||
[2] http://www.ietf.org/rfc/rfc1738.txt
|
||||
"""
|
||||
assert urlmatch.UrlPattern(pattern).matches(QUrl(url))
|
||||
|
||||
|
||||
def test_urlpattern_benchmark(benchmark):
|
||||
url = QUrl('https://www.example.com/barfoobar')
|
||||
|
||||
def run():
|
||||
up = urlmatch.UrlPattern('https://*.example.com/*foo*')
|
||||
up.matches(url)
|
||||
|
||||
benchmark(run)
|
||||
|
||||
|
||||
URL_TEXT = hst.text(alphabet=string.ascii_letters)
|
||||
|
||||
|
||||
@hypothesis.given(pattern=hst.builds(
|
||||
lambda *a: ''.join(a),
|
||||
# Scheme
|
||||
hst.one_of(hst.just('*'), hst.just('http'), hst.just('file')),
|
||||
# Separator
|
||||
hst.one_of(hst.just(':'), hst.just('://')),
|
||||
# Host
|
||||
hst.one_of(hst.just('*'),
|
||||
hst.builds(lambda *a: ''.join(a), hst.just('*.'), URL_TEXT),
|
||||
URL_TEXT),
|
||||
# Port
|
||||
hst.one_of(hst.just(''),
|
||||
hst.builds(lambda *a: ''.join(a), hst.just(':'),
|
||||
hst.integers(min_value=0,
|
||||
max_value=65535).map(str))),
|
||||
# Path
|
||||
hst.one_of(hst.just(''),
|
||||
hst.builds(lambda *a: ''.join(a), hst.just('/'), URL_TEXT))
|
||||
))
|
||||
def test_urlpattern_hypothesis(pattern):
|
||||
try:
|
||||
up = urlmatch.UrlPattern(pattern)
|
||||
except urlmatch.ParseError:
|
||||
return
|
||||
up.matches(QUrl('https://www.example.com/'))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('text1, text2, equal', [
|
||||
# schemes
|
||||
("http://en.google.com/blah/*/foo",
|
||||
"https://en.google.com/blah/*/foo",
|
||||
False),
|
||||
("https://en.google.com/blah/*/foo",
|
||||
"https://en.google.com/blah/*/foo",
|
||||
True),
|
||||
("https://en.google.com/blah/*/foo",
|
||||
"ftp://en.google.com/blah/*/foo",
|
||||
False),
|
||||
|
||||
# subdomains
|
||||
("https://en.google.com/blah/*/foo",
|
||||
"https://fr.google.com/blah/*/foo",
|
||||
False),
|
||||
("https://www.google.com/blah/*/foo",
|
||||
"https://*.google.com/blah/*/foo",
|
||||
False),
|
||||
("https://*.google.com/blah/*/foo",
|
||||
"https://*.google.com/blah/*/foo",
|
||||
True),
|
||||
|
||||
# domains
|
||||
("http://en.example.com/blah/*/foo",
|
||||
"http://en.google.com/blah/*/foo",
|
||||
False),
|
||||
|
||||
# ports
|
||||
("http://en.google.com:8000/blah/*/foo",
|
||||
"http://en.google.com/blah/*/foo",
|
||||
False),
|
||||
("http://fr.google.com:8000/blah/*/foo",
|
||||
"http://fr.google.com:8000/blah/*/foo",
|
||||
True),
|
||||
("http://en.google.com:8000/blah/*/foo",
|
||||
"http://en.google.com:8080/blah/*/foo",
|
||||
False),
|
||||
|
||||
# paths
|
||||
("http://en.google.com/blah/*/foo",
|
||||
"http://en.google.com/blah/*",
|
||||
False),
|
||||
("http://en.google.com/*",
|
||||
"http://en.google.com/",
|
||||
False),
|
||||
("http://en.google.com/*",
|
||||
"http://en.google.com/*",
|
||||
True),
|
||||
|
||||
# all_urls
|
||||
("<all_urls>",
|
||||
"<all_urls>",
|
||||
True),
|
||||
("<all_urls>",
|
||||
"http://*/*",
|
||||
False)
|
||||
])
|
||||
def test_equal(text1, text2, equal):
|
||||
pat1 = urlmatch.UrlPattern(text1)
|
||||
pat2 = urlmatch.UrlPattern(text2)
|
||||
|
||||
assert (pat1 == pat2) == equal
|
||||
assert (hash(pat1) == hash(pat2)) == equal
|
||||
|
||||
|
||||
def test_equal_string():
|
||||
assert urlmatch.UrlPattern("<all_urls>") != '<all_urls>'
|
||||
|
||||
|
||||
def test_repr():
|
||||
pat = urlmatch.UrlPattern('https://www.example.com/')
|
||||
expected = ("qutebrowser.utils.urlmatch.UrlPattern("
|
||||
"pattern='https://www.example.com/')")
|
||||
assert repr(pat) == expected
|
||||
|
||||
|
||||
def test_str():
|
||||
text = 'https://www.example.com/'
|
||||
pat = urlmatch.UrlPattern(text)
|
||||
assert str(pat) == text
|
Loading…
Reference in New Issue
Block a user