Merge branch 'master' into relapaths
Sync with upstream/master before creating a pull request
This commit is contained in:
commit
f326fa28a6
@ -3,6 +3,7 @@ branch = true
|
||||
omit =
|
||||
qutebrowser/__main__.py
|
||||
*/__init__.py
|
||||
qutebrowser/resources.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
|
@ -32,6 +32,7 @@ Added
|
||||
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
|
||||
- New argument `--no-err-windows` to suppress all error windows.
|
||||
- New visual/caret mode (bound to `v`) to select text by keyboard.
|
||||
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@ -73,6 +74,7 @@ Fixed
|
||||
- Various fixes for deprecated key bindings and auto-migrations.
|
||||
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug)
|
||||
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
||||
-----------------------------------------------------------------------
|
||||
|
@ -150,6 +150,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Error 800
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
* Tobias Patzl
|
||||
* Johannes Altmanninger
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
|
@ -111,6 +111,7 @@
|
||||
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|
||||
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|
||||
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|
||||
|<<tabs-mousewheel-tab-switching,mousewheel-tab-switching>>|Switch between tabs using the mouse wheel.
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``storage''
|
||||
@ -1031,6 +1032,17 @@ The format to use for the tab title. The following placeholders are defined:
|
||||
|
||||
Default: +pass:[{index}: {title}]+
|
||||
|
||||
[[tabs-mousewheel-tab-switching]]
|
||||
=== mousewheel-tab-switching
|
||||
Switch between tabs using the mouse wheel.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
== storage
|
||||
Settings related to cache and storage.
|
||||
|
||||
|
@ -11,6 +11,7 @@ What to do now
|
||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
to make yourself familiar with the key bindings: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||
* If you just cloned the repository, you'll need to run
|
||||
`scripts/asciidoc2html.py` to generate the documentation.
|
||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
|
||||
|
@ -686,8 +686,11 @@ class DownloadManager(QAbstractListModel):
|
||||
if fileobj is not None or filename is not None:
|
||||
return self.fetch_request(request, page, fileobj, filename,
|
||||
auto_remove, suggested_fn)
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
else:
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||
q = self._prepare_question()
|
||||
q.default = _path_suggestion(suggested_fn)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
|
@ -522,6 +522,10 @@ def data(readonly=False):
|
||||
"* `{index}`: The index of this tab.\n"
|
||||
"* `{id}`: The internal tab ID of this tab."),
|
||||
|
||||
('mousewheel-tab-switching',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Switch between tabs using the mouse wheel."),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
|
@ -577,3 +577,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"""
|
||||
super().resizeEvent(e)
|
||||
self.resized.emit(self.geometry())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Override wheelEvent of QWidget to forward it to the focused tab.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent
|
||||
"""
|
||||
if self._now_focused is not None:
|
||||
self._now_focused.wheelEvent(e)
|
||||
else:
|
||||
e.ignore()
|
||||
|
@ -480,6 +480,19 @@ class TabBar(QTabBar):
|
||||
new_idx = super().insertTab(idx, icon, '')
|
||||
self.set_page_title(new_idx, text)
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Override wheelEvent to make the action configurable.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent
|
||||
"""
|
||||
if config.get('tabs', 'mousewheel-tab-switching'):
|
||||
super().wheelEvent(e)
|
||||
else:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tabbed_browser.wheelEvent(e)
|
||||
|
||||
|
||||
class TabBarStyle(QCommonStyle):
|
||||
|
||||
|
@ -131,7 +131,7 @@ def ensure_valid(obj):
|
||||
def ensure_not_null(obj):
|
||||
"""Ensure a Qt object with an .isNull() method is not null."""
|
||||
if obj.isNull():
|
||||
raise QtValueError(obj)
|
||||
raise QtValueError(obj, null=True)
|
||||
|
||||
|
||||
def check_qdatastream(stream):
|
||||
@ -180,7 +180,7 @@ def deserialize_stream(stream, obj):
|
||||
def savefile_open(filename, binary=False, encoding='utf-8'):
|
||||
"""Context manager to easily use a QSaveFile."""
|
||||
f = QSaveFile(filename)
|
||||
new_f = None
|
||||
cancelled = False
|
||||
try:
|
||||
ok = f.open(QIODevice.WriteOnly)
|
||||
if not ok:
|
||||
@ -192,13 +192,14 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
|
||||
yield new_f
|
||||
except:
|
||||
f.cancelWriting()
|
||||
cancelled = True
|
||||
raise
|
||||
else:
|
||||
new_f.flush()
|
||||
finally:
|
||||
if new_f is not None:
|
||||
new_f.flush()
|
||||
commit_ok = f.commit()
|
||||
if not commit_ok:
|
||||
raise OSError(f.errorString())
|
||||
if not commit_ok and not cancelled:
|
||||
raise OSError("Commit failed!")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@ -221,27 +222,58 @@ class PyQIODevice(io.BufferedIOBase):
|
||||
"""Wrapper for a QIODevice which provides a python interface.
|
||||
|
||||
Attributes:
|
||||
_dev: The underlying QIODevice.
|
||||
dev: The underlying QIODevice.
|
||||
"""
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
def __init__(self, dev):
|
||||
self._dev = dev
|
||||
self.dev = dev
|
||||
|
||||
def __len__(self):
|
||||
return self._dev.size()
|
||||
return self.dev.size()
|
||||
|
||||
def _check_open(self):
|
||||
"""Check if the device is open, raise OSError if not."""
|
||||
if not self._dev.isOpen():
|
||||
raise OSError("IO operation on closed device!")
|
||||
"""Check if the device is open, raise ValueError if not."""
|
||||
if not self.dev.isOpen():
|
||||
raise ValueError("IO operation on closed device!")
|
||||
|
||||
def _check_random(self):
|
||||
"""Check if the device supports random access, raise OSError if not."""
|
||||
if not self.seekable():
|
||||
raise OSError("Random access not allowed!")
|
||||
|
||||
def _check_readable(self):
|
||||
"""Check if the device is readable, raise OSError if not."""
|
||||
if not self.dev.isReadable():
|
||||
raise OSError("Trying to read unreadable file!")
|
||||
|
||||
def _check_writable(self):
|
||||
"""Check if the device is writable, raise OSError if not."""
|
||||
if not self.writable():
|
||||
raise OSError("Trying to write to unwritable file!")
|
||||
|
||||
def open(self, mode):
|
||||
"""Open the underlying device and ensure opening succeeded.
|
||||
|
||||
Raises OSError if opening failed.
|
||||
|
||||
Args:
|
||||
mode: QIODevice::OpenMode flags.
|
||||
|
||||
Return:
|
||||
A contextlib.closing() object so this can be used as
|
||||
contextmanager.
|
||||
"""
|
||||
ok = self.dev.open(mode)
|
||||
if not ok:
|
||||
raise OSError(self.dev.errorString())
|
||||
return contextlib.closing(self)
|
||||
|
||||
def close(self):
|
||||
"""Close the underlying device."""
|
||||
self.dev.close()
|
||||
|
||||
def fileno(self):
|
||||
raise io.UnsupportedOperation
|
||||
|
||||
@ -249,85 +281,102 @@ class PyQIODevice(io.BufferedIOBase):
|
||||
self._check_open()
|
||||
self._check_random()
|
||||
if whence == io.SEEK_SET:
|
||||
ok = self._dev.seek(offset)
|
||||
ok = self.dev.seek(offset)
|
||||
elif whence == io.SEEK_CUR:
|
||||
ok = self._dev.seek(self.tell() + offset)
|
||||
ok = self.dev.seek(self.tell() + offset)
|
||||
elif whence == io.SEEK_END:
|
||||
ok = self._dev.seek(len(self) + offset)
|
||||
ok = self.dev.seek(len(self) + offset)
|
||||
else:
|
||||
raise io.UnsupportedOperation("whence = {} is not "
|
||||
"supported!".format(whence))
|
||||
if not ok:
|
||||
raise OSError(self._dev.errorString())
|
||||
raise OSError("seek failed!")
|
||||
|
||||
def truncate(self, size=None): # pylint: disable=unused-argument
|
||||
raise io.UnsupportedOperation
|
||||
|
||||
def close(self):
|
||||
self._dev.close()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return not self._dev.isOpen()
|
||||
return not self.dev.isOpen()
|
||||
|
||||
def flush(self):
|
||||
self._check_open()
|
||||
self._dev.waitForBytesWritten(-1)
|
||||
self.dev.waitForBytesWritten(-1)
|
||||
|
||||
def isatty(self):
|
||||
self._check_open()
|
||||
return False
|
||||
|
||||
def readable(self):
|
||||
return self._dev.isReadable()
|
||||
return self.dev.isReadable()
|
||||
|
||||
def readline(self, size=-1):
|
||||
self._check_open()
|
||||
if size == -1:
|
||||
size = 0
|
||||
return self._dev.readLine(size)
|
||||
self._check_readable()
|
||||
|
||||
if size < 0:
|
||||
qt_size = 0 # no maximum size
|
||||
elif size == 0:
|
||||
return QByteArray()
|
||||
else:
|
||||
qt_size = size + 1 # Qt also counts the NUL byte
|
||||
|
||||
if self.dev.canReadLine():
|
||||
buf = self.dev.readLine(qt_size)
|
||||
else:
|
||||
if size < 0:
|
||||
buf = self.dev.readAll()
|
||||
else:
|
||||
buf = self.dev.read(size)
|
||||
|
||||
if buf is None:
|
||||
raise OSError(self.dev.errorString())
|
||||
return buf
|
||||
|
||||
def seekable(self):
|
||||
return not self._dev.isSequential()
|
||||
return not self.dev.isSequential()
|
||||
|
||||
def tell(self):
|
||||
self._check_open()
|
||||
self._check_random()
|
||||
return self._dev.pos()
|
||||
return self.dev.pos()
|
||||
|
||||
def writable(self):
|
||||
return self._dev.isWritable()
|
||||
|
||||
def readinto(self, b):
|
||||
self._check_open()
|
||||
return self._dev.read(b, len(b))
|
||||
return self.dev.isWritable()
|
||||
|
||||
def write(self, b):
|
||||
self._check_open()
|
||||
num = self._dev.write(b)
|
||||
self._check_writable()
|
||||
num = self.dev.write(b)
|
||||
if num == -1 or num < len(b):
|
||||
raise OSError(self._dev.errorString())
|
||||
raise OSError(self.dev.errorString())
|
||||
return num
|
||||
|
||||
def read(self, size):
|
||||
def read(self, size=-1):
|
||||
self._check_open()
|
||||
buf = bytes()
|
||||
num = self._dev.read(buf, size)
|
||||
if num == -1:
|
||||
raise OSError(self._dev.errorString())
|
||||
return num
|
||||
self._check_readable()
|
||||
if size < 0:
|
||||
buf = self.dev.readAll()
|
||||
else:
|
||||
buf = self.dev.read(size)
|
||||
if buf is None:
|
||||
raise OSError(self.dev.errorString())
|
||||
return buf
|
||||
|
||||
|
||||
class QtValueError(ValueError):
|
||||
|
||||
"""Exception which gets raised by ensure_valid."""
|
||||
|
||||
def __init__(self, obj):
|
||||
def __init__(self, obj, null=False):
|
||||
try:
|
||||
self.reason = obj.errorString()
|
||||
except AttributeError:
|
||||
self.reason = None
|
||||
err = "{} is not valid".format(obj)
|
||||
if null:
|
||||
err = "{} is null".format(obj)
|
||||
else:
|
||||
err = "{} is not valid".format(obj)
|
||||
if self.reason:
|
||||
err += ": {}".format(self.reason)
|
||||
super().__init__(err)
|
||||
|
@ -94,6 +94,8 @@ def _is_url_naive(urlstr):
|
||||
True if the URL really is a URL, False otherwise.
|
||||
"""
|
||||
url = qurl_from_user_input(urlstr)
|
||||
assert url.isValid()
|
||||
|
||||
if not utils.raises(ValueError, ipaddress.ip_address, urlstr):
|
||||
# Valid IPv4/IPv6 address
|
||||
return True
|
||||
@ -104,9 +106,7 @@ def _is_url_naive(urlstr):
|
||||
if not QHostAddress(urlstr).isNull():
|
||||
return False
|
||||
|
||||
if not url.isValid():
|
||||
return False
|
||||
elif '.' in url.host():
|
||||
if '.' in url.host():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@ -122,9 +122,7 @@ def _is_url_dns(urlstr):
|
||||
True if the URL really is a URL, False otherwise.
|
||||
"""
|
||||
url = qurl_from_user_input(urlstr)
|
||||
if not url.isValid():
|
||||
log.url.debug("Invalid URL -> False")
|
||||
return False
|
||||
assert url.isValid()
|
||||
|
||||
if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and
|
||||
not QHostAddress(urlstr).isNull()):
|
||||
@ -246,16 +244,13 @@ def is_url(urlstr):
|
||||
return False
|
||||
|
||||
if not qurl_userinput.isValid():
|
||||
# This will also catch URLs containing spaces.
|
||||
return False
|
||||
|
||||
if _has_explicit_scheme(qurl):
|
||||
# URLs with explicit schemes are always URLs
|
||||
log.url.debug("Contains explicit scheme")
|
||||
url = True
|
||||
elif ' ' in urlstr:
|
||||
# A URL will never contain a space
|
||||
log.url.debug("Contains space -> no URL")
|
||||
url = False
|
||||
elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'):
|
||||
log.url.debug("Is localhost.")
|
||||
url = True
|
||||
@ -274,7 +269,7 @@ def is_url(urlstr):
|
||||
else:
|
||||
raise ValueError("Invalid autosearch value")
|
||||
log.url.debug("url = {}".format(url))
|
||||
return url and qurl_userinput.isValid()
|
||||
return url
|
||||
|
||||
|
||||
def qurl_from_user_input(urlstr):
|
||||
|
@ -130,7 +130,7 @@ def _module_versions():
|
||||
try:
|
||||
import sipconfig # pylint: disable=import-error,unused-variable
|
||||
except ImportError:
|
||||
pass
|
||||
lines.append('SIP: ?')
|
||||
else:
|
||||
try:
|
||||
lines.append('SIP: {}'.format(
|
||||
|
@ -70,13 +70,16 @@ def link_pyqt(sys_path, venv_path):
|
||||
if not globbed_sip:
|
||||
raise Error("Did not find sip in {}!".format(sys_path))
|
||||
|
||||
files = ['PyQt5']
|
||||
files += [os.path.basename(e) for e in globbed_sip]
|
||||
for fn in files:
|
||||
files = [('PyQt5', True), ('sipconfig.py', False)]
|
||||
files += [(os.path.basename(e), True) for e in globbed_sip]
|
||||
for fn, required in files:
|
||||
source = os.path.join(sys_path, fn)
|
||||
dest = os.path.join(venv_path, fn)
|
||||
if not os.path.exists(source):
|
||||
raise FileNotFoundError(source)
|
||||
if required:
|
||||
raise FileNotFoundError(source)
|
||||
else:
|
||||
continue
|
||||
if os.path.exists(dest):
|
||||
if os.path.isdir(dest) and not os.path.islink(dest):
|
||||
shutil.rmtree(dest)
|
||||
|
@ -157,14 +157,6 @@ class TestConfigParser:
|
||||
self.cfg.get('general', 'bar') # pylint: disable=bad-config-call
|
||||
|
||||
|
||||
def keyconfig_deprecated_test_cases():
|
||||
"""Generator yielding test cases (command, rgx) for TestKeyConfigParser."""
|
||||
for sect in configdata.KEY_DATA.values():
|
||||
for command in sect:
|
||||
for rgx, _repl in configdata.CHANGED_KEY_COMMANDS:
|
||||
yield (command, rgx)
|
||||
|
||||
|
||||
class TestKeyConfigParser:
|
||||
|
||||
"""Test config.parsers.keyconf.KeyConfigParser."""
|
||||
@ -185,10 +177,13 @@ class TestKeyConfigParser:
|
||||
with pytest.raises(keyconf.KeyConfigError):
|
||||
kcp._read_command(cmdline_test.cmd)
|
||||
|
||||
@pytest.mark.parametrize('command, rgx', keyconfig_deprecated_test_cases())
|
||||
def test_default_config_no_deprecated(self, command, rgx):
|
||||
@pytest.mark.parametrize('rgx', [rgx for rgx, _repl
|
||||
in configdata.CHANGED_KEY_COMMANDS])
|
||||
def test_default_config_no_deprecated(self, rgx):
|
||||
"""Make sure the default config contains no deprecated commands."""
|
||||
assert rgx.match(command) is None
|
||||
for sect in configdata.KEY_DATA.values():
|
||||
for command in sect:
|
||||
assert rgx.match(command) is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'old, new_expected',
|
||||
|
@ -72,7 +72,7 @@ class LineParserWrapper:
|
||||
return True
|
||||
|
||||
|
||||
class TestableAppendLineParser(LineParserWrapper,
|
||||
class AppendLineParserTestable(LineParserWrapper,
|
||||
lineparsermod.AppendLineParser):
|
||||
|
||||
"""Wrapper over AppendLineParser to make it testable."""
|
||||
@ -80,14 +80,14 @@ class TestableAppendLineParser(LineParserWrapper,
|
||||
pass
|
||||
|
||||
|
||||
class TestableLineParser(LineParserWrapper, lineparsermod.LineParser):
|
||||
class LineParserTestable(LineParserWrapper, lineparsermod.LineParser):
|
||||
|
||||
"""Wrapper over LineParser to make it testable."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestableLimitLineParser(LineParserWrapper,
|
||||
class LimitLineParserTestable(LineParserWrapper,
|
||||
lineparsermod.LimitLineParser):
|
||||
|
||||
"""Wrapper over LimitLineParser to make it testable."""
|
||||
@ -137,7 +137,7 @@ class TestAppendLineParser:
|
||||
@pytest.fixture
|
||||
def lineparser(self):
|
||||
"""Fixture to get an AppendLineParser for tests."""
|
||||
lp = TestableAppendLineParser('this really', 'does not matter')
|
||||
lp = AppendLineParserTestable('this really', 'does not matter')
|
||||
lp.new_data = self.BASE_DATA
|
||||
lp.save()
|
||||
return lp
|
||||
@ -178,7 +178,7 @@ class TestAppendLineParser:
|
||||
|
||||
def test_get_recent_none(self):
|
||||
"""Test get_recent with no data."""
|
||||
linep = TestableAppendLineParser('this really', 'does not matter')
|
||||
linep = AppendLineParserTestable('this really', 'does not matter')
|
||||
assert linep.get_recent() == []
|
||||
|
||||
def test_get_recent_little(self, lineparser):
|
||||
|
9
tox.ini
9
tox.ini
@ -19,17 +19,18 @@ usedevelop = true
|
||||
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
|
||||
passenv = DISPLAY XAUTHORITY HOME
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
py==1.4.27
|
||||
pytest==2.7.1
|
||||
pytest-capturelog==0.7
|
||||
pytest-qt==1.3.0
|
||||
pytest-mock==0.5
|
||||
pytest-html==1.2
|
||||
pytest-html==1.3.1
|
||||
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
|
||||
# on Ubuntu Trusty.
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
{envpython} -m py.test --strict {posargs}
|
||||
{envpython} -m py.test --strict -rfEsw {posargs}
|
||||
|
||||
[testenv:coverage]
|
||||
passenv = DISPLAY XAUTHORITY HOME
|
||||
@ -40,7 +41,7 @@ deps =
|
||||
cov-core==1.15.0
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envpython} -m py.test --strict --cov qutebrowser --cov-report term --cov-report html {posargs}
|
||||
{envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html {posargs}
|
||||
|
||||
[testenv:misc]
|
||||
commands =
|
||||
@ -96,7 +97,7 @@ commands =
|
||||
[testenv:check-manifest]
|
||||
skip_install = true
|
||||
deps =
|
||||
check-manifest==0.24
|
||||
check-manifest==0.25
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
||||
|
Loading…
Reference in New Issue
Block a user