Repository: qutebrowser/qutebrowser

Issue:
KeyError when deleting URL with space
When opening an [URL with a space](https://github.com/qutebrowser/qutebrowser/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20test) and then deleting that entry with `Ctrl-d` in the URL completion, this happens:

```
13:15:01 DEBUG    commands   command:run:515 Calling qutebrowser.completion.completionwidget.CompletionView.completion_item_del(<qutebrowser.completion.completionwidget.CompletionView>)
13:15:01 DEBUG    completion urlmodel:_delete_history:33 Deleting history entry https://github.com/qutebrowser/qutebrowser/issues?utf8=✓&q=is%3Aissue is%3Aopen test
13:15:01 DEBUG    sql        sql:__init__:80 Preparing SQL query: "DELETE FROM History where url = :val"
13:15:01 DEBUG    sql        sql:run:99 Running SQL query: "DELETE FROM History where url = :val"
13:15:01 DEBUG    sql        sql:run:102 query bindings: {':val': 'https://github.com/qutebrowser/qutebrowser/issues?utf8=✓&q=is%3Aissue is%3Aopen test'}
13:15:01 ERROR    misc       crashsignal:exception_hook:210 Uncaught exception
Traceback (most recent call last):
  File "/home/florian/proj/qutebrowser/git/qutebrowser/app.py", line 882, in eventFilter
    return handler(event)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/app.py", line 842, in _handle_key_event
    return man.eventFilter(event)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/modeman.py", line 330, in eventFilter
    return self._eventFilter_keypress(event)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/modeman.py", line 165, in _eventFilter_keypress
    handled = parser.handle(event)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/basekeyparser.py", line 258, in handle
    handled = self._handle_special_key(e)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/basekeyparser.py", line 137, in _handle_special_key
    self.execute(cmdstr, self.Type.special, count)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/keyparser.py", line 44, in execute
    self._commandrunner.run(cmdstr, count)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/commands/runners.py", line 292, in run
    result.cmd.run(self._win_id, args, count=count)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/commands/command.py", line 516, in run
    self.handler(*posargs, **kwargs)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/completion/completionwidget.py", line 379, in completion_item_del
    self.model().delete_cur_item(index)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/completion/models/completionmodel.py", line 228, in delete_cur_item
    cat.delete_func(data)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/completion/models/urlmodel.py", line 35, in _delete_history
    hist.delete_url(urlstr)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/browser/history.py", line 147, in delete_url
    self.delete('url', url)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/misc/sql.py", line 196, in delete
    raise KeyError('No row with {} = "{}"'.format(field, value))
KeyError: 'No row with url = "https://github.com/qutebrowser/qutebrowser/issues?utf8=✓&q=is%3Aissue is%3Aopen test"'
```

cc @rcorre @forkbong 


Discussion:

Resolving patch:
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -26,7 +26,7 @@
 
 from qutebrowser.commands import cmdutils, cmdexc
 from qutebrowser.utils import (utils, objreg, log, usertypes, message,
-                               debug, standarddir)
+                               debug, standarddir, qtutils)
 from qutebrowser.misc import objects, sql
 
 
@@ -144,8 +144,10 @@ def delete_url(self, url):
         Args:
             url: URL string to delete.
         """
-        self.delete('url', url)
-        self.completion.delete('url', url)
+        qurl = QUrl(url)
+        qtutils.ensure_valid(qurl)
+        self.delete('url', self._format_url(qurl))
+        self.completion.delete('url', self._format_completion_url(qurl))
 
     @pyqtSlot(QUrl, QUrl, str)
     def add_from_tab(self, url, requested_url, title):


Test patch:
diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py
--- a/tests/unit/browser/test_history.py
+++ b/tests/unit/browser/test_history.py
@@ -127,21 +127,25 @@ def test_clear_force(qtbot, tmpdir, hist):
     assert not len(hist.completion)
 
 
-def test_delete_url(hist):
+@pytest.mark.parametrize('raw, escaped', [
+    ('http://example.com/1', 'http://example.com/1'),
+    ('http://example.com/1 2', 'http://example.com/1%202'),
+])
+def test_delete_url(hist, raw, escaped):
     hist.add_url(QUrl('http://example.com/'), atime=0)
-    hist.add_url(QUrl('http://example.com/1'), atime=0)
+    hist.add_url(QUrl(escaped), atime=0)
     hist.add_url(QUrl('http://example.com/2'), atime=0)
 
     before = set(hist)
     completion_before = set(hist.completion)
 
-    hist.delete_url(QUrl('http://example.com/1'))
+    hist.delete_url(QUrl(raw))
 
     diff = before.difference(set(hist))
-    assert diff == {('http://example.com/1', '', 0, False)}
+    assert diff == {(escaped, '', 0, False)}
 
     completion_diff = completion_before.difference(set(hist.completion))
-    assert completion_diff == {('http://example.com/1', '', 0)}
+    assert completion_diff == {(raw, '', 0)}
 
 
 @pytest.mark.parametrize(