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}$
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

View File

@ -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:

View File

@ -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.

View File

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

View File

@ -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

View File

@ -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:

View File

@ -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)):

View File

@ -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):

View File

@ -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):

View File

@ -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('-')]

View File

@ -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):

View File

@ -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:

View File

@ -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:

View File

@ -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,

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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.

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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"),

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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):

View File

@ -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.

View File

@ -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:

View File

@ -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):

View File

@ -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):

View File

@ -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))

View File

@ -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):

View File

@ -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']

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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")

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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)