Start using attrs

Closes #1073
This commit is contained in:
Florian Bruhin 2017-09-19 22:18:02 +02:00
parent 7226750363
commit 3a5241b642
49 changed files with 453 additions and 252 deletions

View File

@ -43,6 +43,7 @@ function-rgx=[a-z_][a-z0-9_]{2,50}$
const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$ const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
method-rgx=[a-z_][A-Za-z0-9_]{1,50}$ method-rgx=[a-z_][A-Za-z0-9_]{1,50}$
attr-rgx=[a-z_][a-z0-9_]{0,30}$ 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}$ argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$ variable-rgx=[a-z_][a-z0-9_]{0,30}$
docstring-min-length=3 docstring-min-length=3
@ -64,6 +65,7 @@ valid-metaclass-classmethod-first-arg=cls
[TYPECHECK] [TYPECHECK]
ignored-modules=PyQt5,PyQt5.QtWebKit ignored-modules=PyQt5,PyQt5.QtWebKit
ignored-classes=_CountingAttr
[IMPORTS] [IMPORTS]
# WORKAROUND # WORKAROUND

View File

@ -116,6 +116,7 @@ The following software and libraries are required to run qutebrowser:
* http://jinja.pocoo.org/[jinja2] * http://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments] * http://pygments.org/[pygments]
* http://pyyaml.org/wiki/PyYAML[PyYAML] * http://pyyaml.org/wiki/PyYAML[PyYAML]
* http://www.attrs.org/[attrs]
The following libraries are optional: The following libraries are optional:

View File

@ -27,6 +27,7 @@ Breaking changes
- (TODO) New dependency on ruamel.yaml; dropped PyYAML dependency. - (TODO) New dependency on ruamel.yaml; dropped PyYAML dependency.
- (TODO) The QtWebEngine backend is now used by default if available. - (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 QtSql module and Qt sqlite support.
- New dependency on the `attrs` Python module.
- New config system which ignores the old config file. - New config system which ignores the old config file.
- The depedency on PyOpenGL (when using QtWebEngine) got removed. Note - The depedency on PyOpenGL (when using QtWebEngine) got removed. Note
that PyQt5.QtOpenGL is still a dependency. that PyQt5.QtOpenGL is still a dependency.

View File

@ -4,3 +4,4 @@ pyPEG2
PyYAML PyYAML
colorama colorama
cssutils cssutils
attrs

View File

@ -40,6 +40,7 @@ git+https://github.com/pallets/jinja.git
git+https://github.com/pallets/markupsafe.git git+https://github.com/pallets/markupsafe.git
hg+http://bitbucket.org/birkenfeld/pygments-main hg+http://bitbucket.org/birkenfeld/pygments-main
hg+https://bitbucket.org/fdik/pypeg hg+https://bitbucket.org/fdik/pypeg
git+https://github.com/python-attrs/attrs.git
# Fails to build: # Fails to build:
# gcc: error: ext/_yaml.c: No such file or directory # gcc: error: ext/_yaml.c: No such file or directory

View File

@ -21,6 +21,7 @@
import itertools import itertools
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QApplication from PyQt5.QtWidgets import QWidget, QApplication
@ -82,6 +83,7 @@ TerminationStatus = usertypes.enum('TerminationStatus', [
]) ])
@attr.s
class TabData: class TabData:
"""A simple namespace with a fixed set of attributes. """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. fullscreen: Whether the tab has a video shown fullscreen currently.
""" """
def __init__(self): keep_icon = attr.ib(False)
self.keep_icon = False viewing_source = attr.ib(False)
self.viewing_source = False inspector = attr.ib(None)
self.inspector = None override_target = attr.ib(None)
self.override_target = None pinned = attr.ib(False)
self.pinned = False fullscreen = attr.ib(False)
self.fullscreen = False
class AbstractAction: class AbstractAction:

View File

@ -26,6 +26,7 @@ import re
import html import html
from string import ascii_lowercase from string import ascii_lowercase
import attr
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel from PyQt5.QtWidgets import QLabel
@ -131,6 +132,7 @@ class HintLabel(QLabel):
self.deleteLater() self.deleteLater()
@attr.s
class HintContext: class HintContext:
"""Context namespace used for hinting. """Context namespace used for hinting.
@ -158,19 +160,18 @@ class HintContext:
group: The group of web elements to hint. group: The group of web elements to hint.
""" """
def __init__(self): all_labels = attr.ib(attr.Factory(list))
self.all_labels = [] labels = attr.ib(attr.Factory(dict))
self.labels = {} target = attr.ib(None)
self.target = None baseurl = attr.ib(None)
self.baseurl = None to_follow = attr.ib(None)
self.to_follow = None rapid = attr.ib(False)
self.rapid = False add_history = attr.ib(False)
self.add_history = False filterstr = attr.ib(None)
self.filterstr = None args = attr.ib(attr.Factory(list))
self.args = [] tab = attr.ib(None)
self.tab = None group = attr.ib(None)
self.group = None hint_mode = attr.ib(None)
self.hint_mode = None
def get_args(self, urlstr): def get_args(self, urlstr):
"""Get the arguments, with {hint-url} replaced by the given URL.""" """Get the arguments, with {hint-url} replaced by the given URL."""
@ -389,6 +390,7 @@ class HintManager(QObject):
def _cleanup(self): def _cleanup(self):
"""Clean up after hinting.""" """Clean up after hinting."""
# pylint: disable=not-an-iterable
for label in self._context.all_labels: for label in self._context.all_labels:
label.cleanup() label.cleanup()
@ -795,6 +797,7 @@ class HintManager(QObject):
log.hints.debug("Filtering hints on {!r}".format(filterstr)) log.hints.debug("Filtering hints on {!r}".format(filterstr))
visible = [] visible = []
# pylint: disable=not-an-iterable
for label in self._context.all_labels: for label in self._context.all_labels:
try: try:
if self._filter_matches(filterstr, str(label.elem)): if self._filter_matches(filterstr, str(label.elem)):

