Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7762017f00 | ||
|
|
2ba40acf74 | ||
|
|
388c155ebb | ||
|
|
adf2f9860d | ||
|
|
f528a5dd21 | ||
|
|
f0f1a4a1d1 | ||
|
|
ae295a7f65 | ||
|
|
68794cc2e2 | ||
|
|
acdf0a1c60 | ||
|
|
626abd3c83 | ||
|
|
979b7cfaba | ||
|
|
2b6b4e82a7 | ||
|
|
80778a9ff3 | ||
|
|
6eb8284fe0 | ||
|
|
49bdcd5a97 | ||
|
|
106e591a36 | ||
|
|
a70f864ff5 | ||
|
|
4932cc4d24 | ||
|
|
2b5f133726 | ||
|
|
5436e27e41 | ||
|
|
4adf10a2f2 | ||
|
|
b80fa7a197 | ||
|
|
3cc790afb3 | ||
|
|
91aa9f6c0c | ||
|
|
d6cacdb42f | ||
|
|
19554ba4a1 | ||
|
|
d16d9e403a | ||
|
|
432d666d25 | ||
|
|
1c3ee0db20 | ||
|
|
bdc0c0ddc1 | ||
|
|
b9c8a79f10 | ||
|
|
801e9e0334 | ||
|
|
cfa5ee2835 | ||
|
|
9c6437b3b9 | ||
|
|
486488e2cd | ||
|
|
92aedf84f5 | ||
|
|
6825dfb8d8 | ||
|
|
d6c6014b85 | ||
|
|
c1ac1d702f | ||
|
|
9e50b7afcc | ||
|
|
1388880e7b | ||
|
|
30d60ea740 | ||
|
|
28cac01a1f | ||
|
|
1689cb09f8 | ||
|
|
286c71a48a | ||
|
|
c073234a8d | ||
|
|
8c286412cb | ||
|
|
b3cef948b0 | ||
|
|
c94ea5f8d4 | ||
|
|
e2d249541d | ||
|
|
2de6428830 | ||
|
|
c2472d88f1 | ||
|
|
1d2dd5bf55 | ||
|
|
cbb246fd0b | ||
|
|
646e92707a | ||
|
|
c5334fb683 | ||
|
|
76dbfa7305 | ||
|
|
9ad6cef369 | ||
|
|
423192e9c9 | ||
|
|
d606cd5550 | ||
|
|
7c1de99876 | ||
|
|
b873cfb18a | ||
|
|
a1776087e0 | ||
|
|
948866f4f2 | ||
|
|
a9a7f5da45 | ||
|
|
b169a1c802 | ||
|
|
f9e702bae5 | ||
|
|
29eadf7141 |
2
.flake8
2
.flake8
@@ -32,7 +32,7 @@ exclude = .*,__pycache__,resources.py
|
||||
# D403: First word of the first line should be properly capitalized
|
||||
# (false-positives)
|
||||
# D413: Missing blank line after last section (not in pep257?)
|
||||
# A003: Builtin name for class attribute (needed for attrs)
|
||||
# A003: Builtin name for class attribute (needed for overridden methods)
|
||||
ignore =
|
||||
B001,B008,B305,
|
||||
E128,E226,E265,E501,E402,E266,E722,E731,
|
||||
|
||||
@@ -15,8 +15,8 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v1.3.0 (unreleased)
|
||||
-------------------
|
||||
v1.3.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
@@ -25,7 +25,9 @@ Added
|
||||
- New `url.open_base_url` option to open the base URL of a searchengine when no
|
||||
search term is given.
|
||||
- New `tabs.min_width` setting to configure the minimal width for tabs.
|
||||
- New `getbib` userscript to download bibtex information for DOIs on a page.
|
||||
- New userscripts:
|
||||
* `getbib` to download bibtex information for DOIs on a page.
|
||||
* `qute-keepass` to get passwords from KeePassX.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@@ -52,7 +54,6 @@ Changed
|
||||
- Error messages when trying to wrap when `tabs.wrap` is `False` are now logged
|
||||
to debug instead of messages.
|
||||
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
@@ -81,7 +82,18 @@ Fixed
|
||||
- The Makefile (intended for packagers) now supports `PREFIX` properly.
|
||||
- The workaround for a black window with Nvidia graphics is now enabled on
|
||||
non-Linux systems (like FreeBSD) as well.
|
||||
- Initial support for Qt 5.11
|
||||
- Initial support for Qt 5.11.
|
||||
- Checking for a new version after sending a crash report now works properly
|
||||
again.
|
||||
- `@match` in Greasemonkey scripts now more closely matches the proper pattern
|
||||
syntax.
|
||||
- Searching via `/` or `?` now doesn't handle any characters in a special way.
|
||||
- Fixed crash when trying to retry some failed downloads on QtWebEngine.
|
||||
- An invalid spellcheck dictionary filename now doesn't crash anymore.
|
||||
- When no spellcheck dictionaries are configured, it's now disabled internally.
|
||||
This works around an issue with entering special characters on Facebook
|
||||
messenger.
|
||||
- The macOS release now should work again on macOS 10.11 and newer.
|
||||
|
||||
v1.2.1
|
||||
------
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
check-manifest==0.36
|
||||
check-manifest==0.37
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
codecov==2.0.15
|
||||
coverage==4.5.1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
attrs==17.4.0
|
||||
flake8==3.5.0
|
||||
flake8-bugbear==18.2.0
|
||||
flake8-builtins==1.2.2
|
||||
flake8-builtins==1.3.1
|
||||
flake8-comprehensions==1.4.1
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==3.1.0
|
||||
@@ -19,7 +19,7 @@ flake8-tuple==0.2.13
|
||||
mccabe==0.6.1
|
||||
pathmatch==0.2.1
|
||||
pep8-naming==0.5.0
|
||||
pycodestyle==2.3.1
|
||||
pycodestyle==2.3.1 # rq.filter: < 2.4.0
|
||||
pydocstyle==2.1.1
|
||||
pyflakes==1.6.0
|
||||
six==1.11.0
|
||||
|
||||
@@ -15,3 +15,6 @@ flake8-tuple
|
||||
pep8-naming
|
||||
pydocstyle
|
||||
pyflakes
|
||||
|
||||
# https://github.com/PyCQA/pycodestyle/issues/741
|
||||
#@ filter: pycodestyle < 2.4.0
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==17.1
|
||||
pyparsing==2.2.0
|
||||
setuptools==39.0.1
|
||||
setuptools==39.1.0
|
||||
six==1.11.0
|
||||
wheel==0.31.0
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==1.0.2
|
||||
github3.py==1.1.0
|
||||
idna==2.6
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==1.6.3
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==1.0.2
|
||||
github3.py==1.1.0
|
||||
idna==2.6
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
attrs==17.4.0
|
||||
beautifulsoup4==4.6.0
|
||||
cheroot==6.1.2
|
||||
cheroot==6.2.4
|
||||
click==6.7
|
||||
# colorama==0.3.9
|
||||
coverage==4.5.1
|
||||
EasyProcess==0.2.3
|
||||
fields==5.0.0
|
||||
Flask==0.12.2
|
||||
Flask==1.0.1
|
||||
glob2==0.6
|
||||
hunter==2.0.2
|
||||
hypothesis==3.55.1
|
||||
hypothesis==3.56.5
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
@@ -22,13 +22,13 @@ parse-type==0.4.2
|
||||
pluggy==0.6.0
|
||||
py==1.5.3
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==3.5.0
|
||||
pytest==3.5.1
|
||||
pytest-bdd==2.21.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.5.1
|
||||
pytest-faulthandler==1.5.0
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.8.0
|
||||
pytest-mock==1.9.0
|
||||
pytest-qt==2.3.1
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==4.0
|
||||
|
||||
261
misc/userscripts/qute-keepass
Executable file
261
misc/userscripts/qute-keepass
Executable file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2018 Jay Kamat <jaygkamat@gmail.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""This userscript allows for insertion of usernames and passwords from keepass
|
||||
databases using pykeepass. Since it is a userscript, it must be run from
|
||||
qutebrowser.
|
||||
|
||||
A sample invocation of this script is:
|
||||
|
||||
:spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
|
||||
|
||||
And a sample binding
|
||||
|
||||
:bind --mode=insert <ctrl-i> spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
|
||||
|
||||
-p or --path is a required argument.
|
||||
|
||||
--keyfile-path allows you to specify a keepass keyfile. If you only use a
|
||||
keyfile, also add --no-password as well. Specifying --no-password without
|
||||
--keyfile-path will lead to an error.
|
||||
|
||||
login information is inserted using :insert-text and :fake-key <Tab>, which
|
||||
means you must have a cursor in position before initiating this userscript. If
|
||||
you do not do this, you will get 'element not editable' errors.
|
||||
|
||||
If keepass takes a while to open the DB, you might want to consider reducing
|
||||
the number of transform rounds in your database settings.
|
||||
|
||||
Dependencies: pykeepass (in python3), PyQt5. Without pykeepass, you will get an
|
||||
exit code of 100.
|
||||
|
||||
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
|
||||
|
||||
WARNING: The login details are viewable as plaintext in qutebrowser's debug log
|
||||
(qute://log) and could be compromised if you decide to submit a crash report!
|
||||
|
||||
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=bad-builtin
|
||||
|
||||
import argparse
|
||||
import enum
|
||||
import functools
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit
|
||||
|
||||
try:
|
||||
import pykeepass
|
||||
except ImportError as e:
|
||||
print("pykeepass not found: {}".format(str(e)), file=sys.stderr)
|
||||
|
||||
# Since this is a common error, try to print it to the FIFO if we can.
|
||||
if 'QUTE_FIFO' in os.environ:
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write('message-error "pykeepass failed to be imported."\n')
|
||||
fifo.flush()
|
||||
sys.exit(100)
|
||||
|
||||
argument_parser = argparse.ArgumentParser(
|
||||
description="Fill passwords using keepass.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__)
|
||||
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
|
||||
argument_parser.add_argument('--path', '-p', required=True,
|
||||
help='Path to the keepass db.')
|
||||
argument_parser.add_argument('--keyfile-path', '-k', default=None,
|
||||
help='Path to a keepass keyfile')
|
||||
argument_parser.add_argument(
|
||||
'--no-password', action='store_true',
|
||||
help='Supply if no password is required to unlock this database. '
|
||||
'Only allowed with --keyfile-path')
|
||||
argument_parser.add_argument(
|
||||
'--dmenu-invocation', '-d', default='dmenu',
|
||||
help='Invocation used to execute a dmenu-provider')
|
||||
argument_parser.add_argument(
|
||||
'--dmenu-format', '-f', default='{title}: {username}',
|
||||
help='Format string for keys to display in dmenu.'
|
||||
' Must generate a unique string.')
|
||||
argument_parser.add_argument(
|
||||
'--no-insert-mode', '-n', dest='insert_mode', action='store_false',
|
||||
help="Don't automatically enter insert mode")
|
||||
argument_parser.add_argument(
|
||||
'--io-encoding', '-i', default='UTF-8',
|
||||
help='Encoding used to communicate with subprocesses')
|
||||
group = argument_parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--username-fill-only', '-e',
|
||||
action='store_true', help='Only insert username')
|
||||
group.add_argument('--password-fill-only', '-w',
|
||||
action='store_true', help='Only insert password')
|
||||
|
||||
CMD_DELAY = 50
|
||||
|
||||
|
||||
class ExitCodes(enum.IntEnum):
|
||||
"""Stores various exit codes groups to use."""
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
# 1 is automatically used if Python throws an exception
|
||||
NO_CANDIDATES = 2
|
||||
USER_QUIT = 3
|
||||
DB_OPEN_FAIL = 4
|
||||
|
||||
INTERNAL_ERROR = 10
|
||||
|
||||
|
||||
def qute_command(command):
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write(command + '\n')
|
||||
fifo.flush()
|
||||
|
||||
|
||||
def stderr(to_print):
|
||||
"""Extra functionality to echo out errors to qb ui."""
|
||||
print(to_print, file=sys.stderr)
|
||||
qute_command('message-error "{}"'.format(to_print))
|
||||
|
||||
|
||||
def dmenu(items, invocation, encoding):
|
||||
"""Runs dmenu with given arguments."""
|
||||
command = shlex.split(invocation)
|
||||
process = subprocess.run(command, input='\n'.join(items).encode(encoding),
|
||||
stdout=subprocess.PIPE)
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def get_password():
|
||||
"""Get a keepass db password from user."""
|
||||
_app = QApplication(sys.argv)
|
||||
text, ok = QInputDialog.getText(
|
||||
None, "KeePass DB Password",
|
||||
"Please enter your KeePass Master Password",
|
||||
QLineEdit.Password)
|
||||
if not ok:
|
||||
stderr('Password Prompt Rejected.')
|
||||
sys.exit(ExitCodes.USER_QUIT)
|
||||
return text
|
||||
|
||||
|
||||
def find_candidates(args, host):
|
||||
"""Finds candidates that match host"""
|
||||
file_path = os.path.expanduser(args.path)
|
||||
|
||||
# TODO find a way to keep the db open, so we don't open (and query
|
||||
# password) it every time
|
||||
|
||||
pw = None
|
||||
if not args.no_password:
|
||||
pw = get_password()
|
||||
|
||||
kf = args.keyfile_path
|
||||
if kf:
|
||||
kf = os.path.expanduser(kf)
|
||||
|
||||
try:
|
||||
kp = pykeepass.PyKeePass(file_path, password=pw, keyfile=kf)
|
||||
except Exception as e:
|
||||
stderr("There was an error opening the DB: {}".format(str(e)))
|
||||
|
||||
return kp.find_entries(url="{}{}{}".format(".*", host, ".*"), regex=True)
|
||||
|
||||
|
||||
def candidate_to_str(args, candidate):
|
||||
"""Turns candidate into a human readable string for dmenu"""
|
||||
return args.dmenu_format.format(title=candidate.title,
|
||||
url=candidate.url,
|
||||
username=candidate.username,
|
||||
path=candidate.path,
|
||||
uuid=candidate.uuid)
|
||||
|
||||
|
||||
def candidate_to_secret(candidate):
|
||||
"""Turns candidate into a generic (user, password) tuple"""
|
||||
return (candidate.username, candidate.password)
|
||||
|
||||
|
||||
def run(args):
|
||||
"""Runs qute-keepass"""
|
||||
if not args.url:
|
||||
argument_parser.print_help()
|
||||
return ExitCodes.FAILURE
|
||||
|
||||
url_host = QUrl(args.url).host()
|
||||
|
||||
if not url_host:
|
||||
stderr('{} was not parsed as a valid URL!'.format(args.url))
|
||||
return ExitCodes.INTERNAL_ERROR
|
||||
|
||||
# Find candidates matching the host of the given URL
|
||||
candidates = find_candidates(args, url_host)
|
||||
if not candidates:
|
||||
stderr('No candidates for URL {!r} found!'.format(args.url))
|
||||
return ExitCodes.NO_CANDIDATES
|
||||
|
||||
# Create a map so we can get turn the resulting string from dmenu back into
|
||||
# a candidate
|
||||
candidates_strs = list(map(functools.partial(candidate_to_str, args),
|
||||
candidates))
|
||||
candidates_map = dict(zip(candidates_strs, candidates))
|
||||
|
||||
if len(candidates) == 1:
|
||||
selection = candidates.pop()
|
||||
else:
|
||||
selection = dmenu(candidates_strs,
|
||||
args.dmenu_invocation,
|
||||
args.io_encoding)
|
||||
|
||||
if selection not in candidates_map:
|
||||
stderr("'{}' was not a valid entry!").format(selection)
|
||||
return ExitCodes.USER_QUIT
|
||||
|
||||
selection = candidates_map[selection]
|
||||
|
||||
username, password = candidate_to_secret(selection)
|
||||
|
||||
insert_mode = ';; enter-mode insert' if args.insert_mode else ''
|
||||
if args.username_fill_only:
|
||||
qute_command('insert-text {}{}'.format(username, insert_mode))
|
||||
elif args.password_fill_only:
|
||||
qute_command('insert-text {}{}'.format(password, insert_mode))
|
||||
else:
|
||||
# Enter username and password using insert-key and fake-key <Tab>
|
||||
# (which supports more passwords than fake-key only), then switch back
|
||||
# into insert-mode, so the form can be directly submitted by hitting
|
||||
# enter afterwards. It dosen't matter when we go into insert mode, but
|
||||
# the other commands need to be be executed sequentially, so we add
|
||||
# delays with later.
|
||||
qute_command('insert-text {} ;;'
|
||||
'later {} fake-key <Tab> ;;'
|
||||
'later {} insert-text {}{}'
|
||||
.format(username, CMD_DELAY,
|
||||
CMD_DELAY * 2, password, insert_mode))
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arguments = argument_parser.parse_args()
|
||||
sys.exit(run(arguments))
|
||||
@@ -109,6 +109,13 @@ def dmenu(items, invocation, encoding):
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def fake_key_raw(text):
|
||||
for character in text:
|
||||
# Escape all characters by default, space requires special handling
|
||||
sequence = '" "' if character == ' ' else '\{}'.format(character)
|
||||
qute_command('fake-key {}'.format(sequence))
|
||||
|
||||
|
||||
def main(arguments):
|
||||
if not arguments.url:
|
||||
argument_parser.print_help()
|
||||
@@ -158,15 +165,19 @@ def main(arguments):
|
||||
return ExitCodes.COULD_NOT_MATCH_PASSWORD
|
||||
password = match.group(1)
|
||||
|
||||
insert_mode = ';; enter-mode insert' if arguments.insert_mode else ''
|
||||
if arguments.username_only:
|
||||
qute_command('fake-key {}{}'.format(username, insert_mode))
|
||||
fake_key_raw(username)
|
||||
elif arguments.password_only:
|
||||
qute_command('fake-key {}{}'.format(password, insert_mode))
|
||||
fake_key_raw(password)
|
||||
else:
|
||||
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
|
||||
# back into insert-mode, so the form can be directly submitted by hitting enter afterwards
|
||||
qute_command('fake-key {} ;; fake-key <Tab> ;; fake-key {}{}'.format(username, password, insert_mode))
|
||||
fake_key_raw(username)
|
||||
qute_command('fake-key <Tab>')
|
||||
fake_key_raw(password)
|
||||
|
||||
if arguments.insert_mode:
|
||||
qute_command('enter-mode insert')
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (1, 2, 1)
|
||||
__version_info__ = (1, 3, 0)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
||||
@@ -724,7 +724,13 @@ class AbstractTab(QWidget):
|
||||
if getattr(evt, 'posted', False):
|
||||
raise utils.Unreachable("Can't re-use an event which was already "
|
||||
"posted!")
|
||||
|
||||
recipient = self.event_target()
|
||||
if recipient is None:
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/3888
|
||||
log.webview.warning("Unable to find event target!")
|
||||
return
|
||||
|
||||
evt.posted = True
|
||||
QApplication.postEvent(recipient, evt)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import attr
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
||||
|
||||
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
|
||||
javascript)
|
||||
javascript, urlmatch)
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.browser import downloads
|
||||
|
||||
@@ -48,6 +48,7 @@ class GreasemonkeyScript:
|
||||
def __init__(self, properties, code):
|
||||
self._code = code
|
||||
self.includes = []
|
||||
self.matches = []
|
||||
self.excludes = []
|
||||
self.requires = []
|
||||
self.description = None
|
||||
@@ -63,8 +64,10 @@ class GreasemonkeyScript:
|
||||
self.namespace = value
|
||||
elif name == 'description':
|
||||
self.description = value
|
||||
elif name in ['include', 'match']:
|
||||
elif name == 'include':
|
||||
self.includes.append(value)
|
||||
elif name == 'match':
|
||||
self.matches.append(value)
|
||||
elif name in ['exclude', 'exclude_match']:
|
||||
self.excludes.append(value)
|
||||
elif name == 'run-at':
|
||||
@@ -92,7 +95,7 @@ class GreasemonkeyScript:
|
||||
props = ""
|
||||
script = cls(re.findall(cls.PROPS_REGEX, props), source)
|
||||
script.script_meta = props
|
||||
if not script.includes:
|
||||
if not script.includes and not script.matches:
|
||||
script.includes = ['*']
|
||||
return script
|
||||
|
||||
@@ -117,7 +120,7 @@ class GreasemonkeyScript:
|
||||
return json.dumps({
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'matches': self.includes,
|
||||
'matches': self.matches,
|
||||
'includes': self.includes,
|
||||
'excludes': self.excludes,
|
||||
'run-at': self.run_at,
|
||||
@@ -143,6 +146,42 @@ class MatchingScripts(object):
|
||||
idle = attr.ib(default=attr.Factory(list))
|
||||
|
||||
|
||||
class GreasemonkeyMatcher:
|
||||
|
||||
"""Check whether scripts should be loaded for a given URL."""
|
||||
|
||||
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
|
||||
# Limit the schemes scripts can run on due to unreasonable levels of
|
||||
# exploitability
|
||||
GREASEABLE_SCHEMES = ['http', 'https', 'ftp', 'file']
|
||||
|
||||
def __init__(self, url):
|
||||
self._url = url
|
||||
self._url_string = url.toString(QUrl.FullyEncoded)
|
||||
self.is_greaseable = url.scheme() in self.GREASEABLE_SCHEMES
|
||||
|
||||
def _match_pattern(self, pattern):
|
||||
# For include and exclude rules if they start and end with '/' they
|
||||
# should be treated as a (ecma syntax) regular expression.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
matches = re.search(pattern[1:-1], self._url_string, flags=re.I)
|
||||
return matches is not None
|
||||
|
||||
# Otherwise they are glob expressions.
|
||||
return fnmatch.fnmatch(self._url_string, pattern)
|
||||
|
||||
def matches(self, script):
|
||||
"""Check whether the URL matches filtering rules of the script."""
|
||||
assert self.is_greaseable
|
||||
matching_includes = any(self._match_pattern(pat)
|
||||
for pat in script.includes)
|
||||
matching_match = any(urlmatch.UrlPattern(pat).matches(self._url)
|
||||
for pat in script.matches)
|
||||
matching_excludes = any(self._match_pattern(pat)
|
||||
for pat in script.excludes)
|
||||
return (matching_includes or matching_match) and not matching_excludes
|
||||
|
||||
|
||||
class GreasemonkeyManager(QObject):
|
||||
|
||||
"""Manager of userscripts and a Greasemonkey compatible environment.
|
||||
@@ -154,10 +193,6 @@ class GreasemonkeyManager(QObject):
|
||||
"""
|
||||
|
||||
scripts_reloaded = pyqtSignal()
|
||||
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
|
||||
# Limit the schemes scripts can run on due to unreasonable levels of
|
||||
# exploitability
|
||||
greaseable_schemes = ['http', 'https', 'ftp', 'file']
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -309,30 +344,17 @@ class GreasemonkeyManager(QObject):
|
||||
returns a tuple of lists of scripts meant to run at (document-start,
|
||||
document-end, document-idle)
|
||||
"""
|
||||
if url.scheme() not in self.greaseable_schemes:
|
||||
matcher = GreasemonkeyMatcher(url)
|
||||
if not matcher.is_greaseable:
|
||||
return MatchingScripts(url, [], [], [])
|
||||
|
||||
string_url = url.toString(QUrl.FullyEncoded)
|
||||
|
||||
def _match(pattern):
|
||||
# For include and exclude rules if they start and end with '/' they
|
||||
# should be treated as a (ecma syntax) regular expression.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
matches = re.search(pattern[1:-1], string_url, flags=re.I)
|
||||
return matches is not None
|
||||
|
||||
# Otherwise they are glob expressions.
|
||||
return fnmatch.fnmatch(string_url, pattern)
|
||||
|
||||
tester = (lambda script:
|
||||
any(_match(pat) for pat in script.includes) and
|
||||
not any(_match(pat) for pat in script.excludes))
|
||||
|
||||
return MatchingScripts(
|
||||
url,
|
||||
[script for script in self._run_start if tester(script)],
|
||||
[script for script in self._run_end if tester(script)],
|
||||
[script for script in self._run_idle if tester(script)]
|
||||
url=url,
|
||||
start=[script for script in self._run_start
|
||||
if matcher.matches(script)],
|
||||
end=[script for script in self._run_end
|
||||
if matcher.matches(script)],
|
||||
idle=[script for script in self._run_idle
|
||||
if matcher.matches(script)]
|
||||
)
|
||||
|
||||
def all_scripts(self):
|
||||
|
||||
@@ -24,16 +24,18 @@ import os
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, message
|
||||
|
||||
dict_version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
|
||||
|
||||
def version(filename):
|
||||
"""Extract the version number from the dictionary file name."""
|
||||
version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
match = version_re.fullmatch(filename)
|
||||
match = dict_version_re.match(filename)
|
||||
if match is None:
|
||||
raise ValueError('the given dictionary file name is malformed: {}'
|
||||
.format(filename))
|
||||
message.warning(
|
||||
"Found a dictionary with a malformed name: {}".format(filename))
|
||||
return None
|
||||
return tuple(int(n) for n in match.group('version').split('-'))
|
||||
|
||||
|
||||
@@ -44,15 +46,23 @@ def dictionary_dir():
|
||||
|
||||
|
||||
def local_files(code):
|
||||
"""Return all installed dictionaries for the given code."""
|
||||
"""Return all installed dictionaries for the given code.
|
||||
|
||||
The returned dictionaries are sorted by version, therefore the latest will
|
||||
be the first element. The list will be empty if no dictionaries are found.
|
||||
"""
|
||||
pathname = os.path.join(dictionary_dir(), '{}*.bdic'.format(code))
|
||||
matching_dicts = glob.glob(pathname)
|
||||
files = []
|
||||
for matching_dict in sorted(matching_dicts, key=version, reverse=True):
|
||||
filename = os.path.basename(matching_dict)
|
||||
log.config.debug('Found file for dict {}: {}'.format(code, filename))
|
||||
files.append(filename)
|
||||
return files
|
||||
versioned_dicts = []
|
||||
for matching_dict in matching_dicts:
|
||||
parsed_version = version(matching_dict)
|
||||
if parsed_version is not None:
|
||||
filename = os.path.basename(matching_dict)
|
||||
log.config.debug('Found file for dict {}: {}'
|
||||
.format(code, filename))
|
||||
versioned_dicts.append((parsed_version, filename))
|
||||
return [filename for version, filename
|
||||
in sorted(versioned_dicts, reverse=True)]
|
||||
|
||||
|
||||
def local_filename(code):
|
||||
|
||||
@@ -101,7 +101,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
|
||||
def retry(self):
|
||||
state = self._qt_item.state()
|
||||
assert state == QWebEngineDownloadItem.DownloadInterrupted, state
|
||||
if state != QWebEngineDownloadItem.DownloadInterrupted:
|
||||
log.downloads.warning(
|
||||
"Trying to retry download in state {}".format(
|
||||
debug.qenum_key(QWebEngineDownloadItem, state)))
|
||||
return
|
||||
|
||||
try:
|
||||
self._qt_item.resume()
|
||||
|
||||
@@ -176,24 +176,11 @@ class ProfileSetter:
|
||||
"""Initialize settings on the given profile."""
|
||||
self.set_http_headers()
|
||||
self.set_http_cache_size()
|
||||
self._init_attributes()
|
||||
|
||||
self._profile.settings().setAttribute(
|
||||
QWebEngineSettings.FullScreenSupportEnabled, True)
|
||||
if qtutils.version_check('5.8'):
|
||||
self._profile.setSpellCheckEnabled(True)
|
||||
self.set_dictionary_language()
|
||||
|
||||
def _init_attributes(self):
|
||||
"""Initialize hard-coded attributes."""
|
||||
values = {
|
||||
'FullScreenSupportEnabled': True,
|
||||
'FocusOnNavigationEnabled': True,
|
||||
}
|
||||
settings = self._profile.settings()
|
||||
for name, value in values.items():
|
||||
attr = getattr(QWebEngineSettings, name, None)
|
||||
if attr is not None:
|
||||
settings.setAttribute(attr, value)
|
||||
|
||||
def set_http_headers(self):
|
||||
"""Set the user agent and accept-language for the given profile.
|
||||
|
||||
@@ -242,6 +229,7 @@ class ProfileSetter:
|
||||
|
||||
log.config.debug("Found dicts: {}".format(filenames))
|
||||
self._profile.setSpellCheckLanguages(filenames)
|
||||
self._profile.setSpellCheckEnabled(bool(filenames))
|
||||
|
||||
|
||||
def _update_settings(option):
|
||||
|
||||
@@ -780,6 +780,8 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
url: The QUrl to open.
|
||||
predict: If set to False, predicted_navigation is not emitted.
|
||||
"""
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
|
||||
self._widget.setFocus()
|
||||
self._saved_zoom = self.zoom.factor()
|
||||
self._openurl_prepare(url, predict=predict)
|
||||
self._widget.load(url)
|
||||
@@ -1104,6 +1106,4 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||
|
||||
def event_target(self):
|
||||
fp = self._widget.focusProxy()
|
||||
assert fp is not None
|
||||
return fp
|
||||
return self._widget.focusProxy()
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
"""The commandline in the statusbar."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
|
||||
@@ -69,6 +71,26 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
self.textChanged.connect(self.updateGeometry)
|
||||
self.textChanged.connect(self._incremental_search)
|
||||
|
||||
self._command_dispatcher = objreg.get(
|
||||
'command-dispatcher', scope='window', window=self._win_id)
|
||||
|
||||
def _handle_search(self):
|
||||
"""Check if the currently entered text is a search, and if so, run it.
|
||||
|
||||
Return:
|
||||
True if a search was executed, False otherwise.
|
||||
"""
|
||||
search_prefixes = {
|
||||
'/': self._command_dispatcher.search,
|
||||
'?': functools.partial(
|
||||
self._command_dispatcher.search, reverse=True)
|
||||
}
|
||||
if self.prefix() in search_prefixes:
|
||||
search_fn = search_prefixes[self.prefix()]
|
||||
search_fn(self.text()[1:])
|
||||
return True
|
||||
return False
|
||||
|
||||
def prefix(self):
|
||||
"""Get the currently entered command prefix."""
|
||||
text = self.text()
|
||||
@@ -162,17 +184,17 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
Args:
|
||||
rapid: Run the command without closing or clearing the command bar.
|
||||
"""
|
||||
prefixes = {
|
||||
':': '',
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
text = self.text()
|
||||
self.history.append(text)
|
||||
|
||||
was_search = self._handle_search()
|
||||
|
||||
if not rapid:
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.command,
|
||||
'cmd accept')
|
||||
self.got_cmd[str].emit(prefixes[text[0]] + text[1:])
|
||||
|
||||
if not was_search:
|
||||
self.got_cmd[str].emit(text[1:])
|
||||
|
||||
@cmdutils.register(instance='status-command', scope='window')
|
||||
def edit_command(self, run=False):
|
||||
@@ -253,15 +275,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
width = self.fontMetrics().width(text)
|
||||
return QSize(width, height)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _incremental_search(self, text):
|
||||
@pyqtSlot()
|
||||
def _incremental_search(self):
|
||||
if not config.val.search.incremental:
|
||||
return
|
||||
|
||||
search_prefixes = {
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
|
||||
if self.prefix() in ['/', '?']:
|
||||
self.got_cmd[str].emit(search_prefixes[text[0]] + text[1:])
|
||||
self._handle_search()
|
||||
|
||||
@@ -489,6 +489,8 @@ class TabbedBrowser(QWidget):
|
||||
self.widget.count())
|
||||
else:
|
||||
self.widget.setCurrentWidget(tab)
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
|
||||
tab.setFocus()
|
||||
|
||||
tab.show()
|
||||
self.new_tab.emit(tab, idx)
|
||||
|
||||
@@ -45,7 +45,7 @@ class PyPIVersionClient(QObject):
|
||||
arg: The error message, as string.
|
||||
"""
|
||||
|
||||
API_URL = 'https://pypi.python.org/pypi/{}/json'
|
||||
API_URL = 'https://pypi.org/pypi/{}/json'
|
||||
success = pyqtSignal(str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
|
||||
@@ -28,6 +28,21 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
|
||||
QNetworkReply)
|
||||
|
||||
|
||||
class HTTPRequest(QNetworkRequest):
|
||||
"""A QNetworkRquest that follows (secure) redirects by default."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
try:
|
||||
self.setAttribute(QNetworkRequest.RedirectPolicyAttribute,
|
||||
QNetworkRequest.NoLessSafeRedirectPolicy)
|
||||
except AttributeError:
|
||||
# RedirectPolicyAttribute was introduced in 5.9 to replace
|
||||
# FollowRedirectsAttribute.
|
||||
self.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
|
||||
True)
|
||||
|
||||
|
||||
class HTTPClient(QObject):
|
||||
|
||||
"""An HTTP client based on QNetworkAccessManager.
|
||||
@@ -63,7 +78,7 @@ class HTTPClient(QObject):
|
||||
if data is None:
|
||||
data = {}
|
||||
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
|
||||
request = QNetworkRequest(url)
|
||||
request = HTTPRequest(url)
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader,
|
||||
'application/x-www-form-urlencoded;charset=utf-8')
|
||||
reply = self._nam.post(request, encoded_data)
|
||||
@@ -77,7 +92,7 @@ class HTTPClient(QObject):
|
||||
Args:
|
||||
url: The URL to access, as QUrl.
|
||||
"""
|
||||
request = QNetworkRequest(url)
|
||||
request = HTTPRequest(url)
|
||||
reply = self._nam.get(request)
|
||||
self._handle_reply(reply)
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
BAZ<br/>
|
||||
space travel<br/>
|
||||
/slash<br/>
|
||||
-r reversed<br/>
|
||||
;; semicolons<br/>
|
||||
<a class="toselect" href="hello.txt">follow me!</a><br/>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
@@ -40,11 +40,26 @@ Feature: Searching on a page
|
||||
Then "space " should be found
|
||||
|
||||
Scenario: Searching with / and slash in search term (issue 507)
|
||||
When I run :set-cmd-text -s //slash
|
||||
When I run :set-cmd-text //slash
|
||||
And I run :command-accept
|
||||
And I wait for "search found /slash" in the log
|
||||
Then "/slash" should be found
|
||||
|
||||
Scenario: Searching with arguments at start of search term
|
||||
When I run :set-cmd-text /-r reversed
|
||||
And I run :command-accept
|
||||
And I wait for "search found -r reversed" in the log
|
||||
Then "-r reversed" should be found
|
||||
|
||||
Scenario: Searching with semicolons in search term
|
||||
When I run :set-cmd-text /;
|
||||
And I run :fake-key -g ;
|
||||
And I run :fake-key -g <space>
|
||||
And I run :fake-key -g semi
|
||||
And I run :command-accept
|
||||
And I wait for "search found ;; semi" in the log
|
||||
Then ";; semi" should be found
|
||||
|
||||
# This doesn't work because this is QtWebKit behavior.
|
||||
@xfail_norun
|
||||
Scenario: Searching text with umlauts
|
||||
|
||||
@@ -17,31 +17,69 @@
|
||||
# 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
|
||||
"""Tests for qutebrowser.browser.webengine.spell module."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
from qutebrowser.browser.webengine import spell
|
||||
from qutebrowser.utils import usertypes
|
||||
|
||||
|
||||
def test_version():
|
||||
def test_version(message_mock, caplog):
|
||||
"""Tests parsing dictionary version from its file name."""
|
||||
assert spell.version('en-US-8-0.bdic') == (8, 0)
|
||||
assert spell.version('pl-PL-3-0.bdic') == (3, 0)
|
||||
with pytest.raises(ValueError):
|
||||
spell.version('malformed_filename')
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert spell.version('malformed_filename') is None
|
||||
msg = message_mock.getmsg(usertypes.MessageLevel.warning)
|
||||
expected = ("Found a dictionary with a malformed name: malformed_filename")
|
||||
assert msg.text == expected
|
||||
|
||||
|
||||
def test_local_filename_dictionary_does_not_exist(tmpdir, monkeypatch):
|
||||
def test_dictionary_dir(monkeypatch):
|
||||
monkeypatch.setattr(QLibraryInfo, 'location', lambda _: 'datapath')
|
||||
assert spell.dictionary_dir() == os.path.join('datapath',
|
||||
'qtwebengine_dictionaries')
|
||||
|
||||
|
||||
def test_local_filename_dictionary_does_not_exist(monkeypatch):
|
||||
"""Tests retrieving local filename when the dir doesn't exits."""
|
||||
monkeypatch.setattr(
|
||||
spell, 'dictionary_dir', lambda: '/some-non-existing-dir')
|
||||
assert not spell.local_filename('en-US')
|
||||
|
||||
|
||||
def test_local_filename_dictionary_not_installed(tmpdir, monkeypatch):
|
||||
"""Tests retrieving local filename when the dict not installed."""
|
||||
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
|
||||
assert not spell.local_filename('en-US')
|
||||
|
||||
|
||||
def test_local_filename_not_installed_malformed(tmpdir, monkeypatch, caplog):
|
||||
"""Tests retrieving local filename when the only file is malformed."""
|
||||
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
|
||||
(tmpdir / 'en-US.bdic').ensure()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert not spell.local_filename('en-US')
|
||||
|
||||
|
||||
def test_local_filename_dictionary_installed(tmpdir, monkeypatch):
|
||||
"""Tests retrieving local filename when the dict installed."""
|
||||
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
|
||||
for lang_file in ['en-US-11-0.bdic', 'en-US-7-1.bdic', 'pl-PL-3-0.bdic']:
|
||||
(tmpdir / lang_file).ensure()
|
||||
assert spell.local_filename('en-US') == 'en-US-11-0'
|
||||
assert spell.local_filename('pl-PL') == 'pl-PL-3-0'
|
||||
|
||||
|
||||
def test_local_filename_installed_malformed(tmpdir, monkeypatch, caplog):
|
||||
"""Tests retrieving local filename when the dict installed.
|
||||
|
||||
In this usecase, another existing file is malformed."""
|
||||
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
|
||||
for lang_file in ['en-US-11-0.bdic', 'en-US-7-1.bdic', 'en-US.bdic']:
|
||||
(tmpdir / lang_file).ensure()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert spell.local_filename('en-US') == 'en-US-11-0'
|
||||
|
||||
@@ -73,3 +73,14 @@ def test_existing_dict(config_stub, monkeypatch):
|
||||
webenginesettings.private_profile]:
|
||||
assert profile.isSpellCheckEnabled()
|
||||
assert profile.spellCheckLanguages() == ['en-US-8-0']
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
|
||||
def test_spell_check_disabled(config_stub, monkeypatch):
|
||||
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
|
||||
config_stub.val.spellcheck.languages = []
|
||||
webenginesettings._update_settings('spellcheck.languages')
|
||||
for profile in [webenginesettings.default_profile,
|
||||
webenginesettings.private_profile]:
|
||||
assert not profile.isSpellCheckEnabled()
|
||||
|
||||
@@ -32,7 +32,7 @@ test_gm_script = r"""
|
||||
// @name qutebrowser test userscript
|
||||
// @namespace invalid.org
|
||||
// @include http://localhost:*/data/title.html
|
||||
// @match http://trolol*
|
||||
// @match http://*.trolol.com/*
|
||||
// @exclude https://badhost.xxx/*
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
@@ -60,7 +60,7 @@ def test_all():
|
||||
|
||||
@pytest.mark.parametrize("url, expected_matches", [
|
||||
# included
|
||||
('http://trololololololo.com/', 1),
|
||||
('http://trolol.com/', 1),
|
||||
# neither included nor excluded
|
||||
('http://aaaaaaaaaa.com/', 0),
|
||||
# excluded
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_get_version_success(qtbot):
|
||||
with qtbot.waitSignal(client.success):
|
||||
client.get_version('test')
|
||||
|
||||
assert http_stub.url == QUrl('https://pypi.python.org/pypi/test/json')
|
||||
assert http_stub.url == QUrl(client.API_URL.format('test'))
|
||||
|
||||
|
||||
def test_get_version_error(qtbot):
|
||||
|
||||
Reference in New Issue
Block a user