Drop support for Python 3.4

See #2742
This commit is contained in:
Florian Bruhin 2017-09-18 09:41:12 +02:00
parent 852baaa8c3
commit 505321c336
27 changed files with 55 additions and 189 deletions

View File

@ -99,9 +99,7 @@ Requirements
The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4 or newer (3.6 recommended) - note that
support for Python 3.4
https://github.com/qutebrowser/qutebrowser/issues/2742[will be dropped soon].
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
* http://qt.io/[Qt] 5.7.1 or newer with the following modules:
- QtCore / qtbase
- QtQuick (part of qtbase in some distributions)

View File

@ -22,7 +22,7 @@ Breaking changes
- (TODO) Support for legacy QtWebKit (before 5.212 which is distributed
independently from Qt) is dropped.
- (TODO) Support for Python 3.4 is dropped.
- Support for Python 3.4 is dropped.
- Support for Qt before 5.7.1 and PyQt before 5.7 is dropped.
- (TODO) New dependency on ruamel.yaml; dropped PyYAML dependency.
- (TODO) The QtWebEngine backend is now used by default if available.

View File

@ -180,7 +180,7 @@ In the _scripts/_ subfolder there's a `run_profile.py` which profiles the code
and shows a graphical representation of what takes how much time.
It uses the built-in Python
https://docs.python.org/3.4/library/profile.html[cProfile] module and can show
https://docs.python.org/3.6/library/profile.html[cProfile] module and can show
the output in four different ways:
* Raw profile file (`--profile-tool=none`)
@ -535,11 +535,11 @@ ____
Setting up a Windows Development Environment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Install https://www.python.org/downloads/release/python-344/[Python 3.4]
* Install https://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-5.5.1/[PyQt 5.5]
* Create a file at `C:\Windows\system32\python3.bat` with the following content:
`@C:\Python34\python %*`
This will make the Python 3.4 interpreter available as `python3`, which is used by various development scripts.
* Install https://www.python.org/downloads/release/python-362/[Python 3.6].
* Install PyQt via `pip install PyQt5`
* Create a file at `C:\Windows\system32\python3.bat` with the following content (adjust the path as necessary):
`@C:\Python36\python %*`
This will make the Python 3.6 interpreter available as `python3`, which is used by various development scripts.
* Install git from the https://git-scm.com/download/win[git-scm downloads page]
Try not to enable `core.autocrlf`, since that will cause `flake8` to complain a lot. Use an editor that can deal with plain line feeds instead.
* Clone your favourite qutebrowser repository.

View File

@ -17,8 +17,8 @@ running.
Debian Jessie / Ubuntu 14.04 LTS / Linux Mint < 18
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Those distributions only have Python 3.4 and a too old Qt version available. A
newer Qt isn't easily installable on Python 3.4, unfortunately.
Those distributions only have Python 3.4 and a too old Qt version available,
while qutebrowser requires Python 3.5 and Qt 5.7.1 or newer.
It should be possible to install Python 3.5 e.g. from the
https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or via_ipca

View File

@ -24,6 +24,7 @@ import sys
import os.path
import shlex
import functools
import typing
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery
@ -39,10 +40,11 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads)
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing, debug)
objreg, utils, debug)
from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import urlmodel, miscmodels
from qutebrowser.mainwindow import mainwindow
class CommandDispatcher:
@ -70,7 +72,6 @@ class CommandDispatcher:
def _new_tabbed_browser(self, private):
"""Get a tabbed-browser from a new window."""
from qutebrowser.mainwindow import mainwindow
new_window = mainwindow.MainWindow(private=private)
new_window.show()
return new_window.tabbed_browser

View File

@ -24,6 +24,7 @@ import posixpath
from qutebrowser.browser import webelem
from qutebrowser.config import config
from qutebrowser.utils import objreg, urlutils, log, message, qtutils
from qutebrowser.mainwindow import mainwindow
class Error(Exception):
@ -134,7 +135,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
window=win_id)
if window:
from qutebrowser.mainwindow import mainwindow
new_window = mainwindow.MainWindow(
private=cur_tabbed_browser.private)
new_window.show()

View File

@ -23,6 +23,7 @@ import html
from qutebrowser.config import config
from qutebrowser.utils import usertypes, message, log, objreg, jinja
from qutebrowser.mainwindow import mainwindow
class CallSuper(Exception):
@ -234,7 +235,6 @@ def get_tab(win_id, target):
elif target == usertypes.ClickTarget.window:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=tabbed_browser.private)
window.show()
win_id = window.win_id