View File

@ -22,8 +22,8 @@
import io import io
import shutil import shutil
import functools import functools
import collections
import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
@ -34,7 +34,11 @@ from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager 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): class DownloadItem(downloads.AbstractDownloadItem):

View File

@ -25,7 +25,6 @@ import io
import os import os
import re import re
import sys import sys
import collections
import uuid import uuid
import email.policy import email.policy
import email.generator import email.generator
@ -34,15 +33,21 @@ import email.mime.multipart
import email.message import email.message
import quopri import quopri
import attr
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import webkitelem from qutebrowser.browser.webkit import webkitelem
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
_File = collections.namedtuple('_File',
['content', 'content_type', 'content_location', @attr.s
'transfer_encoding']) 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 [ _CSS_URL_PATTERNS = [re.compile(x) for x in [
@ -174,7 +179,7 @@ class MHTMLWriter:
root_content: The root content as bytes. root_content: The root content as bytes.
content_location: The url of the page as str. content_location: The url of the page as str.
content_type: The MIME-type of the root content 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): def __init__(self, root_content, content_location, content_type):

View File

@ -24,6 +24,7 @@ import collections
import netrc import netrc
import html import html
import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
QByteArray) QByteArray)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
@ -37,10 +38,19 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%' HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
ProxyId = collections.namedtuple('ProxyId', 'type, hostname, port')
_proxy_auth_cache = {} _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): def _is_secure_cipher(cipher):
"""Check if a given SSL cipher (hopefully) isn't broken yet.""" """Check if a given SSL cipher (hopefully) isn't broken yet."""
tokens = [e.upper() for e in cipher.name().split('-')] tokens = [e.upper() for e in cipher.name().split('-')]

View File

