diff options
49 files changed, 453 insertions, 252 deletions
@@ -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. @@ -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) |