From 5a78bffb153b4f992d40afbb24cfde5107a1c117 Mon Sep 17 00:00:00 2001
From: Florian Bruhin <me@the-compiler.org>
Date: Thu, 24 Sep 2020 14:37:52 +0200
Subject: Update similar project list

---
 README.asciidoc | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

(limited to 'README.asciidoc')

diff --git a/README.asciidoc b/README.asciidoc
index 6f4bf2a4a..48bf61ca3 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -218,11 +218,11 @@ Active
 
 * https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
 * https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
-* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
 * https://nyxt.atlas.engineer/[Nyxt browser] (formerly "Next browser", Lisp, Emacs-like but also offers Vim bindings, QtWebKit or GTK+/WebKit2 - note there was a http://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly)
 * https://vieb.dev/[Vieb] (JavaScript, Electron)
 * Chrome/Chromium addons:
   https://vimium.github.io/[Vimium],
+  https://github.com/dcchambers/vb4c[vb4c] (fork of cVim)
 * Firefox addons (based on WebExtensions):
   https://github.com/tridactyl/tridactyl[Tridactyl],
   https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental),
@@ -230,7 +230,6 @@ Active
   https://github.com/amedama41/vvimpulation[VVimpulation]
 * Addons for Firefox and Chrome:
   https://github.com/brookhong/Surfingkeys[Surfingkeys],
-  https://github.com/lusakasa/saka-key[Saka Key],
   https://krabby.netlify.com/[Krabby],
   https://lydell.github.io/LinkHints/[Link Hints] (hinting only)
 * Addons for Safari:
@@ -252,6 +251,7 @@ original site is gone but the Arch Linux wiki has some data)
 * https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
 * https://github.com/conformal/xombrero[xombrero] (C, GTK+ with WebKit1)
 * https://github.com/linkdd/cream-browser[Cream Browser] (C, GTK+ with WebKit1)
+* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
 * Firefox addons (not based on WebExtensions or no recent activity):
   http://www.vimperator.org/[Vimperator],
   http://bug.5digits.org/pentadactyl/index[Pentadactyl],
@@ -261,7 +261,7 @@ original site is gone but the Arch Linux wiki has some data)
 * Chrome/Chromium addons:
   https://github.com/k2nr/ViChrome/[ViChrome],
   https://github.com/jinzhu/vrome[Vrome],
-  https://github.com/lusakasa/saka-key[Saka Key],
+  https://github.com/lusakasa/saka-key[Saka Key] (https://github.com/lusakasa/saka-key/issues/171[unmaintained]),
   https://github.com/1995eaton/chromium-vim[cVim],
   https://glee.github.io/[GleeBox]
 
-- 
cgit v1.2.3-54-g00ecf


From 685a66280aff600a83aa30da814aea63f0116c31 Mon Sep 17 00:00:00 2001
From: Florian Bruhin <me@the-compiler.org>
Date: Mon, 26 Oct 2020 13:51:12 +0100
Subject: Initial drop of Python 3.5

See #4800
---
 .flake8                                       |  6 +++--
 .github/workflows/ci.yml                      |  5 ----
 .mypy.ini                                     |  2 --
 .travis.yml                                   |  4 +--
 README.asciidoc                               |  3 +--
 doc/changelog.asciidoc                        |  8 ++++++
 doc/contributing.asciidoc                     |  2 +-
 doc/install.asciidoc                          | 24 ++++++++---------
 misc/requirements/requirements-tests.txt      | 15 +++++------
 misc/requirements/requirements-tests.txt-raw  | 13 ----------
 qutebrowser/browser/browsertab.py             |  7 ++---
 qutebrowser/browser/qutescheme.py             | 15 ++---------
 qutebrowser/browser/webengine/webenginetab.py | 12 ++-------
 qutebrowser/browser/webkit/mhtml.py           |  5 +---
 qutebrowser/browser/webkit/webkittab.py       |  7 ++---
 qutebrowser/commands/command.py               | 23 ++++++++---------
 qutebrowser/config/configtypes.py             | 37 ++++++++-------------------
 qutebrowser/misc/checkpyver.py                |  4 +--
 qutebrowser/misc/earlyinit.py                 |  2 +-
 qutebrowser/utils/debug.py                    |  5 +---
 qutebrowser/utils/utils.py                    | 11 ++++----
 setup.py                                      |  3 +--
 tests/end2end/test_invocations.py             |  9 ++++---
 tests/unit/browser/webkit/test_mhtml.py       |  5 +---
 tests/unit/config/test_configtypes.py         | 32 ++---------------------
 tests/unit/misc/test_checkpyver.py            | 11 ++++----
 tests/unit/utils/test_urlmatch.py             |  7 +----
 tox.ini                                       |  1 -
 28 files changed, 91 insertions(+), 187 deletions(-)

