prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  external editor 'gvim' under windows launches in read-only mode
  Hi There
  
  Today I found out, that a long missed feature existed all along. The external editor configured via `general/editor`.
  
  I assume I'm one of the first people actually using it with windows because when I tried to edit the file with gvim it always got opened in read-only mode. Naturally I wanted to try other editors and `notepad` turned out to be very helpful towards finding the cause. When trying to save the following message popped up:
  
  ```
  The process cannot access the file because it is being used by another process.
  ```
  
  I assume that the file is still open in python. A possible solution would be to close the file upon opening an external editor and reading it once the external editor gets closed.
  
  Thank you for this amazing project!
  troubas
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `qutebrowser/misc/editor.py`
  Content:
  ```
  1 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
  2 
  3 # Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
  4 #
  5 # This file is part of qutebrowser.
  6 #
  7 # qutebrowser is free software: you can redistribute it and/or modify
  8 # it under the terms of the GNU General Public License as published by
  9 # the Free Software Foundation, either version 3 of the License, or
  10 # (at your option) any later version.
  11 #
  12 # qutebrowser is distributed in the hope that it will be useful,
  13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15 # GNU General Public License for more details.
  16 #
  17 # You should have received a copy of the GNU General Public License
  18 # along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
  19 
  20 """Launcher for an external editor."""
  21 
  22 import os
  23 import tempfile
  24 
  25 from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess
  26 
  27 from qutebrowser.config import config
  28 from qutebrowser.utils import message, log
  29 from qutebrowser.misc import guiprocess
  30 
  31 
  32 class ExternalEditor(QObject):
  33 
  34     """Class to simplify editing a text in an external editor.
  35 
  36     Attributes:
  37         _text: The current text before the editor is opened.
  38         _oshandle: The OS level handle to the tmpfile.
  39         _filehandle: The file handle to the tmpfile.
  40         _proc: The GUIProcess of the editor.
  41         _win_id: The window ID the ExternalEditor is associated with.
  42     """
  43 
  44     editing_finished = pyqtSignal(str)
  45 
  46     def __init__(self, win_id, parent=None):
  47         super().__init__(parent)
  48         self._text = None
  49         self._oshandle = None
  50         self._filename = None
  51         self._proc = None
  52         self._win_id = win_id
  53 
  54     def _cleanup(self):
  55         """Clean up temporary files after the editor closed."""
  56         if self._oshandle is None or self._filename is None:
  57             # Could not create initial file.
  58             return
  59         try:
  60             os.close(self._oshandle)
  61             if self._proc.exit_status() != QProcess.CrashExit:
  62                 os.remove(self._filename)
  63         except OSError as e:
  64             # NOTE: Do not replace this with "raise CommandError" as it's
  65             # executed async.
  66             message.error(self._win_id,
  67                           "Failed to delete tempfile... ({})".format(e))
  68 
  69     @pyqtSlot(int, QProcess.ExitStatus)
  70     def on_proc_closed(self, exitcode, exitstatus):
  71         """Write the editor text into the form field and clean up tempfile.
  72 
  73         Callback for QProcess when the editor was closed.
  74         """
  75         log.procs.debug("Editor closed")
  76         if exitstatus != QProcess.NormalExit:
  77             # No error/cleanup here, since we already handle this in
  78             # on_proc_error.
  79             return
  80         try:
  81             if exitcode != 0:
  82                 return
  83             encoding = config.get('general', 'editor-encoding')
  84             try:
  85                 with open(self._filename, 'r', encoding=encoding) as f:
  86                     text = f.read()
  87             except OSError as e:
  88                 # NOTE: Do not replace this with "raise CommandError" as it's
  89                 # executed async.
  90                 message.error(self._win_id, "Failed to read back edited file: "
  91                                             "{}".format(e))
  92                 return
  93             log.procs.debug("Read back: {}".format(text))
  94             self.editing_finished.emit(text)
  95         finally:
  96             self._cleanup()
  97 
  98     @pyqtSlot(QProcess.ProcessError)
  99     def on_proc_error(self, _err):
  100         self._cleanup()
  101 
  102     def edit(self, text):
  103         """Edit a given text.
  104 
  105         Args:
  106             text: The initial text to edit.
  107         """
  108         if self._text is not None:
  109             raise ValueError("Already editing a file!")
  110         self._text = text
  111         try:
  112             self._oshandle, self._filename = tempfile.mkstemp(
  113                 text=True, prefix='qutebrowser-editor-')
  114             if text:
  115                 encoding = config.get('general', 'editor-encoding')
  116                 with open(self._filename, 'w', encoding=encoding) as f:
  117                     f.write(text)
  118         except OSError as e:
  119             message.error(self._win_id, "Failed to create initial file: "
  120                                         "{}".format(e))
  121             return
  122         self._proc = guiprocess.GUIProcess(self._win_id, what='editor',
  123                                            parent=self)
  124         self._proc.finished.connect(self.on_proc_closed)
  125         self._proc.error.connect(self.on_proc_error)
  126         editor = config.get('general', 'editor')
  127         executable = editor[0]
  128         args = [arg.replace('{}', self._filename) for arg in editor[1:]]
  129         log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
  130         self._proc.start(executable, args)
  131
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py
  --- a/qutebrowser/misc/editor.py
  +++ b/qutebrowser/misc/editor.py
  @@ -35,8 +35,8 @@
   
       Attributes:
           _text: The current text before the editor is opened.
  -        _oshandle: The OS level handle to the tmpfile.
  -        _filehandle: The file handle to the tmpfile.
  +        _file: The file handle as tempfile.NamedTemporaryFile. Note that this
  +               handle will be closed after the initial file has been created.
           _proc: The GUIProcess of the editor.
           _win_id: The window ID the ExternalEditor is associated with.
       """
  @@ -46,20 +46,18 @@
       def __init__(self, win_id, parent=None):
           super().__init__(parent)
           self._text = None
  -        self._oshandle = None
  -        self._filename = None
  +        self._file = None
           self._proc = None
           self._win_id = win_id
   
       def _cleanup(self):
           """Clean up temporary files after the editor closed."""
  -        if self._oshandle is None or self._filename is None:
  +        if self._file is None:
               # Could not create initial file.
               return
           try:
  -            os.close(self._oshandle)
               if self._proc.exit_status() != QProcess.CrashExit:
  -                os.remove(self._filename)
  +                os.remove(self._file.name)
           except OSError as e:
               # NOTE: Do not replace this with "raise CommandError" as it's
               # executed async.
  @@ -82,7 +80,7 @@
                   return
               encoding = config.get('general', 'editor-encoding')
               try:
  -                with open(self._filename, 'r', encoding=encoding) as f:
  +                with open(self._file.name, 'r', encoding=encoding) as f:
                       text = f.read()
               except OSError as e:
                   # NOTE: Do not replace this with "raise CommandError" as it's
  @@ -108,13 +106,18 @@
           if self._text is not None:
               raise ValueError("Already editing a file!")
           self._text = text
  +        encoding = config.get('general', 'editor-encoding')
           try:
  -            self._oshandle, self._filename = tempfile.mkstemp(
  -                text=True, prefix='qutebrowser-editor-')
  -            if text:
  -                encoding = config.get('general', 'editor-encoding')
  -                with open(self._filename, 'w', encoding=encoding) as f:
  -                    f.write(text)
  +            # Close while the external process is running, as otherwise systems
  +            # with exclusive write access (e.g. Windows) may fail to update
  +            # the file from the external editor, see
  +            # https://github.com/The-Compiler/qutebrowser/issues/1767
  +            with tempfile.NamedTemporaryFile(
  +                    mode='w', prefix='qutebrowser-editor-', encoding=encoding,
  +                    delete=False) as fobj:
  +                if text:
  +                    fobj.write(text)
  +                self._file = fobj
           except OSError as e:
               message.error(self._win_id, "Failed to create initial file: "
                                           "{}".format(e))
  @@ -125,6 +128,6 @@
           self._proc.error.connect(self.on_proc_error)
           editor = config.get('general', 'editor')
           executable = editor[0]
  -        args = [arg.replace('{}', self._filename) for arg in editor[1:]]
  +        args = [arg.replace('{}', self._file.name) for arg in editor[1:]]
           log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
           self._proc.start(executable, args)
  
verification_info: {"golden_diff": "diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py\n--- a/qutebrowser/misc/editor.py\n+++ b/qutebrowser/misc/editor.py\n@@ -35,8 +35,8 @@\n \n     Attributes:\n         _text: The current text before the editor is opened.\n-        _oshandle: The OS level handle to the tmpfile.\n-        _filehandle: The file handle to the tmpfile.\n+        _file: The file handle as tempfile.NamedTemporaryFile. Note that this\n+               handle will be closed after the initial file has been created.\n         _proc: The GUIProcess of the editor.\n         _win_id: The window ID the ExternalEditor is associated with.\n     \"\"\"\n@@ -46,20 +46,18 @@\n     def __init__(self, win_id, parent=None):\n         super().__init__(parent)\n         self._text = None\n-        self._oshandle = None\n-        self._filename = None\n+        self._file = None\n         self._proc = None\n         self._win_id = win_id\n \n     def _cleanup(self):\n         \"\"\"Clean up temporary files after the editor closed.\"\"\"\n-        if self._oshandle is None or self._filename is None:\n+        if self._file is None:\n             # Could not create initial file.\n             return\n         try:\n-            os.close(self._oshandle)\n             if self._proc.exit_status() != QProcess.CrashExit:\n-                os.remove(self._filename)\n+                os.remove(self._file.name)\n         except OSError as e:\n             # NOTE: Do not replace this with \"raise CommandError\" as it's\n             # executed async.\n@@ -82,7 +80,7 @@\n                 return\n             encoding = config.get('general', 'editor-encoding')\n             try:\n-                with open(self._filename, 'r', encoding=encoding) as f:\n+                with open(self._file.name, 'r', encoding=encoding) as f:\n                     text = f.read()\n             except OSError as e:\n                 # NOTE: Do not replace this with \"raise CommandError\" as it's\n@@ -108,13 +106,18 @@\n         if self._text is not None:\n             raise ValueError(\"Already editing a file!\")\n         self._text = text\n+        encoding = config.get('general', 'editor-encoding')\n         try:\n-            self._oshandle, self._filename = tempfile.mkstemp(\n-                text=True, prefix='qutebrowser-editor-')\n-            if text:\n-                encoding = config.get('general', 'editor-encoding')\n-                with open(self._filename, 'w', encoding=encoding) as f:\n-                    f.write(text)\n+            # Close while the external process is running, as otherwise systems\n+            # with exclusive write access (e.g. Windows) may fail to update\n+            # the file from the external editor, see\n+            # https://github.com/The-Compiler/qutebrowser/issues/1767\n+            with tempfile.NamedTemporaryFile(\n+                    mode='w', prefix='qutebrowser-editor-', encoding=encoding,\n+                    delete=False) as fobj:\n+                if text:\n+                    fobj.write(text)\n+                self._file = fobj\n         except OSError as e:\n             message.error(self._win_id, \"Failed to create initial file: \"\n                                         \"{}\".format(e))\n@@ -125,6 +128,6 @@\n         self._proc.error.connect(self.on_proc_error)\n         editor = config.get('general', 'editor')\n         executable = editor[0]\n-        args = [arg.replace('{}', self._filename) for arg in editor[1:]]\n+        args = [arg.replace('{}', self._file.name) for arg in editor[1:]]\n         log.procs.debug(\"Calling \\\"{}\\\" with args {}\".format(executable, args))\n         self._proc.start(executable, args)\n", "issue": "external editor 'gvim' under windows launches in read-only mode\nHi There\n\nToday I found out, that a long missed feature existed all along. The external editor configured via `general/editor`.\n\nI assume I'm one of the first people actually using it with windows because when I tried to edit the file with gvim it always got opened in read-only mode. Naturally I wanted to try other editors and `notepad` turned out to be very helpful towards finding the cause. When trying to save the following message popped up:\n\n```\nThe process cannot access the file because it is being used by another process.\n```\n\nI assume that the file is still open in python. A possible solution would be to close the file upon opening an external editor and reading it once the external editor gets closed.\n\nThank you for this amazing project!\ntroubas\n\n", "before_files": [{"content": "# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n\n# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>\n#\n# This file is part of qutebrowser.\n#\n# qutebrowser is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# qutebrowser is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.\n\n\"\"\"Launcher for an external editor.\"\"\"\n\nimport os\nimport tempfile\n\nfrom PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess\n\nfrom qutebrowser.config import config\nfrom qutebrowser.utils import message, log\nfrom qutebrowser.misc import guiprocess\n\n\nclass ExternalEditor(QObject):\n\n    \"\"\"Class to simplify editing a text in an external editor.\n\n    Attributes:\n        _text: The current text before the editor is opened.\n        _oshandle: The OS level handle to the tmpfile.\n        _filehandle: The file handle to the tmpfile.\n        _proc: The GUIProcess of the editor.\n        _win_id: The window ID the ExternalEditor is associated with.\n    \"\"\"\n\n    editing_finished = pyqtSignal(str)\n\n    def __init__(self, win_id, parent=None):\n        super().__init__(parent)\n        self._text = None\n        self._oshandle = None\n        self._filename = None\n        self._proc = None\n        self._win_id = win_id\n\n    def _cleanup(self):\n        \"\"\"Clean up temporary files after the editor closed.\"\"\"\n        if self._oshandle is None or self._filename is None:\n            # Could not create initial file.\n            return\n        try:\n            os.close(self._oshandle)\n            if self._proc.exit_status() != QProcess.CrashExit:\n                os.remove(self._filename)\n        except OSError as e:\n            # NOTE: Do not replace this with \"raise CommandError\" as it's\n            # executed async.\n            message.error(self._win_id,\n                          \"Failed to delete tempfile... ({})\".format(e))\n\n    @pyqtSlot(int, QProcess.ExitStatus)\n    def on_proc_closed(self, exitcode, exitstatus):\n        \"\"\"Write the editor text into the form field and clean up tempfile.\n\n        Callback for QProcess when the editor was closed.\n        \"\"\"\n        log.procs.debug(\"Editor closed\")\n        if exitstatus != QProcess.NormalExit:\n            # No error/cleanup here, since we already handle this in\n            # on_proc_error.\n            return\n        try:\n            if exitcode != 0:\n                return\n            encoding = config.get('general', 'editor-encoding')\n            try:\n                with open(self._filename, 'r', encoding=encoding) as f:\n                    text = f.read()\n            except OSError as e:\n                # NOTE: Do not replace this with \"raise CommandError\" as it's\n                # executed async.\n                message.error(self._win_id, \"Failed to read back edited file: \"\n                                            \"{}\".format(e))\n                return\n            log.procs.debug(\"Read back: {}\".format(text))\n            self.editing_finished.emit(text)\n        finally:\n            self._cleanup()\n\n    @pyqtSlot(QProcess.ProcessError)\n    def on_proc_error(self, _err):\n        self._cleanup()\n\n    def edit(self, text):\n        \"\"\"Edit a given text.\n\n        Args:\n            text: The initial text to edit.\n        \"\"\"\n        if self._text is not None:\n            raise ValueError(\"Already editing a file!\")\n        self._text = text\n        try:\n            self._oshandle, self._filename = tempfile.mkstemp(\n                text=True, prefix='qutebrowser-editor-')\n            if text:\n                encoding = config.get('general', 'editor-encoding')\n                with open(self._filename, 'w', encoding=encoding) as f:\n                    f.write(text)\n        except OSError as e:\n            message.error(self._win_id, \"Failed to create initial file: \"\n                                        \"{}\".format(e))\n            return\n        self._proc = guiprocess.GUIProcess(self._win_id, what='editor',\n                                           parent=self)\n        self._proc.finished.connect(self.on_proc_closed)\n        self._proc.error.connect(self.on_proc_error)\n        editor = config.get('general', 'editor')\n        executable = editor[0]\n        args = [arg.replace('{}', self._filename) for arg in editor[1:]]\n        log.procs.debug(\"Calling \\\"{}\\\" with args {}\".format(executable, args))\n        self._proc.start(executable, args)\n", "path": "qutebrowser/misc/editor.py"}], "after_files": [{"content": "# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n\n# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>\n#\n# This file is part of qutebrowser.\n#\n# qutebrowser is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# qutebrowser is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.\n\n\"\"\"Launcher for an external editor.\"\"\"\n\nimport os\nimport tempfile\n\nfrom PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess\n\nfrom qutebrowser.config import config\nfrom qutebrowser.utils import message, log\nfrom qutebrowser.misc import guiprocess\n\n\nclass ExternalEditor(QObject):\n\n    \"\"\"Class to simplify editing a text in an external editor.\n\n    Attributes:\n        _text: The current text before the editor is opened.\n        _file: The file handle as tempfile.NamedTemporaryFile. Note that this\n               handle will be closed after the initial file has been created.\n        _proc: The GUIProcess of the editor.\n        _win_id: The window ID the ExternalEditor is associated with.\n    \"\"\"\n\n    editing_finished = pyqtSignal(str)\n\n    def __init__(self, win_id, parent=None):\n        super().__init__(parent)\n        self._text = None\n        self._file = None\n        self._proc = None\n        self._win_id = win_id\n\n    def _cleanup(self):\n        \"\"\"Clean up temporary files after the editor closed.\"\"\"\n        if self._file is None:\n            # Could not create initial file.\n            return\n        try:\n            if self._proc.exit_status() != QProcess.CrashExit:\n                os.remove(self._file.name)\n        except OSError as e:\n            # NOTE: Do not replace this with \"raise CommandError\" as it's\n            # executed async.\n            message.error(self._win_id,\n                          \"Failed to delete tempfile... ({})\".format(e))\n\n    @pyqtSlot(int, QProcess.ExitStatus)\n    def on_proc_closed(self, exitcode, exitstatus):\n        \"\"\"Write the editor text into the form field and clean up tempfile.\n\n        Callback for QProcess when the editor was closed.\n        \"\"\"\n        log.procs.debug(\"Editor closed\")\n        if exitstatus != QProcess.NormalExit:\n            # No error/cleanup here, since we already handle this in\n            # on_proc_error.\n            return\n        try:\n            if exitcode != 0:\n                return\n            encoding = config.get('general', 'editor-encoding')\n            try:\n                with open(self._file.name, 'r', encoding=encoding) as f:\n                    text = f.read()\n            except OSError as e:\n                # NOTE: Do not replace this with \"raise CommandError\" as it's\n                # executed async.\n                message.error(self._win_id, \"Failed to read back edited file: \"\n                                            \"{}\".format(e))\n                return\n            log.procs.debug(\"Read back: {}\".format(text))\n            self.editing_finished.emit(text)\n        finally:\n            self._cleanup()\n\n    @pyqtSlot(QProcess.ProcessError)\n    def on_proc_error(self, _err):\n        self._cleanup()\n\n    def edit(self, text):\n        \"\"\"Edit a given text.\n\n        Args:\n            text: The initial text to edit.\n        \"\"\"\n        if self._text is not None:\n            raise ValueError(\"Already editing a file!\")\n        self._text = text\n        encoding = config.get('general', 'editor-encoding')\n        try:\n            # Close while the external process is running, as otherwise systems\n            # with exclusive write access (e.g. Windows) may fail to update\n            # the file from the external editor, see\n            # https://github.com/The-Compiler/qutebrowser/issues/1767\n            with tempfile.NamedTemporaryFile(\n                    mode='w', prefix='qutebrowser-editor-', encoding=encoding,\n                    delete=False) as fobj:\n                if text:\n                    fobj.write(text)\n                self._file = fobj\n        except OSError as e:\n            message.error(self._win_id, \"Failed to create initial file: \"\n                                        \"{}\".format(e))\n            return\n        self._proc = guiprocess.GUIProcess(self._win_id, what='editor',\n                                           parent=self)\n        self._proc.finished.connect(self.on_proc_closed)\n        self._proc.error.connect(self.on_proc_error)\n        editor = config.get('general', 'editor')\n        executable = editor[0]\n        args = [arg.replace('{}', self._file.name) for arg in editor[1:]]\n        log.procs.debug(\"Calling \\\"{}\\\" with args {}\".format(executable, args))\n        self._proc.start(executable, args)\n", "path": "qutebrowser/misc/editor.py"}]}