@ -19,11 +19,11 @@
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header.""" """pyPEG parsing for the RFC 6266 (Content-Disposition) header."""
import collections
import urllib.parse import urllib.parse
import string import string
import re import re
import attr
import pypeg2 as peg import pypeg2 as peg
from qutebrowser.utils import utils from qutebrowser.utils import utils
@ -210,7 +210,13 @@ class ContentDispositionValue:
peg.optional(';')) 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): class Error(Exception):

View File

@ -24,43 +24,32 @@ import collections
import traceback import traceback
import typing import typing
import attr
from qutebrowser.commands import cmdexc, argparser from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import (log, utils, message, docutils, objreg, from qutebrowser.utils import log, message, docutils, objreg, usertypes
usertypes)
from qutebrowser.utils import debug as debug_utils from qutebrowser.utils import debug as debug_utils
from qutebrowser.misc import objects from qutebrowser.misc import objects
@attr.s
class ArgInfo: class ArgInfo:
"""Information about an argument.""" """Information about an argument."""
def __init__(self, win_id=False, count=False, hide=False, metavar=None, win_id = attr.ib(False)
flag=None, completion=None, choices=None): count = attr.ib(False)
if win_id and count: 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!") 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: class Command:

View File

@ -19,10 +19,10 @@
"""Module containing command managers (SearchRunner and CommandRunner).""" """Module containing command managers (SearchRunner and CommandRunner)."""
import collections
import traceback import traceback
import re import re
import attr
from PyQt5.QtCore import pyqtSlot, QUrl, QObject from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config from qutebrowser.config import config
@ -31,10 +31,19 @@ from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split from qutebrowser.misc import split
ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline'])
last_command = {} 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): def _current_url(tabbed_browser):
"""Convenience method to get the current url.""" """Convenience method to get the current url."""
try: try:

View File

@ -19,8 +19,7 @@
"""Completer attached to a CompletionView.""" """Completer attached to a CompletionView."""
import collections import attr
from PyQt5.QtCore import pyqtSlot, QObject, QTimer from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config from qutebrowser.config import config
@ -29,9 +28,13 @@ from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
# Context passed into all completion functions @attr.s
CompletionInfo = collections.namedtuple('CompletionInfo', class CompletionInfo:
['config', 'keyconf'])
"""Context passed into all completion functions."""
config = attr.ib()
keyconf = attr.ib()
class Completer(QObject): class Completer(QObject):
@ -130,7 +133,9 @@ class Completer(QObject):
return [], '', [] return [], '', []
parser = runners.CommandParser() parser = runners.CommandParser()
result = parser.parse(text, fallback=True, keep=True) result = parser.parse(text, fallback=True, keep=True)
# pylint: disable=not-an-iterable
parts = [x for x in result.cmdline if x] parts = [x for x in result.cmdline if x]
# pylint: enable=not-an-iterable
pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
log.completion.debug('partitioning {} around position {}'.format(parts, log.completion.debug('partitioning {} around position {}'.format(parts,

View File

@ -24,16 +24,29 @@ Module attributes:
DATA: A dict of Option objects after init() has been called. DATA: A dict of Option objects after init() has been called.
""" """
import collections
import functools import functools
import attr
from qutebrowser.config import configtypes from qutebrowser.config import configtypes
from qutebrowser.utils import usertypes, qtutils, utils from qutebrowser.utils import usertypes, qtutils, utils
DATA = None 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): def _raise_invalid_node(name, what, node):

View File

@ -19,7 +19,9 @@
"""Exceptions related to config parsing.""" """Exceptions related to config parsing."""
from qutebrowser.utils import utils, jinja import attr
from qutebrowser.utils import jinja
class Error(Exception): class Error(Exception):
@ -74,6 +76,7 @@ class NoOptionError(Error):
self.option = option self.option = option
@attr.s
class ConfigErrorDesc: class ConfigErrorDesc:
"""A description of an error happening while reading the config. """A description of an error happening while reading the config.
@ -84,13 +87,9 @@ class ConfigErrorDesc:
traceback: The formatted traceback of the exception. traceback: The formatted traceback of the exception.
""" """
def __init__(self, text, exception, traceback=None): text = attr.ib()
self.text = text exception = attr.ib()
self.exception = exception traceback = attr.ib(None)
self.traceback = traceback
def __repr__(self):
return utils.get_repr(self, text=self.text, exception=self.exception)
def __str__(self): def __str__(self):
return '{}: {}'.format(self.text, self.exception) return '{}: {}'.format(self.text, self.exception)

View File

@ -47,13 +47,13 @@ import html
import codecs import codecs
import os.path import os.path
import itertools import itertools
import collections
import warnings import warnings
import datetime import datetime
import functools import functools
import operator import operator
import json import json
import attr
import yaml import yaml
from PyQt5.QtCore import QUrl, Qt from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont from PyQt5.QtGui import QColor, QFont
@ -1360,8 +1360,15 @@ class FuzzyUrl(BaseType):
raise configexc.ValidationError(value, str(e)) raise configexc.ValidationError(value, str(e))
PaddingValues = collections.namedtuple('PaddingValues', ['top', 'bottom', @attr.s
'left', 'right']) class PaddingValues:
"""Four padding values."""
top = attr.ib()
bottom = attr.ib()
left = attr.ib()
right = attr.ib()
class Padding(Dict): class Padding(Dict):

View File

@ -21,6 +21,7 @@
import functools import functools
import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -31,6 +32,7 @@ from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.misc import objects from qutebrowser.misc import objects
@attr.s(frozen=True)
class KeyEvent: class KeyEvent:
"""A small wrapper over a QKeyEvent storing its data. """A small wrapper over a QKeyEvent storing its data.
@ -44,18 +46,13 @@ class KeyEvent:
text: A string (QKeyEvent::text). text: A string (QKeyEvent::text).
""" """
def __init__(self, keyevent): key = attr.ib()
self.key = keyevent.key() text = attr.ib()
self.text = keyevent.text()
def __repr__(self): @classmethod
return utils.get_repr(self, key=self.key, text=self.text) def from_event(cls, event):
"""Initialize a KeyEvent from a QKeyEvent."""
def __eq__(self, other): return cls(event.key(), event.text())
return self.key == other.key and self.text == other.text
def __hash__(self):
return hash((self.key, self.text))
class NotInModeError(Exception): class NotInModeError(Exception):
@ -179,7 +176,7 @@ class ModeManager(QObject):
filter_this = True filter_this = True
if not filter_this: 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: if curmode != usertypes.KeyMode.insert:
focus_widget = QApplication.instance().focusWidget() focus_widget = QApplication.instance().focusWidget()
@ -201,7 +198,7 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise. True if event should be filtered, False otherwise.
""" """
# handle like matching KeyPress # handle like matching KeyPress
keyevent = KeyEvent(event) keyevent = KeyEvent.from_event(event)
if keyevent in self._releaseevents_to_pass: if keyevent in self._releaseevents_to_pass:
self._releaseevents_to_pass.remove(keyevent) self._releaseevents_to_pass.remove(keyevent)
filter_this = False filter_this = False

View File

@ -23,6 +23,7 @@ import os.path
import html import html
import collections import collections
import attr
import sip import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
QItemSelectionModel, QObject, QEventLoop) QItemSelectionModel, QObject, QEventLoop)
@ -39,7 +40,13 @@ from qutebrowser.commands import cmdutils, cmdexc
prompt_queue = None 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): class Error(Exception):
@ -750,7 +757,7 @@ class AuthenticationPrompt(_BasePrompt):
"username:password, but {} was given".format( "username:password, but {} was given".format(
value)) value))
username, password = value.split(':', maxsplit=1) username, password = value.split(':', maxsplit=1)
self.question.answer = AuthTuple(username, password) self.question.answer = AuthInfo(username, password)
return True return True
elif self._user_lineedit.hasFocus(): elif self._user_lineedit.hasFocus():
# Earlier, tab was bound to :prompt-accept, so to still support # Earlier, tab was bound to :prompt-accept, so to still support
@ -758,8 +765,8 @@ class AuthenticationPrompt(_BasePrompt):
self._password_lineedit.setFocus() self._password_lineedit.setFocus()
return False return False
else: else:
self.question.answer = AuthTuple(self._user_lineedit.text(), self.question.answer = AuthInfo(self._user_lineedit.text(),
self._password_lineedit.text()) self._password_lineedit.text())
return True return True
def item_focus(self, which): def item_focus(self, which):

View File

@ -19,6 +19,7 @@
"""The main statusbar widget.""" """The main statusbar widget."""
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy 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 from qutebrowser.mainwindow.statusbar import text as textwidget
@attr.s
class ColorFlags: class ColorFlags:
"""Flags which change the appearance of the statusbar. """Flags which change the appearance of the statusbar.
@ -44,13 +46,11 @@ class ColorFlags:
""" """
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection']) CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
prompt = attr.ib(False)
def __init__(self): insert = attr.ib(False)
self.prompt = False command = attr.ib(False)
self.insert = False caret = attr.ib(CaretMode.off)
self.command = False private = attr.ib(False)
self.caret = self.CaretMode.off
self.private = False
def to_stringlist(self): def to_stringlist(self):
"""Get a string list of set flags used in the stylesheet. """Get a string list of set flags used in the stylesheet.

View File

@ -20,8 +20,8 @@
"""The main tabbed browser widget.""" """The main tabbed browser widget."""
import functools import functools
import collections
import attr
from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
@ -34,8 +34,15 @@ from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message, jinja) urlutils, message, jinja)
UndoEntry = collections.namedtuple('UndoEntry', @attr.s
['url', 'history', 'index', 'pinned']) class UndoEntry:
"""Information needed for :undo."""
url = attr.ib()
history = attr.ib()
index = attr.ib()
pinned = attr.ib()
class TabDeletedError(Exception): class TabDeletedError(Exception):
@ -64,7 +71,7 @@ class TabbedBrowser(tabwidget.TabWidget):
_tab_insert_idx_left: Where to insert a new tab with _tab_insert_idx_left: Where to insert a new tab with
tabs.new_tab_position set to 'prev'. tabs.new_tab_position set to 'prev'.
_tab_insert_idx_right: Same as above, for 'next'. _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. shutting_down: Whether we're currently shutting down.
_local_marks: Jump markers local to each page _local_marks: Jump markers local to each page
_global_marks: Jump markers used across all pages _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 use_current_tab = (only_one_tab_open and no_history and
last_close_url_used) last_close_url_used)
url, history_data, idx, pinned = self._undo_stack.pop() entry = self._undo_stack.pop()
if use_current_tab: if use_current_tab:
self.openurl(url, newtab=False) self.openurl(entry.url, newtab=False)
newtab = self.widget(0) newtab = self.widget(0)
else: else:
newtab = self.tabopen(url, background=False, idx=idx) newtab = self.tabopen(entry.url, background=False, idx=entry.index)
newtab.history.deserialize(history_data) newtab.history.deserialize(entry.history)
self.set_tab_pinned(newtab, pinned) self.set_tab_pinned(newtab, entry.pinned)
@pyqtSlot('QUrl', bool) @pyqtSlot('QUrl', bool)
def openurl(self, url, newtab): def openurl(self, url, newtab):

View File

@ -19,9 +19,9 @@
"""The tab widget used for TabbedBrowser from browser.py.""" """The tab widget used for TabbedBrowser from browser.py."""
import collections
import functools import functools
import attr
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,
QTimer, QUrl) QTimer, QUrl)
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
@ -592,8 +592,17 @@ class TabBar(QTabBar):
tabbed_browser.wheelEvent(e) tabbed_browser.wheelEvent(e)
# Used by TabBarStyle._tab_layout(). @attr.s
Layouts = collections.namedtuple('Layouts', ['text', 'icon', 'indicator']) class Layouts:
"""Layout information for tab.
Used by TabBarStyle._tab_layout().
"""
text = attr.ib()
icon = attr.ib()
indicator = attr.ib()
class TabBarStyle(QCommonStyle): class TabBarStyle(QCommonStyle):
@ -765,7 +774,7 @@ class TabBarStyle(QCommonStyle):
opt: QStyleOptionTab opt: QStyleOptionTab
Return: Return:
A Layout namedtuple with two QRects. A Layout object with two QRects.
""" """
padding = config.val.tabs.padding padding = config.val.tabs.padding
indicator_padding = config.val.tabs.indicator_padding indicator_padding = config.val.tabs.indicator_padding

View File

@ -27,13 +27,13 @@ import signal
import functools import functools
import faulthandler import faulthandler
import os.path import os.path
import collections
try: try:
# WORKAROUND for segfaults when using pdb in pytest for some reason... # WORKAROUND for segfaults when using pdb in pytest for some reason...
import readline # pylint: disable=unused-import import readline # pylint: disable=unused-import
except ImportError: except ImportError:
pass pass
import attr
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject, from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl) QSocketNotifier, QTimer, QUrl)
from PyQt5.QtWidgets import QApplication, QDialog 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 from qutebrowser.utils import usertypes, standarddir, log, objreg, debug
ExceptionInfo = collections.namedtuple('ExceptionInfo', @attr.s
'pages, cmd_history, objects') 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 # 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. """Get info needed for the exception hook/dialog.
Return: Return:
An ExceptionInfo namedtuple. An ExceptionInfo object.
""" """
try: try:
pages = self._recover_pages(forgiving=True) pages = self._recover_pages(forgiving=True)