(limited to 'README.asciidoc')

diff --git a/.flake8 b/.flake8
index e913647f9..573d0856f 100644
--- a/.flake8
+++ b/.flake8
@@ -38,6 +38,7 @@ exclude = .*,__pycache__,resources.py
 # D413: Missing blank line after last section (not in pep257?)
 # A003: Builtin name for class attribute (needed for overridden methods)
 # W504: line break after binary operator
+# FI15: __future__ import "generator_stop" missing
 ignore =
     B001,B008,B305,
     E128,E226,E265,E501,E402,E266,E722,E731,
@@ -46,8 +47,9 @@ ignore =
     P101,P102,P103,
     D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D412,D413,
     A003,
-    W504
-min-version = 3.4.0
+    W504,
+    FI15
+min-version = 3.6.0
 max-complexity = 12
 per-file-ignores =
     qutebrowser/api/hook.py : N801
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 828b9a739..d950494eb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -100,11 +100,6 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          ### PyQt 5.7.1 (Python 3.5)
-          # - testenv: py35-pyqt57
-          #   os: ubuntu-16.04
-          #   python: 3.5
-          #   experimental: true
           ### PyQt 5.9 (Python 3.6)
           - testenv: py36-pyqt59
             os: ubuntu-18.04
diff --git a/.mypy.ini b/.mypy.ini
index 6295d4ebc..dfef7d3e5 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -1,6 +1,4 @@
 [mypy]
-# We also need to support 3.5, but if we'd chose that here, we'd need to deal
-# with conditional imports (like secrets.py).
 python_version = 3.6
 
 # --strict
diff --git a/.travis.yml b/.travis.yml
index 9a56a756c..b75081477 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
 dist: xenial
 language: python
-python: 3.5
+python: 3.6
 os: linux
-env: TESTENV=py35-pyqt57
+env: TESTENV=py36-pyqt57
 
 install:
   - python -m pip install -U pip
diff --git a/README.asciidoc b/README.asciidoc
index 48bf61ca3..42dd8f034 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -109,8 +109,7 @@ Requirements
 
 The following software and libraries are required to run qutebrowser:
 
-* https://www.python.org/[Python] 3.5.2 or newer (3.6 - 3.8 recommended;
-  support for 3.5 will be dropped with qutebrowser v2.0.0)
+* https://www.python.org/[Python] 3.6 or newer
 * https://www.qt.io/[Qt] 5.7.1 or newer (5.14 recommended; support for < 5.11
   will be dropped with qutebrowser v2.0.0) with the following modules:
   - QtCore / qtbase
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 1db13b0b2..42698c6a9 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -18,6 +18,14 @@ breaking changes (such as renamed commands) can happen in minor releases.
 v2.0.0 (unreleased)
 -------------------
 
+Major changes
+~~~~~~~~~~~~~
+
+- At least Python 3.6 is now required to run qutebrowser, support for Python
+  3.5 is dropped. Note that Python 3.5 is
+  https://www.python.org/downloads/release/python-3510/[no longer supported
+  upstream] since September 2020.
+
 Changed
 ~~~~~~~
 
diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc
index 6fc54ae5d..b8c9b9010 100644
--- a/doc/contributing.asciidoc
+++ b/doc/contributing.asciidoc
@@ -111,7 +111,7 @@ unittests and several linters/checkers.
 Currently, the following tox environments are available:
 
 * Tests using https://www.pytest.org[pytest]:
-  - `py35`, `py36`: Run pytest for python 3.5/3.6 with the system-wide PyQt.
+  - `py36`, `py37`, ...: Run pytest for python 3.6/3.7/... with the system-wide PyQt.
   - `py36-pyqt57`, ..., `py36-pyqt59`: Run pytest with the given PyQt version (`py35-*` also works).
   - `py36-pyqt59-cov`: Run with coverage support (other Python/PyQt versions work too).
 * `flake8`: Run various linting checks via https://pypi.python.org/pypi/flake8[flake8].
diff --git a/doc/install.asciidoc b/doc/install.asciidoc
index ec0710c08..9c71bf2b5 100644
--- a/doc/install.asciidoc
+++ b/doc/install.asciidoc
@@ -32,10 +32,17 @@ Ubuntu 16.04 LTS / Linux Mint 18
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or
-QtWebEngine). However, it comes with Python 3.5, so you can
-<<tox,install qutebrowser in a virtualenv>>.
+QtWebEngine). It also comes with Python 3.5 which is not supported anymore since
+qutebrowser v2.0.0.
 
-You'll need some basic libraries to use the virtualenv-installed PyQt:
+You should be able to install a newer Python (3.6+) using the
+https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or
+https://github.com/pyenv/pyenv[pyenv], and then proceed to
+<<tox,install qutebrowser in a virtualenv>>. However, this is currently untested. If you
+got this setup to work successfully, please submit a pull request to adjust these
+instructions!
+
+Note you'll need some basic libraries to use the virtualenv-installed PyQt:
 
 ----
 # apt install --no-install-recommends git ca-certificates python3 python3-venv asciidoc libglib2.0-0 libgl1 libfontconfig1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1 libxkbcommon-x11-0 libdbus-1-3 libyaml-dev gcc python3-dev
@@ -44,12 +51,6 @@ You'll need some basic libraries to use the virtualenv-installed PyQt:
 // FIXME not needed anymore?
 // libxi6 libxrender1 libegl1-mesa
 
-NOTE: Support for Python 3.5 will be dropped with the qutebrowser v2.0.0
-release, preliminarily planned for late 2020. At that point, you will need to
-either upgrade to a newer Ubuntu release, or use something like the
-https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or
-https://github.com/pyenv/pyenv[pyenv] to install a newer Python.
-
 Debian Stretch
 ~~~~~~~~~~~~~~
 
@@ -439,9 +440,8 @@ This installs all needed Python dependencies in a `.venv` subfolder
 This comes with an up-to-date Qt/PyQt including a pre-compiled QtWebEngine
 binary, but has a few caveats:
 
