summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.pylintrc2
-rw-r--r--README.asciidoc1
-rw-r--r--doc/changelog.asciidoc1
-rw-r--r--misc/requirements/requirements-qutebrowser.txt-raw1
-rw-r--r--misc/requirements/requirements-tests-git.txt1
-rw-r--r--qutebrowser/browser/browsertab.py15
-rw-r--r--qutebrowser/browser/hints.py29
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py8
-rw-r--r--qutebrowser/browser/webkit/mhtml.py15
-rw-r--r--qutebrowser/browser/webkit/network/networkmanager.py12
-rw-r--r--qutebrowser/browser/webkit/rfc6266.py10
-rw-r--r--qutebrowser/commands/command.py43
-rw-r--r--qutebrowser/commands/runners.py13
-rw-r--r--qutebrowser/completion/completer.py15
-rw-r--r--qutebrowser/config/configdata.py21
-rw-r--r--qutebrowser/config/configexc.py15
-rw-r--r--qutebrowser/config/configtypes.py13
-rw-r--r--qutebrowser/keyinput/modeman.py23
-rw-r--r--qutebrowser/mainwindow/prompt.py15
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py14
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py25
-rw-r--r--qutebrowser/mainwindow/tabwidget.py17
-rw-r--r--qutebrowser/misc/crashsignal.py14
-rw-r--r--qutebrowser/misc/earlyinit.py3
-rw-r--r--qutebrowser/utils/utils.py13
-rw-r--r--qutebrowser/utils/version.py25
-rw-r--r--requirements.txt1
-rw-r--r--scripts/dev/check_coverage.py16
-rw-r--r--scripts/dev/get_coredumpctl_traces.py16
-rwxr-xr-xscripts/dev/run_vulture.py6
-rwxr-xr-xsetup.py2
-rw-r--r--tests/end2end/fixtures/testprocess.py10
-rw-r--r--tests/end2end/fixtures/webserver.py14
-rw-r--r--tests/end2end/test_dirbrowser.py20
-rw-r--r--tests/end2end/test_hints_html.py9
-rw-r--r--tests/helpers/fixtures.py17
-rw-r--r--tests/helpers/messagemock.py14
-rw-r--r--tests/helpers/stubs.py26
-rw-r--r--tests/unit/browser/test_signalfilter.py8
-rw-r--r--tests/unit/browser/webkit/network/test_filescheme.py16
-rw-r--r--tests/unit/browser/webkit/test_tabhistory.py9
-rw-r--r--tests/unit/browser/webkit/test_webkitelem.py12
-rw-r--r--tests/unit/config/test_configtypes.py27
-rw-r--r--tests/unit/misc/test_ipc.py15
-rw-r--r--tests/unit/misc/test_split.py27
-rw-r--r--tests/unit/utils/test_standarddir.py31
-rw-r--r--tests/unit/utils/test_urlutils.py9
-rw-r--r--tests/unit/utils/test_utils.py8
-rw-r--r--tests/unit/utils/test_version.py28
49 files changed, 453 insertions, 252 deletions
diff --git a/.pylintrc b/.pylintrc
index 31e91a3d3..39cbf052d 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -43,6 +43,7 @@ function-rgx=[a-z_][a-z0-9_]{2,50}$
const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
method-rgx=[a-z_][A-Za-z0-9_]{1,50}$
attr-rgx=[a-z_][a-z0-9_]{0,30}$
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{1,30}|(__.*__))$
argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$
docstring-min-length=3
@@ -64,6 +65,7 @@ valid-metaclass-classmethod-first-arg=cls
[TYPECHECK]
ignored-modules=PyQt5,PyQt5.QtWebKit
+ignored-classes=_CountingAttr
[IMPORTS]
# WORKAROUND
diff --git a/README.asciidoc b/README.asciidoc
index bf2231063..212d3bec0 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -116,6 +116,7 @@ The following software and libraries are required to run qutebrowser:
* http://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments]
* http://pyyaml.org/wiki/PyYAML[PyYAML]
+* http://www.attrs.org/[attrs]
The following libraries are optional:
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 8c4cbcf79..302a172f7 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -27,6 +27,7 @@ Breaking changes
- (TODO) New dependency on ruamel.yaml; dropped PyYAML dependency.
- (TODO) The QtWebEngine backend is now used by default if available.
- New dependency on the QtSql module and Qt sqlite support.
+- New dependency on the `attrs` Python module.
- New config system which ignores the old config file.
- The depedency on PyOpenGL (when using QtWebEngine) got removed. Note
that PyQt5.QtOpenGL is still a dependency.
diff --git a/misc/requirements/requirements-qutebrowser.txt-raw b/misc/requirements/requirements-qutebrowser.txt-raw
index 8aadfa8c4..c66c65beb 100644
--- a/misc/requirements/requirements-qutebrowser.txt-raw
+++ b/misc/requirements/requirements-qutebrowser.txt-raw
@@ -4,3 +4,4 @@ pyPEG2
PyYAML
colorama
cssutils
+attrs
diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt
index 771cf58c8..6b31140bb 100644
--- a/misc/requirements/requirements-tests-git.txt
+++ b/misc/requirements/requirements-tests-git.txt
@@ -40,6 +40,7 @@ git+https://github.com/pallets/jinja.git
git+https://github.com/pallets/markupsafe.git
hg+http://bitbucket.org/birkenfeld/pygments-main
hg+https://bitbucket.org/fdik/pypeg
+git+https://github.com/python-attrs/attrs.git
# Fails to build:
# gcc: error: ext/_yaml.c: No such file or directory
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 60b5b3b31..0b21b6b05 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -21,6 +21,7 @@
import itertools
+import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QApplication
@@ -82,6 +83,7 @@ TerminationStatus = usertypes.enum('TerminationStatus', [
])
+@attr.s
class TabData:
"""A simple namespace with a fixed set of attributes.
@@ -97,13 +99,12 @@ class TabData:
fullscreen: Whether the tab has a video shown fullscreen currently.
"""
- def __init__(self):
- self.keep_icon = False
- self.viewing_source = False
- self.inspector = None
- self.override_target = None
- self.pinned = False
- self.fullscreen = False
+ keep_icon = attr.ib(False)
+ viewing_source = attr.ib(False)
+ inspector = attr.ib(None)
+ override_target = attr.ib(None)
+ pinned = attr.ib(False)
+ fullscreen = attr.ib(False)
class AbstractAction:
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 14a9cd7c2..8694a6ad5 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -26,6 +26,7 @@ import re
import html
from string import ascii_lowercase
+import attr
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel
@@ -131,6 +132,7 @@ class HintLabel(QLabel):
self.deleteLater()
+@attr.s
class HintContext:
"""Context namespace used for hinting.
@@ -158,19 +160,18 @@ class HintContext:
group: The group of web elements to hint.
"""
- def __init__(self):
- self.all_labels = []
- self.labels = {}
- self.target = None
- self.baseurl = None
- self.to_follow = None
- self.rapid = False
- self.add_history = False
- self.filterstr = None
- self.args = []
- self.tab = None
- self.group = None
- self.hint_mode = None
+ all_labels = attr.ib(attr.Factory(list))
+ labels = attr.ib(attr.Factory(dict))
+ target = attr.ib(None)
+ baseurl = attr.ib(None)
+ to_follow = attr.ib(None)
+ rapid = attr.ib(False)
+ add_history = attr.ib(False)
+ filterstr = attr.ib(None)
+ args = attr.ib(attr.Factory(list))
+ tab = attr.ib(None)
+ group = attr.ib(None)
+ hint_mode = attr.ib(None)
def get_args(self, urlstr):
"""Get the arguments, with {hint-url} replaced by the given URL."""
@@ -389,6 +390,7 @@ class HintManager(QObject):
def _cleanup(self):
"""Clean up after hinting."""
+ # pylint: disable=not-an-iterable
for label in self._context.all_labels:
label.cleanup()
@@ -795,6 +797,7 @@ class HintManager(QObject):
log.hints.debug("Filtering hints on {!r}".format(filterstr))
visible = []
+ # pylint: disable=not-an-iterable
for label in self._context.all_labels:
try:
if self._filter_matches(filterstr, str(label.elem)):
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index 22ec4c02c..921fee54d 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -22,8 +22,8 @@
import io
import shutil
import functools
-import collections
+import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
@@ -34,7 +34,11 @@ from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
-_RetryInfo = collections.namedtuple('_RetryInfo', ['request', 'manager'])
+@attr.s
+class _RetryInfo:
+
+ request = attr.ib()
+ manager = attr.ib()
class DownloadItem(downloads.AbstractDownloadItem):
diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py
index ed357f2bd..ccdd03dad 100644
--- a/qutebrowser/browser/webkit/mhtml.py
+++ b/qutebrowser/browser/webkit/mhtml.py
@@ -25,7 +25,6 @@ import io
import os
import re
import sys
-import collections
import uuid
import email.policy
import email.generator
@@ -34,15 +33,21 @@ import email.mime.multipart
import email.message
import quopri
+import attr
from PyQt5.QtCore import QUrl
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import webkitelem
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
-_File = collections.namedtuple('_File',
- ['content', 'content_type', 'content_location',
- 'transfer_encoding'])
+
+@attr.s
+class _File:
+
+ content = attr.ib()
+ content_type = attr.ib()
+ content_location = attr.ib()
+ transfer_encoding = attr.ib()
_CSS_URL_PATTERNS = [re.compile(x) for x in [
@@ -174,7 +179,7 @@ class MHTMLWriter:
root_content: The root content as bytes.
content_location: The url of the page as str.
content_type: The MIME-type of the root content as str.
- _files: Mapping of location->_File namedtuple.
+ _files: Mapping of location->_File object.
"""
def __init__(self, root_content, content_location, content_type):
diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py
index 770cb2f20..f93d47d43 100644
--- a/qutebrowser/browser/webkit/network/networkmanager.py
+++ b/qutebrowser/browser/webkit/network/networkmanager.py
@@ -24,6 +24,7 @@ import collections
import netrc
import html
+import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
QByteArray)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
@@ -37,10 +38,19 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
-ProxyId = collections.namedtuple('ProxyId', 'type, hostname, port')
_proxy_auth_cache = {}
+@attr.s
+class ProxyId:
+
+ """Information identifying a proxy server."""
+
+ type = attr.ib()
+ hostname = attr.ib()
+ port = attr.ib()
+
+
def _is_secure_cipher(cipher):
"""Check if a given SSL cipher (hopefully) isn't broken yet."""
tokens = [e.upper() for e in cipher.name().split('-')]
diff --git a/qutebrowser/browser/webkit/rfc6266.py b/qutebrowser/browser/webkit/rfc6266.py
index c3c8f7a4b..1f71b23e5 100644
--- a/qutebrowser/browser/webkit/rfc6266.py
+++ b/qutebrowser/browser/webkit/rfc6266.py
@@ -19,11 +19,11 @@
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header."""
-import collections
import urllib.parse
import string
import re
+import attr
import pypeg2 as peg
from qutebrowser.utils import utils
@@ -210,7 +210,13 @@ class ContentDispositionValue:
peg.optional(';'))
-LangTagged = collections.namedtuple('LangTagged', ['string', 'langtag'])
+@attr.s
+class LangTagged:
+
+ """A string with an associated language."""
+
+ string = attr.ib()
+ langtag = attr.ib()
class Error(Exception):
diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py
index 233f9d1a7..8559099f2 100644
--- a/qutebrowser/commands/command.py
+++ b/qutebrowser/commands/command.py
@@ -24,43 +24,32 @@ import collections
import traceback
import typing
+import attr
+
from qutebrowser.commands import cmdexc, argparser
-from qutebrowser.utils import (log, utils, message, docutils, objreg,
- usertypes)
+from qutebrowser.utils import log, message, docutils, objreg, usertypes
from qutebrowser.utils import debug as debug_utils
from qutebrowser.misc import objects
+@attr.s
class ArgInfo:
"""Information about an argument."""
- def __init__(self, win_id=False, count=False, hide=False, metavar=None,
- flag=None, completion=None, choices=None):
- if win_id and count:
+ win_id = attr.ib(False)
+ count = attr.ib(False)
+ hide = attr.ib(False)
+ metavar = attr.ib(None)
+ flag = attr.ib(None)
+ completion = attr.ib(None)
+ choices = attr.ib(None)
+
+ @win_id.validator
+ @count.validator
+ def _validate_exclusive(self, _attr, _value):
+ if self.win_id and self.count:
raise TypeError("Argument marked as both count/win_id!")
- self.win_id = win_id
- self.count = count
- self.flag = flag
- self.hide = hide
- self.metavar = metavar
- self.completion = completion
- self.choices = choices
-
- def __eq__(self, other):
- return (self.win_id == other.win_id and
- self.count == other.count and
- self.flag == other.flag and
- self.hide == other.hide and
- self.metavar == other.metavar and
- self.completion == other.completion and
- self.choices == other.choices)
-
- def __repr__(self):
- return utils.get_repr(self, win_id=self.win_id, count=self.count,
- flag=self.flag, hide=self.hide,
- metavar=self.metavar, completion=self.completion,
- choices=self.choices, constructor=True)
class Command:
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
index 1b81c37c6..9d480cc5d 100644
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -19,10 +19,10 @@
"""Module containing command managers (SearchRunner and CommandRunner)."""
-import collections
import traceback
import re
+import attr
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config
@@ -31,10 +31,19 @@ from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split
-ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline'])
last_command = {}
+@attr.s
+class ParseResult:
+
+ """The result of parsing a commandline."""
+
+ cmd = attr.ib()
+ args = attr.ib()
+ cmdline = attr.ib()
+
+
def _current_url(tabbed_browser):
"""Convenience method to get the current url."""
try:
diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py
index c6a2643fb..854231305 100644
--- a/qutebrowser/completion/completer.py
+++ b/qutebrowser/completion/completer.py
@@ -19,8 +19,7 @@
"""Completer attached to a CompletionView."""
-import collections
-
+import attr
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
@@ -29,9 +28,13 @@ from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import miscmodels
-# Context passed into all completion functions
-CompletionInfo = collections.namedtuple('CompletionInfo',
- ['config', 'keyconf'])
+@attr.s
+class CompletionInfo:
+
+ """Context passed into all completion functions."""
+
+ config = attr.ib()
+ keyconf = attr.ib()
class Completer(QObject):
@@ -130,7 +133,9 @@ class Completer(QObject):
return [], '', []
parser = runners.CommandParser()
result = parser.parse(text, fallback=True, keep=True)
+ # pylint: disable=not-an-iterable
parts = [x for x in result.cmdline if x]
+ # pylint: enable=not-an-iterable
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
log.completion.debug('partitioning {} around position {}'.format(parts,
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
index db24338b8..739086628 100644
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -24,16 +24,29 @@ Module attributes:
DATA: A dict of Option objects after init() has been called.
"""
-import collections
import functools
+import attr
from qutebrowser.config import configtypes
from qutebrowser.utils import usertypes, qtutils, utils
DATA = None
-Option = collections.namedtuple('Option', ['name', 'typ', 'default',
- 'backends', 'raw_backends',
- 'description'])
+
+
+@attr.s
+class Option:
+
+ """Description of an Option in the config.
+
+ Note that this is just an option which exists, with no value associated.
+ """
+
+ name = attr.ib()
+ typ = attr.ib()
+ default = attr.ib()
+ backends = attr.ib()
+ raw_backends = attr.ib()
+ description = attr.ib()
def _raise_invalid_node(name, what, node):
diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py
index a4514cd1f..4d283d21f 100644
--- a/qutebrowser/config/configexc.py
+++ b/qutebrowser/config/configexc.py
@@ -19,7 +19,9 @@
"""Exceptions related to config parsing."""
-from qutebrowser.utils import utils, jinja
+import attr
+
+from qutebrowser.utils import jinja
class Error(Exception):
@@ -74,6 +76,7 @@ class NoOptionError(Error):
self.option = option
+@attr.s
class ConfigErrorDesc:
"""A description of an error happening while reading the config.
@@ -84,13 +87,9 @@ class ConfigErrorDesc:
traceback: The formatted traceback of the exception.
"""
- def __init__(self, text, exception, traceback=None):
- self.text = text
- self.exception = exception
- self.traceback = traceback
-
- def __repr__(self):
- return utils.get_repr(self, text=self.text, exception=self.exception)
+ text = attr.ib()
+ exception = attr.ib()
+ traceback = attr.ib(None)
def __str__(self):
return '{}: {}'.format(self.text, self.exception)
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index b92722d16..c669ff426 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -47,13 +47,13 @@ import html
import codecs
import os.path
import itertools
-import collections
import warnings
import datetime
import functools
import operator
import json
+import attr
import yaml
from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont
@@ -1360,8 +1360,15 @@ class FuzzyUrl(BaseType):
raise configexc.ValidationError(value, str(e))
-PaddingValues = collections.namedtuple('PaddingValues', ['top', 'bottom',
- 'left', 'right'])
+@attr.s
+class PaddingValues:
+
+ """Four padding values."""
+
+ top = attr.ib()
+ bottom = attr.ib()
+ left = attr.ib()
+ right = attr.ib()
class Padding(Dict):
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index 504ccc916..6a6dd5459 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -21,6 +21,7 @@
import functools
+import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication
@@ -31,6 +32,7 @@ from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.misc import objects
+@attr.s(frozen=True)
class KeyEvent:
"""A small wrapper over a QKeyEvent storing its data.
@@ -44,18 +46,13 @@ class KeyEvent:
text: A string (QKeyEvent::text).
"""
- def __init__(self, keyevent):
- self.key = keyevent.key()
- self.text = keyevent.text()
+ key = attr.ib()
+ text = attr.ib()
- def __repr__(self):
- return utils.get_repr(self, key=self.key, text=self.text)
-
- def __eq__(self, other):
- return self.key == other.key and self.text == other.text
-
- def __hash__(self):
- return hash((self.key, self.text))
+ @classmethod
+ def from_event(cls, event):
+ """Initialize a KeyEvent from a QKeyEvent."""
+ return cls(event.key(), event.text())
class NotInModeError(Exception):
@@ -179,7 +176,7 @@ class ModeManager(QObject):
filter_this = True
if not filter_this:
- self._releaseevents_to_pass.add(KeyEvent(event))
+ self._releaseevents_to_pass.add(KeyEvent.from_event(event))
if curmode != usertypes.KeyMode.insert:
focus_widget = QApplication.instance().focusWidget()
@@ -201,7 +198,7 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise.
"""
# handle like matching KeyPress
- keyevent = KeyEvent(event)
+ keyevent = KeyEvent.from_event(event)
if keyevent in self._releaseevents_to_pass:
self._releaseevents_to_pass.remove(keyevent)
filter_this = False
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index 1ea81c75f..4242ceb8e 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -23,6 +23,7 @@ import os.path
import html
import collections
+import attr
import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
QItemSelectionModel, QObject, QEventLoop)
@@ -39,7 +40,13 @@ from qutebrowser.commands import cmdutils, cmdexc
prompt_queue = None
-AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
+@attr.s
+class AuthInfo:
+
+ """Authentication info returned by a prompt."""
+
+ user = attr.ib()
+ password = attr.ib()
class Error(Exception):
@@ -750,7 +757,7 @@ class AuthenticationPrompt(_BasePrompt):
"username:password, but {} was given".format(
value))
username, password = value.split(':', maxsplit=1)
- self.question.answer = AuthTuple(username, password)
+ self.question.answer = AuthInfo(username, password)
return True
elif self._user_lineedit.hasFocus():
# Earlier, tab was bound to :prompt-accept, so to still support
@@ -758,8 +765,8 @@ class AuthenticationPrompt(_BasePrompt):
self._password_lineedit.setFocus()
return False
else:
- self.question.answer = AuthTuple(self._user_lineedit.text(),
- self._password_lineedit.text())
+ self.question.answer = AuthInfo(self._user_lineedit.text(),
+ self._password_lineedit.text())
return True
def item_focus(self, which):
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index ca75be1ac..e8aacc935 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -19,6 +19,7 @@
"""The main statusbar widget."""
+import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
@@ -31,6 +32,7 @@ from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
from qutebrowser.mainwindow.statusbar import text as textwidget
+@attr.s
class ColorFlags:
"""Flags which change the appearance of the statusbar.
@@ -44,13 +46,11 @@ class ColorFlags:
"""
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
-
- def __init__(self):
- self.prompt = False
- self.insert = False
- self.command = False
- self.caret = self.CaretMode.off
- self.private = False
+ prompt = attr.ib(False)
+ insert = attr.ib(False)
+ command = attr.ib(False)
+ caret = attr.ib(CaretMode.off)
+ private = attr.ib(False)
def to_stringlist(self):
"""Get a string list of set flags used in the stylesheet.
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index acf966cd5..13865ba90 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -20,8 +20,8 @@
"""The main tabbed browser widget."""
import functools
-import collections
+import attr
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
from PyQt5.QtGui import QIcon
@@ -34,8 +34,15 @@ from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message, jinja)
-UndoEntry = collections.namedtuple('UndoEntry',
- ['url', 'history', 'index', 'pinned'])
+@attr.s
+class UndoEntry:
+
+ """Information needed for :undo."""
+
+ url = attr.ib()
+ history = attr.ib()
+ index = attr.ib()
+ pinned = attr.ib()
class TabDeletedError(Exception):
@@ -64,7 +71,7 @@ class TabbedBrowser(tabwidget.TabWidget):
_tab_insert_idx_left: Where to insert a new tab with
tabs.new_tab_position set to 'prev'.
_tab_insert_idx_right: Same as above, for 'next'.
- _undo_stack: List of UndoEntry namedtuples of closed tabs.
+ _undo_stack: List of UndoEntry objects of closed tabs.
shutting_down: Whether we're currently shutting down.
_local_marks: Jump markers local to each page
_global_marks: Jump markers used across all pages
@@ -352,16 +359,16 @@ class TabbedBrowser(tabwidget.TabWidget):
use_current_tab = (only_one_tab_open and no_history and
last_close_url_used)
- url, history_data, idx, pinned = self._undo_stack.pop()
+ entry = self._undo_stack.pop()
if use_current_tab:
- self.openurl(url, newtab=False)
+ self.openurl(entry.url, newtab=False)
newtab = self.widget(0)
else:
- newtab = self.tabopen(url, background=False, idx=idx)
+ newtab = self.tabopen(entry.url, background=False, idx=entry.index)
- newtab.history.deserialize(history_data)
- self.set_tab_pinned(newtab, pinned)
+ newtab.history.deserialize(entry.history)
+ self.set_tab_pinned(newtab, entry.pinned)
@pyqtSlot('QUrl', bool)
def openurl(self, url, newtab):
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index f86699589..111be2931 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -19,9 +19,9 @@
"""The tab widget used for TabbedBrowser from browser.py."""
-import collections
import functools
+import attr
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,
QTimer, QUrl)
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
@@ -592,8 +592,17 @@ class TabBar(QTabBar):
tabbed_browser.wheelEvent(e)
-# Used by TabBarStyle._tab_layout().
-Layouts = collections.namedtuple('Layouts', ['text', 'icon', 'indicator'])
+@attr.s
+class Layouts:
+
+ """Layout information for tab.
+
+ Used by TabBarStyle._tab_layout().
+ """
+
+ text = attr.ib()
+ icon = attr.ib()
+ indicator = attr.ib()
class TabBarStyle(QCommonStyle):
@@ -765,7 +774,7 @@ class TabBarStyle(QCommonStyle):
opt: QStyleOptionTab
Return:
- A Layout namedtuple with two QRects.
+ A Layout object with two QRects.
"""
padding = config.val.tabs.padding
indicator_padding = config.val.tabs.indicator_padding
diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py
index e51855af2..393aa26f0 100644
--- a/qutebrowser/misc/crashsignal.py
+++ b/qutebrowser/misc/crashsignal.py
@@ -27,13 +27,13 @@ import signal
import functools
import faulthandler
import os.path
-import collections
try:
# WORKAROUND for segfaults when using pdb in pytest for some reason...
import readline # pylint: disable=unused-import
except ImportError:
pass
+import attr
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl)
from PyQt5.QtWidgets import QApplication, QDialog
@@ -43,8 +43,14 @@ from qutebrowser.misc import earlyinit, crashdialog
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug
-ExceptionInfo = collections.namedtuple('ExceptionInfo',
- 'pages, cmd_history, objects')
+@attr.s
+class ExceptionInfo:
+
+ """Information stored when there was an exception."""
+
+ pages = attr.ib()
+ cmd_history = attr.ib()
+ objects = attr.ib()
# Used by mainwindow.py to skip confirm questions on crashes
@@ -172,7 +178,7 @@ class CrashHandler(QObject):
"""Get info needed for the exception hook/dialog.
Return:
- An ExceptionInfo namedtuple.
+ An ExceptionInfo object.
"""
try:
pages = self._recover_pages(forgiving=True)
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index 058a66882..032dfc53b 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -261,6 +261,9 @@ def check_libraries():
"http://pyyaml.org/download/pyyaml/ (py3.4) "
"or Install via pip.",
pip="PyYAML"),
+ 'attr':
+ _missing_str("attrs",
+ pip="attrs"),
'PyQt5.QtQml': _missing_str("PyQt5.QtQml"),
'PyQt5.QtSql': _missing_str("PyQt5.QtSql"),
'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"),
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 23c37a042..b60b1bc13 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -31,6 +31,7 @@ import contextlib
import socket
import shlex
+import attr
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices
from PyQt5.QtWidgets import QApplication
@@ -409,6 +410,7 @@ def keyevent_to_string(e):
return normalize_keystr('+'.join(parts))
+@attr.s(repr=False)
class KeyInfo:
"""Stores information about a key, like used in a QKeyEvent.
@@ -419,10 +421,9 @@ class KeyInfo:
text: str
"""
- def __init__(self, key, modifiers, text):
- self.key = key
- self.modifiers = modifiers
- self.text = text
+ key = attr.ib()
+ modifiers = attr.ib()
+ text = attr.ib()
def __repr__(self):
if self.modifiers is None:
@@ -434,10 +435,6 @@ class KeyInfo:
key=debug.qenum_key(Qt, self.key),
modifiers=modifiers, text=self.text)
- def __eq__(self, other):
- return (self.key == other.key and self.modifiers == other.modifiers and
- self.text == other.text)
-
class KeyParseError(Exception):
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 75ac197fa..673590208 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -29,6 +29,7 @@ import importlib
import collections
import pkg_resources
+import attr
from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo
from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtWidgets import QApplication
@@ -49,8 +50,15 @@ from qutebrowser.misc import objects, earlyinit, sql
from qutebrowser.browser import pdfjs
-DistributionInfo = collections.namedtuple(
- 'DistributionInfo', ['id', 'parsed', 'version', 'pretty'])
+@attr.s
+class DistributionInfo:
+
+ """Information about the running distribution."""
+
+ id = attr.ib()
+ parsed = attr.ib()
+ version = attr.ib()
+ pretty = attr.ib()
Distribution = usertypes.enum(
@@ -190,24 +198,25 @@ def _module_versions():
('pygments', ['__version__']),
('yaml', ['__version__']),
('cssutils', ['__version__']),
+ ('attr', ['__version__']),
('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebKitWidgets', []),
])
- for name, attributes in modules.items():
+ for modname, attributes in modules.items():
try:
- module = importlib.import_module(name)
+ module = importlib.import_module(modname)
except ImportError:
- text = '{}: no'.format(name)
+ text = '{}: no'.format(modname)
else:
- for attr in attributes:
+ for name in attributes:
try:
- text = '{}: {}'.format(name, getattr(module, attr))
+ text = '{}: {}'.format(modname, getattr(module, name))
except AttributeError:
pass
else:
break
else:
- text = '{}: yes'.format(name)
+ text = '{}: yes'.format(modname)
lines.append(text)
return lines
diff --git a/requirements.txt b/requirements.txt
index b2cc93c1f..6a4739aba 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
+attrs==17.2.0
colorama==0.3.9
cssutils==1.0.2
Jinja2==2.9.6
diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py
index 0848c404b..dfd336d16 100644
--- a/scripts/dev/check_coverage.py
+++ b/scripts/dev/check_coverage.py
@@ -25,16 +25,26 @@ import sys
import enum
import os.path
import subprocess
-import collections
-
from xml.etree import ElementTree
+import attr
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir))
from scripts import utils
-Message = collections.namedtuple('Message', 'typ, filename, text')
+
+@attr.s
+class Message:
+
+ """A message shown by coverage.py."""
+
+ typ = attr.ib()
+ filename = attr.ib()
+ text = attr.ib()
+
+
MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file')
diff --git a/scripts/dev/get_coredumpctl_traces.py b/scripts/dev/get_coredumpctl_traces.py
index 8cae2a190..6d1a41a11 100644
--- a/scripts/dev/get_coredumpctl_traces.py
+++ b/scripts/dev/get_coredumpctl_traces.py
@@ -24,17 +24,29 @@ import os
import sys
import argparse
import subprocess
-import collections
import os.path
import tempfile
+import attr
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir))
from scripts import utils
-Line = collections.namedtuple('Line', 'time, pid, uid, gid, sig, present, exe')
+@attr.s
+class Line:
+
+ """A line in "coredumpctl list"."""
+
+ time = attr.ib()
+ pid = attr.ib()
+ uid = attr.ib()
+ gid = attr.ib()
+ sig = attr.ib()
+ present = attr.ib()
+ exe = attr.ib()
def _convert_present(data):
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index c747787f6..7e5713615 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -115,6 +115,12 @@ def whitelist_generator():
'_get_default_metavar_for_positional', '_metavar_formatter']:
yield 'scripts.dev.src2asciidoc.UsageFormatter.' + attr
+ # attrs
+ yield 'qutebrowser.browser.webkit.network.networkmanager.ProxyId.hostname'
+ yield 'qutebrowser.command.command.ArgInfo._validate_exclusive'
+ yield 'scripts.get_coredumpctl_traces.Line.uid'
+ yield 'scripts.get_coredumpctl_traces.Line.gid'
+
def filter_func(item):
"""Check if a missing function should be filtered or not.
diff --git a/setup.py b/setup.py
index f594009a0..7bfd968f6 100755
--- a/setup.py
+++ b/setup.py
@@ -44,7 +44,7 @@ try:
['qutebrowser = qutebrowser.qutebrowser:main']},
test_suite='qutebrowser.test',
zip_safe=True,
- install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML'],
+ install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
**common.setupdata
)
finally:
diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py
index 68b30a105..e955937bc 100644
--- a/tests/end2end/fixtures/testprocess.py
+++ b/tests/end2end/fixtures/testprocess.py
@@ -23,6 +23,7 @@ import re
import os
import time
+import attr
import pytest
import pytestqt.plugin
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QProcess, QObject,
@@ -58,6 +59,7 @@ class BlacklistedMessageError(Exception):
"""Raised when ensure_not_logged found a message."""
+@attr.s
class Line:
"""Container for a line of data the process emits.
@@ -67,12 +69,8 @@ class Line:
waited_for: If Process.wait_for was used on this line already.
"""
- def __init__(self, data):
- self.data = data
- self.waited_for = False
-
- def __repr__(self):
- return '{}({!r})'.format(self.__class__.__name__, self.data)
+ data = attr.ib()
+ waited_for = attr.ib(False)
def _render_log(data, threshold=100):
diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py
index 01887d9b9..d9d8f5eca 100644
--- a/tests/end2end/fixtures/webserver.py
+++ b/tests/end2end/fixtures/webserver.py
@@ -25,6 +25,7 @@ import json
import os.path
import http.client
+import attr
import pytest
from PyQt5.QtCore import pyqtSignal, QUrl
@@ -99,13 +100,13 @@ class Request(testprocess.Line):
return NotImplemented
+@attr.s(frozen=True, cmp=False, hash=True)
class ExpectedRequest:
"""Class to compare expected requests easily."""
- def __init__(self, verb, path):
- self.verb = verb
- self.path = path
+ verb = attr.ib()
+ path = attr.ib()
@classmethod
def from_request(cls, request):
@@ -118,13 +119,6 @@ class ExpectedRequest:
else:
return NotImplemented
- def __hash__(self):
- return hash(('ExpectedRequest', self.verb, self.path))
-
- def __repr__(self):
- return ('ExpectedRequest(verb={!r}, path={!r})'
- .format(self.verb, self.path))
-
class WebserverProcess(testprocess.Process):
diff --git a/tests/end2end/test_dirbrowser.py b/tests/end2end/test_dirbrowser.py
index 0a2c3d7b9..83fa1d4c0 100644
--- a/tests/end2end/test_dirbrowser.py
+++ b/tests/end2end/test_dirbrowser.py
@@ -20,8 +20,8 @@
"""Test the built-in directory browser."""
import os
-import collections
+import attr
import pytest
import bs4
@@ -101,8 +101,21 @@ class DirLayout:
return os.path.normpath(str(self.base))
-Parsed = collections.namedtuple('Parsed', 'path, parent, folders, files')
-Item = collections.namedtuple('Item', 'path, link, text')
+@attr.s
+class Parsed:
+
+ path = attr.ib()
+ parent = attr.ib()
+ folders = attr.ib()
+ files = attr.ib()
+
+
+@attr.s
+class Item:
+
+ path = attr.ib()
+ link = attr.ib()
+ text = attr.ib()
def parse(quteproc):
@@ -182,6 +195,7 @@ def test_enter_folder_smoke(dir_layout, quteproc):
@pytest.mark.parametrize('folder', DirLayout.layout_folders())
def test_enter_folder(dir_layout, quteproc, folder):
+ # pylint: disable=not-an-iterable
quteproc.open_url(dir_layout.file_url())
quteproc.click_element_by_text(text=folder)
expected_url = urlutils.file_url(dir_layout.path(folder))
diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py
index 7ba13f7d0..abc106505 100644
--- a/tests/end2end/test_hints_html.py
+++ b/tests/end2end/test_hints_html.py
@@ -22,8 +22,8 @@
import os
import os.path
import textwrap
-import collections
+import attr
import yaml
import pytest
import bs4
@@ -36,8 +36,11 @@ def collect_tests():
return files
-ParsedFile = collections.namedtuple('ParsedFile', ['target',
- 'qtwebengine_todo'])
+@attr.s
+class ParsedFile:
+
+ target = attr.ib()
+ qtwebengine_todo = attr.ib()
class InvalidFile(Exception):
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index 08aea97f6..496eb486d 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -26,13 +26,13 @@ See https://pytest.org/latest/fixture.html
import sys
-import collections
import tempfile
import itertools
import textwrap
import unittest.mock
import types
+import attr
import pytest
import py.path # pylint: disable=no-name-in-module
@@ -53,7 +53,12 @@ class WinRegistryHelper:
"""Helper class for win_registry."""
- FakeWindow = collections.namedtuple('FakeWindow', ['registry'])
+ @attr.s
+ class FakeWindow:
+
+ """A fake window object for the registry."""
+
+ registry = attr.ib()
def __init__(self):
self._ids = []
@@ -161,8 +166,12 @@ def fake_web_tab(stubs, tab_registry, mode_manager, qapp):
def _generate_cmdline_tests():
"""Generate testcases for test_split_binding."""
- # pylint: disable=invalid-name
- TestCase = collections.namedtuple('TestCase', 'cmd, valid')
+ @attr.s
+ class TestCase:
+
+ cmd = attr.ib()
+ valid = attr.ib()
+
separators = [';;', ' ;; ', ';; ', ' ;;']
invalid = ['foo', '']
valid = ['leave-mode', 'hint all']
diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py
index 77116115f..6fad75281 100644
--- a/tests/helpers/messagemock.py
+++ b/tests/helpers/messagemock.py
@@ -20,14 +20,20 @@
"""pytest helper to monkeypatch the message module."""
import logging
-import collections
+import attr
import pytest
from qutebrowser.utils import usertypes, message
-Message = collections.namedtuple('Message', ['level', 'text'])
+@attr.s
+class Message:
+
+ """Information about a shown message."""
+
+ level = attr.ib()
+ text = attr.ib()
class MessageMock:
@@ -35,8 +41,8 @@ class MessageMock:
"""Helper object for message_mock.
Attributes:
- Message: A namedtuple representing a message.
- messages: A list of Message tuples.
+ Message: A object representing a message.
+ messages: A list of Message objects.
"""
def __init__(self):
diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py
index d7dcb427f..1b386c201 100644
--- a/tests/helpers/stubs.py
+++ b/tests/helpers/stubs.py
@@ -23,6 +23,7 @@
from unittest import mock
+import attr
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject
from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
QNetworkCacheMetaData)
@@ -202,9 +203,9 @@ class FakeNetworkReply:
def fake_qprocess():
"""Factory for a QProcess mock which has the QProcess enum values."""
m = mock.Mock(spec=QProcess)
- for attr in ['NormalExit', 'CrashExit', 'FailedToStart', 'Crashed',
+ for name in ['NormalExit', 'CrashExit', 'FailedToStart', 'Crashed',
'Timedout', 'WriteError', 'ReadError', 'UnknownError']:
- setattr(m, attr, getattr(QProcess, attr))
+ setattr(m, name, getattr(QProcess, name))
return m
@@ -315,27 +316,26 @@ class FakeSignal:
pass
+@attr.s
class FakeCmdUtils:
"""Stub for cmdutils which provides a cmd_dict."""
- def __init__(self, commands):
- self.cmd_dict = commands
+ cmd_dict = attr.ib()
+@attr.s(frozen=True)
class FakeCommand:
"""A simple command stub which has a description."""
- def __init__(self, name='', desc='', hide=False, debug=False,
- deprecated=False, completion=None, maxsplit=None):
- self.desc = desc
- self.name = name
- self.hide = hide
- self.debug = debug
- self.deprecated = deprecated
- self.completion = completion
- self.maxsplit = maxsplit
+ name = attr.ib('')
+ desc = attr.ib('')
+ hide = attr.ib(False)
+ debug = attr.ib(False)
+ deprecated = attr.ib(False)
+ completion = attr.ib(None)
+ maxsplit = attr.ib(None)
class FakeTimer(QObject):
diff --git a/tests/unit/browser/test_signalfilter.py b/tests/unit/browser/test_signalfilter.py
index b56e4ecc0..9d191e7c6 100644
--- a/tests/unit/browser/test_signalfilter.py
+++ b/tests/unit/browser/test_signalfilter.py
@@ -19,9 +19,9 @@
"""Tests for browser.signalfilter."""
-import collections
import logging
+import attr
import pytest
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
@@ -47,7 +47,11 @@ class Signaller(QObject):
self.filtered_signal_arg = s
-Objects = collections.namedtuple('Objects', 'signal_filter, signaller')
+@attr.s
+class Objects:
+
+ signal_filter = attr.ib()
+ signaller = attr.ib()
@pytest.fixture
diff --git a/tests/unit/browser/webkit/network/test_filescheme.py b/tests/unit/browser/webkit/network/test_filescheme.py
index 7a1bd9b19..0cc485556 100644
--- a/tests/unit/browser/webkit/network/test_filescheme.py
+++ b/tests/unit/browser/webkit/network/test_filescheme.py
@@ -18,8 +18,8 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import os
-import collections
+import attr
import pytest
import bs4
from PyQt5.QtCore import QUrl
@@ -109,8 +109,18 @@ def _file_url(path):
class TestDirbrowserHtml:
- Parsed = collections.namedtuple('Parsed', 'parent, folders, files')
- Item = collections.namedtuple('Item', 'link, text')
+ @attr.s
+ class Parsed:
+
+ parent = attr.ib()
+ folders = attr.ib()
+ files = attr.ib()
+
+ @attr.s
+ class Item:
+
+ link = attr.ib()
+ text = attr.ib()
@pytest.fixture
def parser(self):
diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py
index 07b334771..67602db4c 100644
--- a/tests/unit/browser/webkit/test_tabhistory.py
+++ b/tests/unit/browser/webkit/test_tabhistory.py
@@ -19,8 +19,7 @@
"""Tests for webelement.tabhistory."""
-import collections
-
+import attr
from PyQt5.QtCore import QUrl, QPoint
import pytest
@@ -50,7 +49,11 @@ ITEMS = [
]
-Objects = collections.namedtuple('Objects', 'history, user_data')
+@attr.s
+class Objects:
+
+ history = attr.ib()
+ user_data = attr.ib()
@pytest.fixture
diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py
index 9b3407821..871241a04 100644
--- a/tests/unit/browser/webkit/test_webkitelem.py
+++ b/tests/unit/browser/webkit/test_webkitelem.py
@@ -24,6 +24,7 @@ import collections.abc
import operator
import itertools
+import attr
import pytest
from PyQt5.QtCore import QRect, QPoint, QUrl
QWebElement = pytest.importorskip('PyQt5.QtWebKit').QWebElement
@@ -525,7 +526,12 @@ class TestIsVisibleIframe:
elem1-elem4: FakeWebElements to test.
"""
- Objects = collections.namedtuple('Objects', ['frame', 'iframe', 'elems'])
+ @attr.s
+ class Objects:
+
+ frame = attr.ib()
+ iframe = attr.ib()
+ elems = attr.ib()
@pytest.fixture
def objects(self, stubs):
@@ -550,7 +556,7 @@ class TestIsVisibleIframe:
##############################
300, 0 300, 300
- Returns an Objects namedtuple with frame/iframe/elems attributes.
+ Returns an Objects object with frame/iframe/elems attributes.
"""
frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300))
iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame)
@@ -621,7 +627,7 @@ class TestIsVisibleIframe:
##############################
300, 0 300, 300
- Returns an Objects namedtuple with frame/iframe/elems attributes.
+ Returns an Objects object with frame/iframe/elems attributes.
"""
frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300))
iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame)
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index 861c052c2..122a1c20c 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -21,12 +21,12 @@
import re
import json
import math
-import collections
import itertools
import warnings
import inspect
import functools
+import attr
import pytest
import hypothesis
from hypothesis import strategies
@@ -54,15 +54,14 @@ class Font(QFont):
@classmethod
def fromdesc(cls, desc):
"""Get a Font based on a font description."""
- style, weight, ptsize, pxsize, family = desc
f = cls()
- f.setStyle(style)
- f.setWeight(weight)
- if ptsize is not None and ptsize != -1:
- f.setPointSize(ptsize)
- if pxsize is not None and ptsize != -1:
- f.setPixelSize(pxsize)
- f.setFamily(family)
+ f.setStyle(desc.style)
+ f.setWeight(desc.weight)
+ if desc.pt is not None and desc.pt != -1:
+ f.setPointSize(desc.pt)
+ if desc.px is not None and desc.pt != -1:
+ f.setPixelSize(desc.px)
+ f.setFamily(desc.family)
return f
@@ -1195,8 +1194,14 @@ class TestColors:
klass().to_py(val)
-FontDesc = collections.namedtuple('FontDesc',
- ['style', 'weight', 'pt', 'px', 'family'])
+@attr.s
+class FontDesc:
+
+ style = attr.ib()
+ weight = attr.ib()
+ pt = attr.ib()
+ px = attr.ib()
+ family = attr.ib()
class TestFont:
diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py
index a6a663a0d..2b7c17e62 100644
--- a/tests/unit/misc/test_ipc.py
+++ b/tests/unit/misc/test_ipc.py
@@ -22,12 +22,12 @@
import sys
import os
import getpass
-import collections
import logging
import json
import hashlib
from unittest import mock
+import attr
import pytest
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtNetwork import QLocalServer, QLocalSocket, QAbstractSocket
@@ -597,8 +597,13 @@ def test_ipcserver_socket_none_error(ipc_server, caplog):
class TestSendOrListen:
- Args = collections.namedtuple('Args', 'no_err_windows, basedir, command, '
- 'target')
+ @attr.s
+ class Args:
+
+ no_err_windows = attr.ib()
+ basedir = attr.ib()
+ command = attr.ib()
+ target = attr.ib()
@pytest.fixture
def args(self):
@@ -623,10 +628,10 @@ class TestSendOrListen:
def qlocalsocket_mock(self, mocker):
m = mocker.patch('qutebrowser.misc.ipc.QLocalSocket', autospec=True)
m().errorString.return_value = "Error string"
- for attr in ['UnknownSocketError', 'UnconnectedState',
+ for name in ['UnknownSocketError', 'UnconnectedState',
'ConnectionRefusedError', 'ServerNotFoundError',
'PeerClosedError']:
- setattr(m, attr, getattr(QLocalSocket, attr))
+ setattr(m, name, getattr(QLocalSocket, name))
return m
@pytest.mark.linux(reason="Flaky on Windows and macOS")
diff --git a/tests/unit/misc/test_split.py b/tests/unit/misc/test_split.py
index 179084849..eff6d2269 100644
--- a/tests/unit/misc/test_split.py
+++ b/tests/unit/misc/test_split.py
@@ -18,8 +18,8 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.misc.split."""
-import collections
+import attr
import pytest
from qutebrowser.misc import split
@@ -104,21 +104,26 @@ foo\ bar/foo bar/foo\ bar/
def _parse_split_test_data_str():
- """Parse the test data set into a namedtuple to use in tests.
+ """Parse the test data set into a TestCase object to use in tests.
Returns:
- A list of namedtuples with str attributes: input, keep, no_keep
+ A list of TestCase objects with str attributes: inp, keep, no_keep
"""
- tuple_class = collections.namedtuple('TestCase', 'input, keep, no_keep')
+ @attr.s
+ class TestCase:
+
+ inp = attr.ib()
+ keep = attr.ib()
+ no_keep = attr.ib()
for line in test_data_str.splitlines():
if not line:
continue
data = line.split('/')
- item = tuple_class(input=data[0], keep=data[1].split('|'),
- no_keep=data[2].split('|'))
+ item = TestCase(inp=data[0], keep=data[1].split('|'),
+ no_keep=data[2].split('|'))
yield item
- yield tuple_class(input='', keep=[], no_keep=[])
+ yield TestCase(inp='', keep=[], no_keep=[])
class TestSplit:
@@ -137,17 +142,17 @@ class TestSplit:
def test_split(self, split_test_case):
"""Test splitting."""
- items = split.split(split_test_case.input)
+ items = split.split(split_test_case.inp)
assert items == split_test_case.keep
def test_split_keep_original(self, split_test_case):
"""Test if splitting with keep=True yields the original string."""
- items = split.split(split_test_case.input, keep=True)
- assert ''.join(items) == split_test_case.input
+ items = split.split(split_test_case.inp, keep=True)
+ assert ''.join(items) == split_test_case.inp
def test_split_keep(self, split_test_case):
"""Test splitting with keep=True."""
- items = split.split(split_test_case.input, keep=True)
+ items = split.split(split_test_case.inp, keep=True)
assert items == split_test_case.no_keep
diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py
index d652310d5..5230c9c78 100644
--- a/tests/unit/utils/test_standarddir.py
+++ b/tests/unit/utils/test_standarddir.py
@@ -25,10 +25,10 @@ import json
import os.path
import types
import textwrap
-import collections
import logging
import subprocess
+import attr
from PyQt5.QtCore import QStandardPaths
import pytest
@@ -207,9 +207,6 @@ class TestStandardDir:
assert func().split(os.sep)[-elems:] == expected
-DirArgTest = collections.namedtuple('DirArgTest', 'arg, expected')
-
-
class TestArguments:
"""Tests the --basedir argument."""
@@ -370,10 +367,16 @@ class TestMoveWindowsAndMacOS:
@pytest.fixture
def files(self, tmpdir):
- files = collections.namedtuple('Files', [
- 'auto_config_dir', 'config_dir',
- 'local_data_dir', 'roaming_data_dir'])
- return files(
+
+ @attr.s
+ class Files:
+
+ auto_config_dir = attr.ib()
+ config_dir = attr.ib()
+ local_data_dir = attr.ib()
+ roaming_data_dir = attr.ib()
+
+ return Files(
auto_config_dir=tmpdir / 'auto_config' / APPNAME,
config_dir=tmpdir / 'config' / APPNAME,
local_data_dir=tmpdir / 'data' / APPNAME,
@@ -412,11 +415,17 @@ class TestMove:
@pytest.fixture
def dirs(self, tmpdir):
- dirs = collections.namedtuple('Dirs', ['old', 'new',
- 'old_file', 'new_file'])
+ @attr.s
+ class Dirs:
+
+ old = attr.ib()
+ new = attr.ib()
+ old_file = attr.ib()
+ new_file = attr.ib()
+
old_dir = tmpdir / 'old'
new_dir = tmpdir / 'new'
- return dirs(old=old_dir, new=new_dir,
+ return Dirs(old=old_dir, new=new_dir,
old_file=old_dir / 'file', new_file=new_dir / 'file')
def test_no_old_dir(self, dirs, caplog):
diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py
index a6b1242d1..875cc2967 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -20,9 +20,9 @@
"""Tests for qutebrowser.utils.urlutils."""
import os.path
-import collections
import logging
+import attr
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkProxy
import pytest
@@ -38,7 +38,7 @@ class FakeDNS:
"""Helper class for the fake_dns fixture.
Class attributes:
- FakeDNSAnswer: Helper class/namedtuple imitating a QHostInfo object
+ FakeDNSAnswer: Helper class imitating a QHostInfo object
(used by fromname_mock).
Attributes:
@@ -48,7 +48,10 @@ class FakeDNS:
when fromname_mock is called.
"""
- FakeDNSAnswer = collections.namedtuple('FakeDNSAnswer', ['error'])
+ @attr.s
+ class FakeDNSAnswer:
+
+ error = attr.ib()
def __init__(self):
self.used = False
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
index 255c88344..623f77cc7 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -25,11 +25,11 @@ import os.path
import io
import logging
import functools
-import collections
import socket
import re
import shlex
+import attr
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QColor, QClipboard
import pytest
@@ -157,7 +157,11 @@ class TestInterpolateColor:
white: The Color black as a valid Color for tests.
"""
- Colors = collections.namedtuple('Colors', ['white', 'black'])
+ @attr.s
+ class Colors:
+
+ white = attr.ib()
+ black = attr.ib()
@pytest.fixture
def colors(self):
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 4d977540d..4983a4839 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -32,6 +32,7 @@ import logging
import textwrap
import pkg_resources
+import attr
import pytest
import qutebrowser
@@ -474,8 +475,8 @@ def test_path_info(monkeypatch, equal):
'runtime': lambda: 'RUNTIME PATH',
}
- for attr, val in patches.items():
- monkeypatch.setattr(version.standarddir, attr, val)
+ for name, val in patches.items():
+ monkeypatch.setattr(version.standarddir, name, val)
pathinfo = version._path_info()
@@ -515,6 +516,7 @@ class ImportFake:
('pygments', True),
('yaml', True),
('cssutils', True),
+ ('attr', True),
('PyQt5.QtWebEngineWidgets', True),
('PyQt5.QtWebKitWidgets', True),
])
@@ -630,6 +632,7 @@ class TestModuleVersions:
('pygments', True),
('yaml', True),
('cssutils', True),
+ ('attr', True),
])
def test_existing_attributes(self, name, has_version):
"""Check if all dependencies have an expected __version__ attribute.
@@ -817,17 +820,16 @@ def test_chromium_version_unpatched(qapp):
assert version._chromium_version() not in ['', 'unknown', 'unavailable']
+@attr.s
class VersionParams:
- def __init__(self, name, git_commit=True, frozen=False, style=True,
- with_webkit=True, known_distribution=True, ssl_support=True):
- self.name = name
- self.git_commit = git_commit
- self.frozen = frozen
- self.style = style
- self.with_webkit = with_webkit
- self.known_distribution = known_distribution
- self.ssl_support = ssl_support
+ name = attr.ib()
+ git_commit = attr.ib(True)
+ frozen = attr.ib(False)
+ style = attr.ib(True)
+ with_webkit = attr.ib(True)
+ known_distribution = attr.ib(True)
+ ssl_support = attr.ib(True)
@pytest.mark.parametrize('params', [
@@ -901,8 +903,8 @@ def test_version_output(params, stubs, monkeypatch):
substitutions['ssl'] = 'SSL VERSION' if params.ssl_support else 'no'
- for attr, val in patches.items():
- monkeypatch.setattr('qutebrowser.utils.version.' + attr, val)
+ for name, val in patches.items():
+ monkeypatch.setattr('qutebrowser.utils.version.' + name, val)
if params.frozen:
monkeypatch.setattr(sys, 'frozen', True, raising=False)