View File

@ -261,6 +261,9 @@ def check_libraries():
"http://pyyaml.org/download/pyyaml/ (py3.4) " "http://pyyaml.org/download/pyyaml/ (py3.4) "
"or Install via pip.", "or Install via pip.",
pip="PyYAML"), pip="PyYAML"),
'attr':
_missing_str("attrs",
pip="attrs"),
'PyQt5.QtQml': _missing_str("PyQt5.QtQml"), 'PyQt5.QtQml': _missing_str("PyQt5.QtQml"),
'PyQt5.QtSql': _missing_str("PyQt5.QtSql"), 'PyQt5.QtSql': _missing_str("PyQt5.QtSql"),
'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"), 'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"),

View File

@ -31,6 +31,7 @@ import contextlib
import socket import socket
import shlex import shlex
import attr
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -409,6 +410,7 @@ def keyevent_to_string(e):
return normalize_keystr('+'.join(parts)) return normalize_keystr('+'.join(parts))
@attr.s(repr=False)
class KeyInfo: class KeyInfo:
"""Stores information about a key, like used in a QKeyEvent. """Stores information about a key, like used in a QKeyEvent.
@ -419,10 +421,9 @@ class KeyInfo:
text: str text: str
""" """
def __init__(self, key, modifiers, text): key = attr.ib()
self.key = key modifiers = attr.ib()
self.modifiers = modifiers text = attr.ib()
self.text = text
def __repr__(self): def __repr__(self):
if self.modifiers is None: if self.modifiers is None:
@ -434,10 +435,6 @@ class KeyInfo:
key=debug.qenum_key(Qt, self.key), key=debug.qenum_key(Qt, self.key),
modifiers=modifiers, text=self.text) 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): class KeyParseError(Exception):

View File