-- Make sure your `python3` is Python 3.5 or newer, otherwise you'll get a "No
-  matching distribution found" error. Note that qutebrowser itself also requires
-  this.
+- Make sure your `python3` is Python 3.6 or newer, otherwise you'll get a "No
+  matching distribution found" error and/or qutebrowser will not run.
 - It only works on 64-bit x86 systems, with other architectures you'll get the
   same error.
 - It comes with a QtWebEngine compiled without proprietary codec support (such
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 3136d5d90..066f4c4db 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -15,12 +15,12 @@ filelock==3.0.12
 Flask==1.1.2
 glob2==0.7
 hunter==3.3.1
-hypothesis==5.38.0 ; python_version>="3.6"
-icdiff==1.9.1 ; python_version>="3.6"
+hypothesis==5.38.0
+icdiff==1.9.1
 idna==2.10
 iniconfig==1.1.1
 itsdangerous==1.1.0
-jaraco.functools==3.0.1 ; python_version>="3.6"
+jaraco.functools==3.0.1
 # Jinja2==2.11.2
 Mako==1.1.3
 manhole==1.6.0
@@ -30,7 +30,7 @@ packaging==20.4
 parse==1.18.0
 parse-type==0.5.2
 pluggy==0.13.1
-pprintpp==0.4.0 ; python_version>="3.6"
+pprintpp==0.4.0
 py==1.9.0
 py-cpuinfo==7.0.0
 Pygments==2.7.2
@@ -41,7 +41,7 @@ pytest-benchmark==3.2.3
 pytest-clarity==0.3.0a0
 pytest-cov==2.10.1
 pytest-forked==1.3.0
-pytest-icdiff==0.5 ; python_version>="3.6"
+pytest-icdiff==0.5
 pytest-instafail==0.4.2
 pytest-mock==3.3.1
 pytest-qt==3.3.0
@@ -59,8 +59,5 @@ termcolor==1.1.0
 tldextract==3.0.2
 toml==0.10.1
 urllib3==1.25.11
-vulture==2.1 ; python_version>="3.6"
+vulture==2.1
 Werkzeug==1.0.1
-jaraco.functools==2.0; python_version<"3.6"
-vulture==1.6; python_version<"3.6"
-hypothesis<5.34.0; python_version<"3.6"
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index 1ab0fd842..fd346d475 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -34,17 +34,4 @@ pytest-clarity
 # Needed to test misc/userscripts/qute-lastpass
 tldextract
 
-#@ markers: pytest-icdiff python_version>="3.6"
-#@ markers: icdiff python_version>="3.6"
-#@ markers: pprintpp python_version>="3.6"
-
-#@ markers: jaraco.functools python_version>="3.6"
-#@ add: jaraco.functools==2.0; python_version<"3.6"
-
-#@ markers: vulture python_version>="3.6"
-#@ add: vulture==1.6; python_version<"3.6"
-
-#@ markers: hypothesis python_version>="3.6"
-#@ add: hypothesis<5.34.0; python_version<"3.6"
-
 #@ ignore: Jinja2, MarkupSafe, colorama
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 4501ba4aa..1d248b61d 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -465,6 +465,7 @@ class AbstractCaret(QObject):
     follow_selected_done = pyqtSignal()
 
     def __init__(self,
+                 tab: 'AbstractTab',
                  mode_manager: modeman.ModeManager,
                  parent: QWidget = None) -> None:
         super().__init__(parent)
@@ -472,7 +473,7 @@ class AbstractCaret(QObject):
         self._mode_manager = mode_manager
         mode_manager.entered.connect(self._on_mode_entered)
         mode_manager.left.connect(self._on_mode_left)
-        # self._tab is set by subclasses so mypy knows its concrete type.
+        self._tab = tab
 
     def _on_mode_entered(self, mode: usertypes.KeyMode) -> None:
         raise NotImplementedError
@@ -715,9 +716,9 @@ class AbstractElements:
         [typing.Optional['webelem.AbstractWebElement']], None]
     _ErrorCallback = typing.Callable[[Exception], None]
 
-    def __init__(self) -> None:
+    def __init__(self, tab: 'AbstractTab') -> None:
         self._widget = typing.cast(QWidget, None)