View File

@ -31,6 +31,7 @@ from PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import mainwindow
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
@ -372,7 +373,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
background = click_target == usertypes.ClickTarget.tab_bg
tabbed_browser.tabopen(url, background=background)
elif click_target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=tabbed_browser.private)
window.show()
window.tabbed_browser.tabopen(url)

View File

@ -22,10 +22,11 @@
import inspect
import collections
import traceback
import typing
from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import (log, utils, message, docutils, objreg,
usertypes, typing)
usertypes)
from qutebrowser.utils import debug as debug_utils
from qutebrowser.misc import objects
@ -415,10 +416,7 @@ class Command:
# We also can't use isinstance here because typing.Union doesn't
# support that.
# pylint: disable=no-member,useless-suppression
try:
types = list(typ.__union_params__)
except AttributeError:
types = list(typ.__args__)
types = list(typ.__args__)
# pylint: enable=no-member,useless-suppression
if param.default is not inspect.Parameter.empty:
types.append(type(param.default))

View File

@ -28,9 +28,9 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import configdata, configexc, configtypes, configfiles
from qutebrowser.utils import utils, objreg, message, log, usertypes
from qutebrowser.utils import utils, objreg, message, log, usertypes, jinja
from qutebrowser.misc import objects, msgbox
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.commands import cmdexc, cmdutils, runners
from qutebrowser.completion.models import configmodel
# An easy way to access the config from other code via config.val.foo
@ -176,8 +176,6 @@ class KeyConfig:
def bind(self, key, command, *, mode, force=False, save_yaml=False):
"""Add a new binding from key to command."""
# Doing this here to work around a Python 3.4 circular import
from qutebrowser.commands import runners
key = self._prepare(key, mode)
parser = runners.CommandParser()
@ -594,8 +592,6 @@ def set_register_stylesheet(obj, *, stylesheet=None, update=True):
@functools.lru_cache()
def _render_stylesheet(stylesheet):
"""Render the given stylesheet jinja template."""
# Imported here to avoid a Python 3.4 circular import
from qutebrowser.utils import jinja
with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet)
return template.render(conf=val)

View File

@ -19,7 +19,7 @@
"""Exceptions related to config parsing."""
from qutebrowser.utils import utils
from qutebrowser.utils import utils, jinja
class Error(Exception):
@ -108,7 +108,6 @@ class ConfigFileErrors(Error):
def to_html(self):
"""Get the error texts as a HTML snippet."""
from qutebrowser.utils import jinja
template = jinja.environment.from_string("""
Errors occurred while reading {{ basename }}:

View File

@ -30,7 +30,7 @@ import yaml
from PyQt5.QtCore import QSettings
import qutebrowser
from qutebrowser.config import configexc
from qutebrowser.config import configexc, config
from qutebrowser.utils import standarddir, utils, qtutils
@ -152,8 +152,8 @@ class ConfigAPI:
errors: Errors which occurred while setting options.
"""
def __init__(self, config, keyconfig):
self._config = config
def __init__(self, conf, keyconfig):
self._config = conf
self._keyconfig = keyconfig
self.load_autoconfig = True
self.errors = []
@ -189,7 +189,6 @@ class ConfigAPI:
def read_config_py(filename=None):
"""Read a config.py file."""
from qutebrowser.config import config
api = ConfigAPI(config.instance, config.key_instance)
if filename is None:

View File

@ -59,9 +59,9 @@ from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont
from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import configexc
from qutebrowser.utils import standarddir, utils, qtutils
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
SYSTEM_PROXY = object() # Return value for Proxy type
@ -791,7 +791,6 @@ class Command(BaseType):
if not Command.unvalidated:
Command.unvalidated = True
try:
from qutebrowser.commands import runners, cmdexc
parser = runners.CommandParser()
try:
parser.parse_all(value)
@ -1287,7 +1286,6 @@ class Proxy(BaseType):
('none', "Don't use any proxy"))
def to_py(self, value):
from qutebrowser.utils import urlutils
self._basic_py_validation(value, str)
if not value:
return None
@ -1352,7 +1350,6 @@ class FuzzyUrl(BaseType):
"""A URL which gets interpreted as search if needed."""
def to_py(self, value):
from qutebrowser.utils import urlutils
self._basic_py_validation(value, str)
if not value:
return None

View File