@ -29,6 +29,7 @@ import importlib
import collections import collections
import pkg_resources import pkg_resources
import attr
from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo
from PyQt5.QtNetwork import QSslSocket from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -49,8 +50,15 @@ from qutebrowser.misc import objects, earlyinit, sql
from qutebrowser.browser import pdfjs from qutebrowser.browser import pdfjs
DistributionInfo = collections.namedtuple( @attr.s
'DistributionInfo', ['id', 'parsed', 'version', 'pretty']) class DistributionInfo:
"""Information about the running distribution."""
id = attr.ib()
parsed = attr.ib()
version = attr.ib()
pretty = attr.ib()
Distribution = usertypes.enum( Distribution = usertypes.enum(
@ -190,24 +198,25 @@ def _module_versions():
('pygments', ['__version__']), ('pygments', ['__version__']),
('yaml', ['__version__']), ('yaml', ['__version__']),
('cssutils', ['__version__']), ('cssutils', ['__version__']),
('attr', ['__version__']),
('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebKitWidgets', []), ('PyQt5.QtWebKitWidgets', []),
]) ])
for name, attributes in modules.items(): for modname, attributes in modules.items():
try: try:
module = importlib.import_module(name) module = importlib.import_module(modname)
except ImportError: except ImportError:
text = '{}: no'.format(name) text = '{}: no'.format(modname)
else: else:
for attr in attributes: for name in attributes:
try: try:
text = '{}: {}'.format(name, getattr(module, attr)) text = '{}: {}'.format(modname, getattr(module, name))
except AttributeError: except AttributeError:
pass pass
else: else:
break break
else: else:
text = '{}: yes'.format(name) text = '{}: yes'.format(modname)
lines.append(text) lines.append(text)
return lines return lines

View File

@ -1,5 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==17.2.0
colorama==0.3.9 colorama==0.3.9
cssutils==1.0.2 cssutils==1.0.2
Jinja2==2.9.6 Jinja2==2.9.6

View File

@ -25,16 +25,26 @@ import sys
import enum import enum
import os.path import os.path
import subprocess import subprocess
import collections
from xml.etree import ElementTree from xml.etree import ElementTree
import attr
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir)) os.pardir))
from scripts import utils 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') MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file')

View File

@ -24,17 +24,29 @@ import os
import sys import sys
import argparse import argparse
import subprocess import subprocess
import collections
import os.path import os.path
import tempfile import tempfile
import attr
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir)) os.pardir))
from scripts import utils 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): def _convert_present(data):

View File

@ -115,6 +115,12 @@ def whitelist_generator():
'_get_default_metavar_for_positional', '_metavar_formatter']: '_get_default_metavar_for_positional', '_metavar_formatter']:
yield 'scripts.dev.src2asciidoc.UsageFormatter.' + attr 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): def filter_func(item):
"""Check if a missing function should be filtered or not. """Check if a missing function should be filtered or not.

View File

@ -44,7 +44,7 @@ try:
['qutebrowser = qutebrowser.qutebrowser:main']}, ['qutebrowser = qutebrowser.qutebrowser:main']},
test_suite='qutebrowser.test', test_suite='qutebrowser.test',
zip_safe=True, zip_safe=True,
install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML'], install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
**common.setupdata **common.setupdata
) )
finally: finally:

View File

@ -23,6 +23,7 @@ import re
import os import os
import time import time
import attr
import pytest import pytest
import pytestqt.plugin import pytestqt.plugin
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QProcess, QObject, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QProcess, QObject,
@ -58,6 +59,7 @@ class BlacklistedMessageError(Exception):
"""Raised when ensure_not_logged found a message.""" """Raised when ensure_not_logged found a message."""
@attr.s
class Line: class Line:
"""Container for a line of data the process emits. """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. waited_for: If Process.wait_for was used on this line already.
""" """
def __init__(self, data): data = attr.ib()
self.data = data waited_for = attr.ib(False)
self.waited_for = False
def __repr__(self):
return '{}({!r})'.format(self.__class__.__name__, self.data)
def _render_log(data, threshold=100): def _render_log(data, threshold=100):

View File

@ -25,6 +25,7 @@ import json
import os.path import os.path
import http.client import http.client
import attr
import pytest import pytest
from PyQt5.QtCore import pyqtSignal, QUrl from PyQt5.QtCore import pyqtSignal, QUrl
@ -99,13 +100,13 @@ class Request(testprocess.Line):
return NotImplemented return NotImplemented
@attr.s(frozen=True, cmp=False, hash=True)
class ExpectedRequest: class ExpectedRequest:
"""Class to compare expected requests easily.""" """Class to compare expected requests easily."""
def __init__(self, verb, path): verb = attr.ib()
self.verb = verb path = attr.ib()
self.path = path
@classmethod @classmethod
def from_request(cls, request): def from_request(cls, request):
@ -118,13 +119,6 @@ class ExpectedRequest:
else: else:
return NotImplemented 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): class WebserverProcess(testprocess.Process):

View File

@ -20,8 +20,8 @@
"""Test the built-in directory browser.""" """Test the built-in directory browser."""
import os import os
import collections
import attr
import pytest import pytest
import bs4 import bs4
@ -101,8 +101,21 @@ class DirLayout:
return os.path.normpath(str(self.base)) return os.path.normpath(str(self.base))
Parsed = collections.namedtuple('Parsed', 'path, parent, folders, files') @attr.s
Item = collections.namedtuple('Item', 'path, link, text') 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): def parse(quteproc):
@ -182,6 +195,7 @@ def test_enter_folder_smoke(dir_layout, quteproc):
@pytest.mark.parametrize('folder', DirLayout.layout_folders()) @pytest.mark.parametrize('folder', DirLayout.layout_folders())
def test_enter_folder(dir_layout, quteproc, folder): def test_enter_folder(dir_layout, quteproc, folder):
# pylint: disable=not-an-iterable
quteproc.open_url(dir_layout.file_url()) quteproc.open_url(dir_layout.file_url())
quteproc.click_element_by_text(text=folder) quteproc.click_element_by_text(text=folder)
expected_url = urlutils.file_url(dir_layout.path(folder)) expected_url = urlutils.file_url(dir_layout.path(folder))

View File