-        # self._tab is set by subclasses so mypy knows its concrete type.
+        self._tab = tab
 
     def find_css(self, selector: str,
                  callback: _MultiCallback,
diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py
index b661f533d..47a54d2a5 100644
--- a/qutebrowser/browser/qutescheme.py
+++ b/qutebrowser/browser/qutescheme.py
@@ -31,16 +31,10 @@ import time
 import textwrap
 import urllib
 import collections
-import base64
 import typing
+import secrets
 from typing import TypeVar, Callable, Union, Tuple
 
-try:
-    import secrets
-except ImportError:
-    # New in Python 3.6
-    secrets = None  # type: ignore[assignment]
-
 from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
 
 import qutebrowser
@@ -449,12 +443,7 @@ def qute_settings(url: QUrl) -> _HandlerRet:
     # Requests to qute://settings/set should only be allowed from
     # qute://settings. As an additional security precaution, we generate a CSRF
     # token to use here.
-    if secrets:
-        csrf_token = secrets.token_urlsafe()
-    else:
-        # On Python < 3.6, from secrets.py
-        token = base64.urlsafe_b64encode(os.urandom(32))
-        csrf_token = token.rstrip(b'=').decode('ascii')
+    csrf_token = secrets.token_urlsafe()
 
     src = jinja.render('settings.html', title='settings',
                        configdata=configdata,
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index fec87acaf..21d1615b6 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -41,7 +41,6 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
 from qutebrowser.misc import miscwidgets, objects, quitter
 from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
                                message, objreg, jinja, debug)
-from qutebrowser.keyinput import modeman
 from qutebrowser.qt import sip
 
 
@@ -356,12 +355,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
 
     """QtWebEngine implementations related to moving the cursor/selection."""
 
-    def __init__(self,
-                 tab: 'WebEngineTab',
-                 mode_manager: modeman.ModeManager,
-                 parent: QWidget = None) -> None:
-        super().__init__(mode_manager, parent)
-        self._tab = tab
+    _tab: 'WebEngineTab'
 
     def _flags(self):
         """Get flags to pass to JS."""
@@ -786,9 +780,7 @@ class WebEngineElements(browsertab.AbstractElements):
 
     """QtWebEngine implemementations related to elements on the page."""
 
-    def __init__(self, tab: 'WebEngineTab') -> None:
-        super().__init__()
-        self._tab = tab
+    _tab: 'WebEngineTab'
 
     def _js_cb_multiple(self, callback, error_cb, js_elems):
         """Handle found elements coming from JS and call the real callback.
diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py
index a045e10f2..11906e600 100644
--- a/qutebrowser/browser/webkit/mhtml.py
+++ b/qutebrowser/browser/webkit/mhtml.py
@@ -90,10 +90,7 @@ def _get_css_imports_cssutils(data, inline=False):
     """
     try:
         import cssutils
-    except (ImportError, re.error):
-        # Catching re.error because cssutils in earlier releases (<= 1.0) is
-        # broken on Python 3.5
-        # See https://bitbucket.org/cthedot/cssutils/issues/52
+    except ImportError:
         return None
 
     # We don't care about invalid CSS data, this will only litter the log
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index d26975d46..2d0f01212 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -201,8 +201,7 @@ class WebKitCaret(browsertab.AbstractCaret):
                  tab: 'WebKitTab',
                  mode_manager: modeman.ModeManager,
                  parent: QWidget = None) -> None:
-        super().__init__(mode_manager, parent)
-        self._tab = tab
+        super().__init__(tab, mode_manager, parent)
         self._selection_state = browsertab.SelectionState.none
 
     @pyqtSlot(usertypes.KeyMode)
@@ -693,9 +692,7 @@ class WebKitElements(browsertab.AbstractElements):
 
     """QtWebKit implemementations related to elements on the page."""
 
-    def __init__(self, tab: 'WebKitTab') -> None:
-        super().__init__()
-        self._tab = tab
+    _tab: 'WebKitTab'
 
     def find_css(self, selector, callback, error_cb, *, only_visible=False):
         utils.unused(error_cb)
diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py
index 2672fcd68..715bf4972 100644
--- a/qutebrowser/commands/command.py
+++ b/qutebrowser/commands/command.py
@@ -406,22 +406,19 @@ class Command:
             raise TypeError("{}: Legacy tuple type annotation!".format(
                 self.name))
 
-        if hasattr(typing, 'UnionMeta'):
-            # Python 3.5.2
-            # pylint: disable=no-member,useless-suppression
-            is_union = isinstance(
-                typ, typing.UnionMeta)  # type: ignore[attr-defined]
-        else:
-            is_union = getattr(typ, '__origin__', None) is typing.Union
+        try:
+            origin = typing.get_origin(typ)  # type: ignore[attr-defined]
+        except AttributeError:
+            # typing.get_origin was added in Python 3.8
+            origin = getattr(typ, '__origin__', None)
 
-        if is_union:
-            # this is... slightly evil, I know
+        if origin is typing.Union:
             try:
-                types = list(typ.__args__)
+                types = list(typing.get_args(typ))  # type: ignore[attr-defined]
             except AttributeError:
-                # Python 3.5.2
-                types = list(typ.__union_params__)
-            # pylint: enable=no-member,useless-suppression
+                # typing.get_args was added in Python 3.8
+                types = list(typ.__args__)
+
             if param.default is not inspect.Parameter.empty:
                 types.append(type(param.default))
             choices = self.get_arg_info(param).choices
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index 75148947e..a20514a4b 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -47,7 +47,6 @@ import html
 import codecs
 import os.path
 import itertools
-import warnings
 import functools
 import operator
 import json
@@ -1317,32 +1316,16 @@ class Regex(BaseType):
                 (getattr(re, flag.strip()) for flag in flags.split(' | ')))
 
     def _compile_regex(self, pattern: str) -> typing.Pattern[str]:
-        """Check if the given regex is valid.
-
-        This is more complicated than it could be since there's a warning on
-        invalid escapes with newer Python versions, and we want to catch that
-        case and treat it as invalid.
-        """
-        with warnings.catch_warnings(record=True) as recorded_warnings:
-            warnings.simplefilter('always')
-            try:
-                compiled = re.compile(pattern, self.flags)
-            except re.error as e:
-                raise configexc.ValidationError(
-                    pattern, "must be a valid regex - " + str(e))
-            except RuntimeError:  # pragma: no cover
-                raise configexc.ValidationError(
-                    pattern, "must be a valid regex - recursion depth "
-                    "exceeded")
-
-        assert recorded_warnings is not None
-
-        for w in recorded_warnings:
-            if (issubclass(w.category, DeprecationWarning) and
-                    str(w.message).startswith('bad escape')):
-                raise configexc.ValidationError(
-                    pattern, "must be a valid regex - " + str(w.message))
-            warnings.warn(w.message)
+        """Check if the given regex is valid."""
+        try:
+            compiled = re.compile(pattern, self.flags)
+        except re.error as e:
+            raise configexc.ValidationError(
+                pattern, "must be a valid regex - " + str(e))
+        except RuntimeError:  # pragma: no cover
+            raise configexc.ValidationError(
+                pattern, "must be a valid regex - recursion depth "
+                "exceeded")
 
         return compiled
 
diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py
index 8283dd13e..116514b8d 100644
--- a/qutebrowser/misc/checkpyver.py
+++ b/qutebrowser/misc/checkpyver.py
@@ -43,11 +43,11 @@ except ImportError:  # pragma: no cover
 # to stderr.
 def check_python_version():
     """Check if correct python version is run."""
-    if sys.hexversion < 0x03050200:
+    if sys.hexversion < 0x03060000:
         # We don't use .format() and print_function here just in case someone
         # still has < 2.6 installed.
         version_str = '.'.join(map(str, sys.version_info[:3]))
-        text = ("At least Python 3.5.2 is required to run qutebrowser, but " +
+        text = ("At least Python 3.6 is required to run qutebrowser, but " +
                 "it's running with " + version_str + ".\n")
         if (Tk and  # type: ignore[unreachable]
                 '--no-err-windows' not in sys.argv):  # pragma: no cover
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index 1594bdec6..b5bfcb1db 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -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.5 features available.
+At this point we can be sure we have all python 3.6 features available.
 """
 
 try:
diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py
index 24968e4f0..e25252d1c 100644
--- a/qutebrowser/utils/debug.py
+++ b/qutebrowser/utils/debug.py
@@ -310,11 +310,8 @@ class log_time:  # noqa: N801,N806 pylint: disable=invalid-name
     def __enter__(self) -> None:
         self._started = datetime.datetime.now()
 
-    # The string annotation is a WORKAROUND for a Python 3.5.2 bug:
-    # https://github.com/python/typing/issues/266
-
     def __exit__(self,
-                 _exc_type: 'typing.Optional[typing.Type[BaseException]]',
+                 _exc_type: typing.Optional[typing.Type[BaseException]],
                  _exc_val: typing.Optional[BaseException],
                  _exc_tb: typing.Optional[types.TracebackType]) -> None:
         assert self._started is not None
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index d7d2a0ecb..fcfd26846 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -485,12 +485,13 @@ def qualname(obj: typing.Any) -> str:
         return repr(obj)
 
 
-# The string annotation is a WORKAROUND for a Python 3.5.2 bug:
-# https://github.com/python/typing/issues/266
+_ExceptionType = typing.Union[
+    typing.Type[BaseException],
+    typing.Tuple[typing.Type[BaseException]]
+]
 
-def raises(exc: ('typing.Union['  # pylint: disable=bad-docstring-quotes
-                 '    typing.Type[BaseException], '
-                 '    typing.Tuple[typing.Type[BaseException]]]'),
+
+def raises(exc: _ExceptionType,
            func: typing.Callable,
            *args: typing.Any) -> bool:
     """Check if a function raises a given exception.
diff --git a/setup.py b/setup.py
index 0c0bf73b4..1169eae81 100755
--- a/setup.py
+++ b/setup.py
@@ -72,7 +72,7 @@ try:
                       ['qutebrowser = qutebrowser.qutebrowser:main']},
         zip_safe=True,
         install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