@ -31,8 +31,7 @@ from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config, configfiles
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
jinja, debug)
from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt
from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.mainwindow import messageview, prompt
from qutebrowser.completion import completionwidget, completer
from qutebrowser.keyinput import modeman
from qutebrowser.browser import (commands, downloadview, hints,
@ -140,6 +139,11 @@ class MainWindow(QWidget):
parent: The parent the window should get.
"""
super().__init__(parent)
# Late import to avoid a circular dependency
# - browsertab -> hints -> webelem -> mainwindow -> bar -> browsertab
from qutebrowser.mainwindow import tabbedbrowser
from qutebrowser.mainwindow.statusbar import bar
self.setAttribute(Qt.WA_DeleteOnClose)
self._commandrunner = None
self._overlays = []

View File

@ -28,7 +28,7 @@ from PyQt5.QtGui import QIcon
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget
from qutebrowser.mainwindow import tabwidget, mainwindow
from qutebrowser.browser import signalfilter, browsertab
from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message, jinja)
@ -432,7 +432,6 @@ class TabbedBrowser(tabwidget.TabWidget):
if (config.val.tabs.tabs_are_windows and self.count() > 0 and
not ignore_tabs_are_windows):
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=self.private)
window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',

View File

@ -43,12 +43,12 @@ except ImportError: # pragma: no cover
# to stderr.
def check_python_version():
"""Check if correct python version is run."""
if sys.hexversion < 0x03040000:
if sys.hexversion < 0x03050000:
# We don't use .format() and print_function here just in case someone
# still has < 2.6 installed.
# pylint: disable=bad-builtin
version_str = '.'.join(map(str, sys.version_info[:3]))
text = ("At least Python 3.4 is required to run qutebrowser, but " +
text = ("At least Python 3.5 is required to run qutebrowser, but " +
version_str + " is installed!\n")
if Tk and '--no-err-windows' not in sys.argv: # pragma: no cover
root = Tk()

View File

@ -19,7 +19,7 @@
"""Things which need to be done really early (e.g. before importing Qt).
At this point we can be sure we have all python 3.4 features available.
At this point we can be sure we have all python 3.5 features available.
"""
try:

View File

@ -32,6 +32,7 @@ from qutebrowser.utils import (standarddir, objreg, qtutils, log, message,
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.config import config, configfiles
from qutebrowser.completion.models import miscmodels
from qutebrowser.mainwindow import mainwindow
default = object() # Sentinel value
@ -371,7 +372,6 @@ class SessionManager(QObject):
name: The name of the session to load.
temp: If given, don't set the current session.
"""
from qutebrowser.mainwindow import mainwindow
path = self._get_session_path(name, check_exists=True)
try:
with open(path, encoding='utf-8') as f:

View File

@ -1,68 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=unused-import,bad-mcs-method-argument
"""Wrapper for Python 3.5's typing module.
This wrapper is needed as both Python 3.5 and typing for PyPI isn't commonly
packaged yet. As we don't actually need anything from the typing module at
runtime, we instead mock the typing classes (using objects to make things
easier) so the typing module isn't a hard dependency.
"""
# Those are defined here to make them testable easily
class FakeTypingMeta(type):
"""Fake typing metaclass like typing.TypingMeta."""
def __init__(self, *args, # pylint: disable=super-init-not-called
**_kwds):
pass
class FakeUnionMeta(FakeTypingMeta):
"""Fake union metaclass metaclass like typing.UnionMeta."""
def __new__(cls, name, bases, namespace, parameters=None):
if parameters is None:
return super().__new__(cls, name, bases, namespace)
self = super().__new__(cls, name, bases, {})
self.__union_params__ = tuple(parameters)
return self
def __getitem__(self, parameters):
return self.__class__(self.__name__, self.__bases__,
dict(self.__dict__), parameters=parameters)
class FakeUnion(metaclass=FakeUnionMeta):
"""Fake Union type like typing.Union."""
__union_params__ = None
try:
from typing import Union
except ImportError: # pragma: no cover
Union = FakeUnion

View File

@ -42,7 +42,7 @@ except ImportError: # pragma: no cover
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
import qutebrowser
from qutebrowser.utils import qtutils, log
from qutebrowser.utils import qtutils, log, debug
fake_clipboard = None
@ -425,14 +425,13 @@ class KeyInfo:
self.text = text
def __repr__(self):
# Meh, dependency cycle...
from qutebrowser.utils.debug import qenum_key
if self.modifiers is None:
modifiers = None
else:
#modifiers = qflags_key(Qt, self.modifiers)
modifiers = hex(int(self.modifiers))
return get_repr(self, constructor=True, key=qenum_key(Qt, self.key),
return get_repr(self, constructor=True,
key=debug.qenum_key(Qt, self.key),
modifiers=modifiers, text=self.text)
def __eq__(self, other):
@ -814,10 +813,12 @@ def open_file(filename, cmdline=None):
the filename is appended to the cmdline.
"""
# Import late to avoid circular imports:
# utils -> config -> configdata -> configtypes -> cmdutils -> command ->
# utils
from qutebrowser.misc import guiprocess
# - usertypes -> utils -> guiprocess -> message -> usertypes
# - usertypes -> utils -> config -> configdata -> configtypes ->
# cmdutils -> command -> message -> usertypes
from qutebrowser.config import config
from qutebrowser.misc import guiprocess
# the default program to open downloads with - will be empty string
# if we want to use the default
override = config.val.downloads.open_dispatcher

View File

@ -149,8 +149,6 @@ PERFECT_FILES = [
'utils/jinja.py'),
('tests/unit/utils/test_error.py',
'utils/error.py'),
('tests/unit/utils/test_typing.py',
'utils/typing.py'),
('tests/unit/utils/test_javascript.py',
'utils/javascript.py'),
@ -291,7 +289,7 @@ def main_check_all():
tests.
This runs pytest with the used executable, so check_coverage.py should be
called with something like ./.tox/py34/bin/python.
called with something like ./.tox/py36/bin/python.
"""
for test_file, src_file in PERFECT_FILES:
if test_file is None:

View File

@ -5,10 +5,6 @@
#
case $TESTENV in
py34-cov)
exe=/usr/bin/python3.4
full=full
;;
py3*-pyqt*)
exe=$(readlink -f .tox/$TESTENV/bin/python)
full=