@ -22,8 +22,8 @@
import os import os
import os.path import os.path
import textwrap import textwrap
import collections
import attr
import yaml import yaml
import pytest import pytest
import bs4 import bs4
@ -36,8 +36,11 @@ def collect_tests():
return files return files
ParsedFile = collections.namedtuple('ParsedFile', ['target', @attr.s
'qtwebengine_todo']) class ParsedFile:
target = attr.ib()
qtwebengine_todo = attr.ib()
class InvalidFile(Exception): class InvalidFile(Exception):

View File

@ -26,13 +26,13 @@ See https://pytest.org/latest/fixture.html
import sys import sys
import collections
import tempfile import tempfile
import itertools import itertools
import textwrap import textwrap
import unittest.mock import unittest.mock
import types import types
import attr
import pytest import pytest
import py.path # pylint: disable=no-name-in-module import py.path # pylint: disable=no-name-in-module
@ -53,7 +53,12 @@ class WinRegistryHelper:
"""Helper class for win_registry.""" """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): def __init__(self):
self._ids = [] self._ids = []
@ -161,8 +166,12 @@ def fake_web_tab(stubs, tab_registry, mode_manager, qapp):
def _generate_cmdline_tests(): def _generate_cmdline_tests():
"""Generate testcases for test_split_binding.""" """Generate testcases for test_split_binding."""
# pylint: disable=invalid-name @attr.s
TestCase = collections.namedtuple('TestCase', 'cmd, valid') class TestCase:
cmd = attr.ib()
valid = attr.ib()
separators = [';;', ' ;; ', ';; ', ' ;;'] separators = [';;', ' ;; ', ';; ', ' ;;']
invalid = ['foo', ''] invalid = ['foo', '']
valid = ['leave-mode', 'hint all'] valid = ['leave-mode', 'hint all']

View File

@ -20,14 +20,20 @@
"""pytest helper to monkeypatch the message module.""" """pytest helper to monkeypatch the message module."""
import logging import logging
import collections
import attr
import pytest import pytest
from qutebrowser.utils import usertypes, message 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: class MessageMock:
@ -35,8 +41,8 @@ class MessageMock:
"""Helper object for message_mock. """Helper object for message_mock.
Attributes: Attributes:
Message: A namedtuple representing a message. Message: A object representing a message.
messages: A list of Message tuples. messages: A list of Message objects.
""" """
def __init__(self): def __init__(self):

View File

@ -23,6 +23,7 @@
from unittest import mock from unittest import mock
import attr
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject
from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache, from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
QNetworkCacheMetaData) QNetworkCacheMetaData)
@ -202,9 +203,9 @@ class FakeNetworkReply:
def fake_qprocess(): def fake_qprocess():
"""Factory for a QProcess mock which has the QProcess enum values.""" """Factory for a QProcess mock which has the QProcess enum values."""
m = mock.Mock(spec=QProcess) m = mock.Mock(spec=QProcess)
for attr in ['NormalExit', 'CrashExit', 'FailedToStart', 'Crashed', for name in ['NormalExit', 'CrashExit', 'FailedToStart', 'Crashed',
'Timedout', 'WriteError', 'ReadError', 'UnknownError']: 'Timedout', 'WriteError', 'ReadError', 'UnknownError']:
setattr(m, attr, getattr(QProcess, attr)) setattr(m, name, getattr(QProcess, name))
return m return m
@ -315,27 +316,26 @@ class FakeSignal:
pass pass
@attr.s
class FakeCmdUtils: class FakeCmdUtils:
"""Stub for cmdutils which provides a cmd_dict.""" """Stub for cmdutils which provides a cmd_dict."""
def __init__(self, commands): cmd_dict = attr.ib()
self.cmd_dict = commands
@attr.s(frozen=True)
class FakeCommand: class FakeCommand:
"""A simple command stub which has a description.""" """A simple command stub which has a description."""
def __init__(self, name='', desc='', hide=False, debug=False, name = attr.ib('')
deprecated=False, completion=None, maxsplit=None): desc = attr.ib('')
self.desc = desc hide = attr.ib(False)
self.name = name debug = attr.ib(False)
self.hide = hide deprecated = attr.ib(False)
self.debug = debug completion = attr.ib(None)
self.deprecated = deprecated maxsplit = attr.ib(None)
self.completion = completion
self.maxsplit = maxsplit
class FakeTimer(QObject): class FakeTimer(QObject):

View File

@ -19,9 +19,9 @@
"""Tests for browser.signalfilter.""" """Tests for browser.signalfilter."""
import collections
import logging import logging
import attr
import pytest import pytest
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
@ -47,7 +47,11 @@ class Signaller(QObject):
self.filtered_signal_arg = s 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 @pytest.fixture

View File

@ -18,8 +18,8 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import os import os
import collections
import attr
import pytest import pytest
import bs4 import bs4
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
@ -109,8 +109,18 @@ def _file_url(path):
class TestDirbrowserHtml: class TestDirbrowserHtml:
Parsed = collections.namedtuple('Parsed', 'parent, folders, files') @attr.s
Item = collections.namedtuple('Item', 'link, text') class Parsed:
parent = attr.ib()
folders = attr.ib()
files = attr.ib()
@attr.s
class Item:
link = attr.ib()
text = attr.ib()
@pytest.fixture @pytest.fixture
def parser(self): def parser(self):

View File

@ -19,8 +19,7 @@
"""Tests for webelement.tabhistory.""" """Tests for webelement.tabhistory."""
import collections import attr
from PyQt5.QtCore import QUrl, QPoint from PyQt5.QtCore import QUrl, QPoint
import pytest 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 @pytest.fixture

View File

@ -24,6 +24,7 @@ import collections.abc
import operator import operator
import itertools import itertools
import attr
import pytest import pytest
from PyQt5.QtCore import QRect, QPoint, QUrl from PyQt5.QtCore import QRect, QPoint, QUrl
QWebElement = pytest.importorskip('PyQt5.QtWebKit').QWebElement QWebElement = pytest.importorskip('PyQt5.QtWebKit').QWebElement
@ -525,7 +526,12 @@ class TestIsVisibleIframe:
elem1-elem4: FakeWebElements to test. 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 @pytest.fixture
def objects(self, stubs): def objects(self, stubs):
@ -550,7 +556,7 @@ class TestIsVisibleIframe:
############################## ##############################
300, 0 300, 300 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)) frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300))
iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame) iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame)
@ -621,7 +627,7 @@ class TestIsVisibleIframe:
############################## ##############################
300, 0 300, 300 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)) frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300))
iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame) iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame)

View File

@ -21,12 +21,12 @@
import re import re
import json import json
import math import math
import collections
import itertools import itertools
import warnings import warnings
import inspect import inspect
import functools import functools
import attr
import pytest import pytest
import hypothesis import hypothesis
from hypothesis import strategies from hypothesis import strategies
@ -54,15 +54,14 @@ class Font(QFont):
@classmethod @classmethod
def fromdesc(cls, desc): def fromdesc(cls, desc):
"""Get a Font based on a font description.""" """Get a Font based on a font description."""
style, weight, ptsize, pxsize, family = desc
f = cls() f = cls()
f.setStyle(style) f.setStyle(desc.style)
f.setWeight(weight) f.setWeight(desc.weight)
if ptsize is not None and ptsize != -1: if desc.pt is not None and desc.pt != -1:
f.setPointSize(ptsize) f.setPointSize(desc.pt)
if pxsize is not None and ptsize != -1: if desc.px is not None and desc.pt != -1:
f.setPixelSize(pxsize) f.setPixelSize(desc.px)
f.setFamily(family) f.setFamily(desc.family)
return f return f
@ -1195,8 +1194,14 @@ class TestColors:
klass().to_py(val) klass().to_py(val)
FontDesc = collections.namedtuple('FontDesc', @attr.s
['style', 'weight', 'pt', 'px', 'family']) class FontDesc:
style = attr.ib()
weight = attr.ib()
pt = attr.ib()
px = attr.ib()
family = attr.ib()
class TestFont: class TestFont:

View File

@ -22,12 +22,12 @@
import sys import sys
import os import os
import getpass import getpass
import collections
import logging import logging
import json import json
import hashlib import hashlib
from unittest import mock from unittest import mock
import attr
import pytest import pytest
from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtNetwork import QLocalServer, QLocalSocket, QAbstractSocket from PyQt5.QtNetwork import QLocalServer, QLocalSocket, QAbstractSocket
@ -597,8 +597,13 @@ def test_ipcserver_socket_none_error(ipc_server, caplog):
class TestSendOrListen: class TestSendOrListen:
Args = collections.namedtuple('Args', 'no_err_windows, basedir, command, ' @attr.s
'target') class Args:
no_err_windows = attr.ib()
basedir = attr.ib()
command = attr.ib()
target = attr.ib()
@pytest.fixture @pytest.fixture
def args(self): def args(self):
@ -623,10 +628,10 @@ class TestSendOrListen:
def qlocalsocket_mock(self, mocker): def qlocalsocket_mock(self, mocker):
m = mocker.patch('qutebrowser.misc.ipc.QLocalSocket', autospec=True) m = mocker.patch('qutebrowser.misc.ipc.QLocalSocket', autospec=True)
m().errorString.return_value = "Error string" m().errorString.return_value = "Error string"
for attr in ['UnknownSocketError', 'UnconnectedState', for name in ['UnknownSocketError', 'UnconnectedState',
'ConnectionRefusedError', 'ServerNotFoundError', 'ConnectionRefusedError', 'ServerNotFoundError',
'PeerClosedError']: 'PeerClosedError']:
setattr(m, attr, getattr(QLocalSocket, attr)) setattr(m, name, getattr(QLocalSocket, name))
return m return m
@pytest.mark.linux(reason="Flaky on Windows and macOS") @pytest.mark.linux(reason="Flaky on Windows and macOS")

View File

@ -18,8 +18,8 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.misc.split.""" """Tests for qutebrowser.misc.split."""
import collections
import attr
import pytest import pytest
from qutebrowser.misc import split from qutebrowser.misc import split
@ -104,21 +104,26 @@ foo\ bar/foo bar/foo\ bar/
def _parse_split_test_data_str(): 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: 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(): for line in test_data_str.splitlines():
if not line: if not line:
continue continue
data = line.split('/') data = line.split('/')
item = tuple_class(input=data[0], keep=data[1].split('|'), item = TestCase(inp=data[0], keep=data[1].split('|'),
no_keep=data[2].split('|')) no_keep=data[2].split('|'))
yield item yield item
yield tuple_class(input='', keep=[], no_keep=[]) yield TestCase(inp='', keep=[], no_keep=[])
class TestSplit: class TestSplit:
@ -137,17 +142,17 @@ class TestSplit:
def test_split(self, split_test_case): def test_split(self, split_test_case):
"""Test splitting.""" """Test splitting."""
items = split.split(split_test_case.input) items = split.split(split_test_case.inp)
assert items == split_test_case.keep assert items == split_test_case.keep
def test_split_keep_original(self, split_test_case): def test_split_keep_original(self, split_test_case):
"""Test if splitting with keep=True yields the original string.""" """Test if splitting with keep=True yields the original string."""
items = split.split(split_test_case.input, keep=True) items = split.split(split_test_case.inp, keep=True)
assert ''.join(items) == split_test_case.input assert ''.join(items) == split_test_case.inp
def test_split_keep(self, split_test_case): def test_split_keep(self, split_test_case):
"""Test splitting with keep=True.""" """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 assert items == split_test_case.no_keep

View File

@ -25,10 +25,10 @@ import json
import os.path import os.path
import types import types
import textwrap import textwrap
import collections
import logging import logging
import subprocess import subprocess
import attr
from PyQt5.QtCore import QStandardPaths from PyQt5.QtCore import QStandardPaths
import pytest import pytest
@ -207,9 +207,6 @@ class TestStandardDir:
assert func().split(os.sep)[-elems:] == expected assert func().split(os.sep)[-elems:] == expected
DirArgTest = collections.namedtuple('DirArgTest', 'arg, expected')
class TestArguments: class TestArguments:
"""Tests the --basedir argument.""" """Tests the --basedir argument."""
@ -370,10 +367,16 @@ class TestMoveWindowsAndMacOS:
@pytest.fixture @pytest.fixture
def files(self, tmpdir): def files(self, tmpdir):
files = collections.namedtuple('Files', [
'auto_config_dir', 'config_dir', @attr.s
'local_data_dir', 'roaming_data_dir']) class Files:
return 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, auto_config_dir=tmpdir / 'auto_config' / APPNAME,
config_dir=tmpdir / 'config' / APPNAME, config_dir=tmpdir / 'config' / APPNAME,
local_data_dir=tmpdir / 'data' / APPNAME, local_data_dir=tmpdir / 'data' / APPNAME,
@ -412,11 +415,17 @@ class TestMove:
@pytest.fixture @pytest.fixture
def dirs(self, tmpdir): def dirs(self, tmpdir):
dirs = collections.namedtuple('Dirs', ['old', 'new', @attr.s
'old_file', 'new_file']) class Dirs:
old = attr.ib()
new = attr.ib()
old_file = attr.ib()
new_file = attr.ib()
old_dir = tmpdir / 'old' old_dir = tmpdir / 'old'
new_dir = tmpdir / 'new' 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') old_file=old_dir / 'file', new_file=new_dir / 'file')
def test_no_old_dir(self, dirs, caplog): def test_no_old_dir(self, dirs, caplog):

View File

@ -20,9 +20,9 @@
"""Tests for qutebrowser.utils.urlutils.""" """Tests for qutebrowser.utils.urlutils."""
import os.path import os.path
import collections
import logging import logging
import attr
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkProxy from PyQt5.QtNetwork import QNetworkProxy
import pytest import pytest
@ -38,7 +38,7 @@ class FakeDNS:
"""Helper class for the fake_dns fixture. """Helper class for the fake_dns fixture.
Class attributes: Class attributes:
FakeDNSAnswer: Helper class/namedtuple imitating a QHostInfo object FakeDNSAnswer: Helper class imitating a QHostInfo object
(used by fromname_mock). (used by fromname_mock).
Attributes: Attributes:
@ -48,7 +48,10 @@ class FakeDNS:
when fromname_mock is called. when fromname_mock is called.
""" """
FakeDNSAnswer = collections.namedtuple('FakeDNSAnswer', ['error']) @attr.s
class FakeDNSAnswer:
error = attr.ib()
def __init__(self): def __init__(self):
self.used = False self.used = False

View File

@ -25,11 +25,11 @@ import os.path
import io import io
import logging import logging
import functools import functools
import collections
import socket import socket
import re import re
import shlex import shlex
import attr
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QColor, QClipboard from PyQt5.QtGui import QColor, QClipboard
import pytest import pytest
@ -157,7 +157,11 @@ class TestInterpolateColor:
white: The Color black as a valid Color for tests. 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 @pytest.fixture
def colors(self): def colors(self):

View File

@ -32,6 +32,7 @@ import logging
import textwrap import textwrap
import pkg_resources import pkg_resources
import attr
import pytest import pytest
import qutebrowser import qutebrowser
@ -474,8 +475,8 @@ def test_path_info(monkeypatch, equal):
'runtime': lambda: 'RUNTIME PATH', 'runtime': lambda: 'RUNTIME PATH',
} }
for attr, val in patches.items(): for name, val in patches.items():
monkeypatch.setattr(version.standarddir, attr, val) monkeypatch.setattr(version.standarddir, name, val)
pathinfo = version._path_info() pathinfo = version._path_info()
@ -515,6 +516,7 @@ class ImportFake:
('pygments', True), ('pygments', True),
('yaml', True), ('yaml', True),
('cssutils', True), ('cssutils', True),
('attr', True),
('PyQt5.QtWebEngineWidgets', True), ('PyQt5.QtWebEngineWidgets', True),
('PyQt5.QtWebKitWidgets', True), ('PyQt5.QtWebKitWidgets', True),
]) ])
@ -630,6 +632,7 @@ class TestModuleVersions:
('pygments', True), ('pygments', True),
('yaml', True), ('yaml', True),
('cssutils', True), ('cssutils', True),
('attr', True),
]) ])
def test_existing_attributes(self, name, has_version): def test_existing_attributes(self, name, has_version):
"""Check if all dependencies have an expected __version__ attribute. """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'] assert version._chromium_version() not in ['', 'unknown', 'unavailable']
@attr.s
class VersionParams: class VersionParams:
def __init__(self, name, git_commit=True, frozen=False, style=True, name = attr.ib()
with_webkit=True, known_distribution=True, ssl_support=True): git_commit = attr.ib(True)
self.name = name frozen = attr.ib(False)
self.git_commit = git_commit style = attr.ib(True)
self.frozen = frozen with_webkit = attr.ib(True)
self.style = style known_distribution = attr.ib(True)
self.with_webkit = with_webkit ssl_support = attr.ib(True)
self.known_distribution = known_distribution
self.ssl_support = ssl_support
@pytest.mark.parametrize('params', [ @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' substitutions['ssl'] = 'SSL VERSION' if params.ssl_support else 'no'
for attr, val in patches.items(): for name, val in patches.items():
monkeypatch.setattr('qutebrowser.utils.version.' + attr, val) monkeypatch.setattr('qutebrowser.utils.version.' + name, val)
if params.frozen: if params.frozen:
monkeypatch.setattr(sys, 'frozen', True, raising=False) monkeypatch.setattr(sys, 'frozen', True, raising=False)