-        python_requires='>=3.5',
+        python_requires='>=3.6',
         name='qutebrowser',
         version=_get_constant('version'),
         description=_get_constant('description'),
@@ -94,7 +94,6 @@ try:
             'Operating System :: MacOS',
             'Operating System :: POSIX :: BSD',
             'Programming Language :: Python :: 3',
-            'Programming Language :: Python :: 3.5',
             'Programming Language :: Python :: 3.6',
             'Programming Language :: Python :: 3.7',
             'Programming Language :: Python :: 3.8',
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
index cd9aefe16..3e8731fad 100644
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -333,16 +333,17 @@ def test_command_on_start(request, quteproc_new):
     quteproc_new.wait_for_quit()
 
 
-def test_launching_with_python2():
+@pytest.mark.parametrize('python', ['python2', 'python3.5'])
+def test_launching_with_old_python(python):
     try:
         proc = subprocess.run(
-            ['python2', '-m', 'qutebrowser', '--no-err-windows'],
+            [python, '-m', 'qutebrowser', '--no-err-windows'],
             stderr=subprocess.PIPE,
             check=False)
     except FileNotFoundError:
-        pytest.skip("python2 not found")
+        pytest.skip(f"{python} not found")
     assert proc.returncode == 1
-    error = "At least Python 3.5.2 is required to run qutebrowser"
+    error = "At least Python 3.6 is required to run qutebrowser"
     assert proc.stderr.decode('ascii').startswith(error)
 
 
diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py
index 8d4289f4b..58e5602b3 100644
--- a/tests/unit/browser/webkit/test_mhtml.py
+++ b/tests/unit/browser/webkit/test_mhtml.py
@@ -29,10 +29,7 @@ mhtml = pytest.importorskip('qutebrowser.browser.webkit.mhtml')
 
 try:
     import cssutils
-except (ImportError, re.error):
-    # Catching re.error because cssutils in earlier releases (<= 1.0) is
-    # broken on Python 3.5
-    # See https://bitbucket.org/cthedot/cssutils/issues/52
+except ImportError:
     cssutils = None
 
 
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index b28b001d9..ec2e4072f 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -1482,27 +1482,13 @@ class TestRegex:
     @pytest.mark.parametrize('val', [
         pytest.param(r'(foo|bar))?baz[fis]h', id='unmatched parens'),
         pytest.param('(' * 500, id='too many parens'),
+        r'foo\Xbar',
+        r'foo\Cbar',
     ])
     def test_to_py_invalid(self, klass, val):
         with pytest.raises(configexc.ValidationError):
             klass().to_py(val)
 