View File

@ -121,7 +121,8 @@ setupdata = {
'Operating System :: Microsoft :: Windows :: Windows 7',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers',

View File

@ -265,7 +265,7 @@ def test_launching_with_python2():
pytest.skip("python2 not found")
_stdout, stderr = proc.communicate()
assert proc.returncode == 1
error = "At least Python 3.4 is required to run qutebrowser"
error = "At least Python 3.5 is required to run qutebrowser"
assert stderr.decode('ascii').startswith(error)

View File

@ -24,11 +24,12 @@
import sys
import logging
import types
import typing
import pytest
from qutebrowser.commands import cmdutils, cmdexc, argparser, command
from qutebrowser.utils import usertypes, typing
from qutebrowser.utils import usertypes
@pytest.fixture(autouse=True)

View File

@ -28,7 +28,7 @@ import pytest
from qutebrowser.misc import checkpyver
TEXT = (r"At least Python 3.4 is required to run qutebrowser, but "
TEXT = (r"At least Python 3.5 is required to run qutebrowser, but "
r"\d+\.\d+\.\d+ is installed!\n")
@ -60,7 +60,7 @@ def test_patched_no_errwindow(capfd, monkeypatch):
"""Test with a patched sys.hexversion and --no-err-windows."""
monkeypatch.setattr(checkpyver.sys, 'argv',
[sys.argv[0], '--no-err-windows'])
monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03000000)
monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03040000)
monkeypatch.setattr(checkpyver.sys, 'exit', lambda status: None)
checkpyver.check_python_version()
stdout, stderr = capfd.readouterr()
@ -70,7 +70,7 @@ def test_patched_no_errwindow(capfd, monkeypatch):
def test_patched_errwindow(capfd, mocker, monkeypatch):
"""Test with a patched sys.hexversion and a fake Tk."""
monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03000000)
monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03040000)
monkeypatch.setattr(checkpyver.sys, 'exit', lambda status: None)
try:

View File

@ -1,54 +0,0 @@
# Copyright 2016-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.utils.typing."""
import pytest
from qutebrowser.utils import typing
@pytest.fixture
def pytyping():
"""A fixture to get the python 3.5+ typing module."""
pytyping = pytest.importorskip('typing')
return pytyping
class TestUnion:
def test_python_subclass(self, pytyping):
assert (type(pytyping.Union[str, int]) is # flake8: disable=E721
type(pytyping.Union))
def test_qute_subclass(self):
assert (type(typing.FakeUnion[str, int]) is # flake8: disable=E721
type(typing.FakeUnion))
def test_python_params(self, pytyping):
union = pytyping.Union[str, int]
try:
assert union.__union_params__ == (str, int)
except AttributeError:
assert union.__args__ == (str, int)
def test_qute_params(self):
union = typing.FakeUnion[str, int]
try:
assert union.__union_params__ == (str, int)
except AttributeError:
assert union.__args__ == (str, int)