-    @pytest.mark.parametrize('val', [
-        r'foo\Xbar',
-        r'foo\Cbar',
-    ])
-    def test_to_py_maybe_valid(self, klass, val):
-        """Those values are valid on some Python versions (and systems?).
-
-        On others, they raise a DeprecationWarning because of an invalid
-        escape. This tests makes sure this gets translated to a
-        ValidationError.
-        """
-        try:
-            klass().to_py(val)
-        except configexc.ValidationError:
-            pass
-
     @pytest.mark.parametrize('warning', [
         Warning('foo'), DeprecationWarning('foo'),
     ])
@@ -1518,20 +1504,6 @@ class TestRegex:
         with pytest.raises(type(warning)):
             regex.to_py('foo')
 
-    def test_bad_pattern_warning(self, mocker, klass):
-        """Test a simulated bad pattern warning.
-
-        This only seems to happen with Python 3.5, so we simulate this for
-        better coverage.
-        """
-        regex = klass()
-        m = mocker.patch('qutebrowser.config.configtypes.re')
-        m.compile.side_effect = lambda *args: warnings.warn(r'bad escape \C',
-                                                            DeprecationWarning)
-        m.error = re.error
-        with pytest.raises(configexc.ValidationError):
-            regex.to_py('foo')
-
     @pytest.mark.parametrize('flags, expected', [
         (None, 0),
         ('IGNORECASE', re.IGNORECASE),
diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py
index 5202efd07..2d4da12e8 100644
--- a/tests/unit/misc/test_checkpyver.py
+++ b/tests/unit/misc/test_checkpyver.py
@@ -28,21 +28,22 @@ import pytest
 from qutebrowser.misc import checkpyver
 
 
-TEXT = (r"At least Python 3.5.2 is required to run qutebrowser, but it's "
+TEXT = (r"At least Python 3.6 is required to run qutebrowser, but it's "
         r"running with \d+\.\d+\.\d+.")
 
 
 @pytest.mark.not_frozen
-def test_python2():
-    """Run checkpyver with python 2."""
+@pytest.mark.parametrize('python', ['python2', 'python3.5'])
+def test_old_python(python):
+    """Run checkpyver with old python versions."""
     try:
         proc = subprocess.run(
-            ['python2', checkpyver.__file__, '--no-err-windows'],
+            [python, checkpyver.__file__, '--no-err-windows'],
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
             check=False)
     except FileNotFoundError:
-        pytest.skip("python2 not found")
+        pytest.skip(f"{python} not found")
     assert not proc.stdout
     stderr = proc.stderr.decode('utf-8').rstrip()
     assert re.fullmatch(TEXT, stderr), stderr
diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py
index 8292a09ad..c38794c40 100644
--- a/tests/unit/utils/test_urlmatch.py
+++ b/tests/unit/utils/test_urlmatch.py
@@ -28,7 +28,6 @@ Currently not tested:
 - Any other features we don't need, such as .GetAsString() or set operations.
 """
 
-import sys
 import string
 
 import pytest
@@ -89,11 +88,7 @@ from qutebrowser.utils import urlmatch
     ("http://foo:/", "Invalid port: Port is empty"),
     ("http://*.foo:/", "Invalid port: Port is empty"),
     ("http://foo:com/", "Invalid port: .* 'com'"),
-    pytest.param("http://foo:123456/",
-                 "Invalid port: Port out of range 0-65535",
-                 marks=pytest.mark.skipif(
-                     sys.hexversion < 0x03060000,
-                     reason="Doesn't show an error on Python 3.5")),
+    ("http://foo:123456/", "Invalid port: Port out of range 0-65535"),
     ("http://foo:80:80/monkey", "Invalid port: .* '80:80'"),
     ("chrome://foo:1234/bar", "Ports are unsupported with chrome scheme"),
     # No port specified, but port separator.
diff --git a/tox.ini b/tox.ini
index a25374672..bfc1f2b49 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,7 +18,6 @@ setenv =
 passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS
 basepython =
     py3: {env:PYTHON:python3}
-    py35: {env:PYTHON:python3.5}
     py36: {env:PYTHON:python3.6}
     py37: {env:PYTHON:python3.7}
     py38: {env:PYTHON:python3.8}
-- 
cgit v1.2.3-54-g00ecf

