prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  CTRL-C should exit napari gracefully
  ## 🐛 Bug
  
  After #1476 napari just prints `KeyboardInterrupt` when CTRL-C is pressed in the system terminal window that used to launch napari.  Prior to 1476 it exited with a crash, which got the job done but was not great. 
  
  Ideally napari would exit gracefully when you hit CTRL-C in the system terminal window.
  
  
  ## To Reproduce
  
  Steps to reproduce the behavior:
  
  1. From a system terminal (e.g. Terminal program on mac)
  2. Run "napari" or a script that uses `napari.gui_qt()`
  3. Switch back to the terminal window and type CTRL-C
  
  ## Expected behavior
  
  Napari exits gracefully.
  
  ## Environment
  
  ```
  napari: not-installed
  Platform: macOS-10.15.3-x86_64-i386-64bit
  Python: 3.8.1 (default, Jan 8 2020, 16:15:59) [Clang 4.0.1 (tags/RELEASE_401/final)]
  Qt: 5.14.2
  PyQt5: 5.14.2
  NumPy: 1.18.4
  SciPy: 1.4.1
  Dask: 2.17.2
  VisPy: 0.6.5.dev111+g8387ea1a.d20200424
  
  GL version: 2.1 ATI-3.5.5
  MAX_TEXTURE_SIZE: 16384
  
  Plugins:
  - napari-plugin-engine: 0.1.6
  - svg: 0.1.3
  ```
  
  ## Additional context
  
  This is low priority since you can exit with the Quit command, or from the system terminal hit CTRL-Z and `kill %1` the app if necessary. However it seems like exiting gracefully is the right behavior long term.
  
  I tried adding this to our new `ExceptionHandler` class:
  ```
          # Interpret CTRL-C as a request to quit.
          if isinstance(value, KeyboardInterrupt):
              QApplication.instance().quit()
              return
  ```
  but while it exits cleanly sometimes, sometimes it bus errors or seg faults.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `napari/_qt/exceptions.py`
  Content:
  ```
  1 import logging
  2 import os
  3 import traceback
  4 from types import TracebackType
  5 from typing import Optional, Type
  6 
  7 from qtpy.QtCore import QObject, Signal
  8 
  9 from .qt_error_notification import NapariNotification
  10 
  11 
  12 class ExceptionHandler(QObject):
  13     """General class to handle all uncaught exceptions in the Qt event loop.
  14 
  15     Parameters
  16     ----------
  17     parent : QObject, optional
  18         parent object, by default None
  19     gui_exceptions : bool, optional
  20         Whether to show exceptions as, by default True.  May be overriden by
  21         environment variable: ``NAPARI_CATCH_ERRORS=1`
  22         Note: this ``False`` by default in ``gui_qt()`` (the main
  23         instantiator of this class), but it is ``True`` in ``napari.__main__``.
  24         As a result, exceptions will be shown in the GUI only (mostly) when
  25         running napari as ``napari`` or ``python -m napari`` from the command
  26         line.
  27     """
  28 
  29     error = Signal(tuple)
  30     message: Optional[NapariNotification] = None
  31 
  32     def __init__(self, parent=None, *, gui_exceptions=True):
  33         super().__init__(parent)
  34         if os.getenv("NAPARI_CATCH_ERRORS") in ('0', 'False'):
  35             self.gui_exceptions = False
  36         else:
  37             self.gui_exceptions = gui_exceptions
  38 
  39     def handle(
  40         self,
  41         etype: Type[BaseException],
  42         value: BaseException,
  43         tb: TracebackType,
  44     ):
  45         """Our sys.excepthook override.
  46 
  47         This function handles uncaught exceptions and can delegate to a
  48         secondary handler, whether it be a GUI dialog, or an IPython traceback
  49         printout.  The override to ``sys.excepthook`` happens in
  50         :func:`napari.gui_qt`, and therefore this is only active when the qt
  51         event loop has been started by napari.
  52 
  53         The three parameters here are what would be returned from
  54         :func:`sys.exc_info()`.
  55 
  56         Parameters
  57         ----------
  58         etype : Type[BaseException]
  59             The type of error raised
  60         value : BaseException
  61             The error instance
  62         tb : TracebackType
  63             The traceback object associated with the error.
  64         """
  65         if self.gui_exceptions:
  66             self._show_error_dialog(value)
  67         else:
  68             text = "".join(traceback.format_exception(etype, value, tb))
  69             logging.error("Unhandled exception:\n%s", text)
  70         self.error.emit((etype, value, tb))
  71 
  72     def _show_error_dialog(self, exception: BaseException):
  73         self.message = NapariNotification.from_exception(exception)
  74         self.message.show()
  75
  ```
  --- 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/napari/_qt/exceptions.py b/napari/_qt/exceptions.py
  --- a/napari/_qt/exceptions.py
  +++ b/napari/_qt/exceptions.py
  @@ -1,5 +1,6 @@
   import logging
   import os
  +import sys
   import traceback
   from types import TracebackType
   from typing import Optional, Type
  @@ -62,6 +63,12 @@
           tb : TracebackType
               The traceback object associated with the error.
           """
  +        # etype.__module__ contains the module raising the error
  +        # Custom exception classes can have different behavior
  +        # can add custom exception handlers here ...
  +        if isinstance(value, KeyboardInterrupt):
  +            print("Closed by KeyboardInterrupt", file=sys.stderr)
  +            sys.exit(1)
           if self.gui_exceptions:
               self._show_error_dialog(value)
           else:
  
verification_info: {"golden_diff": "diff --git a/napari/_qt/exceptions.py b/napari/_qt/exceptions.py\n--- a/napari/_qt/exceptions.py\n+++ b/napari/_qt/exceptions.py\n@@ -1,5 +1,6 @@\n import logging\n import os\n+import sys\n import traceback\n from types import TracebackType\n from typing import Optional, Type\n@@ -62,6 +63,12 @@\n         tb : TracebackType\n             The traceback object associated with the error.\n         \"\"\"\n+        # etype.__module__ contains the module raising the error\n+        # Custom exception classes can have different behavior\n+        # can add custom exception handlers here ...\n+        if isinstance(value, KeyboardInterrupt):\n+            print(\"Closed by KeyboardInterrupt\", file=sys.stderr)\n+            sys.exit(1)\n         if self.gui_exceptions:\n             self._show_error_dialog(value)\n         else:\n", "issue": "CTRL-C should exit napari gracefully\n## \ud83d\udc1b Bug\r\n\r\nAfter #1476 napari just prints `KeyboardInterrupt` when CTRL-C is pressed in the system terminal window that used to launch napari.  Prior to 1476 it exited with a crash, which got the job done but was not great. \r\n\r\nIdeally napari would exit gracefully when you hit CTRL-C in the system terminal window.\r\n\r\n\r\n## To Reproduce\r\n\r\nSteps to reproduce the behavior:\r\n\r\n1. From a system terminal (e.g. Terminal program on mac)\r\n2. Run \"napari\" or a script that uses `napari.gui_qt()`\r\n3. Switch back to the terminal window and type CTRL-C\r\n\r\n## Expected behavior\r\n\r\nNapari exits gracefully.\r\n\r\n## Environment\r\n\r\n```\r\nnapari: not-installed\r\nPlatform: macOS-10.15.3-x86_64-i386-64bit\r\nPython: 3.8.1 (default, Jan 8 2020, 16:15:59) [Clang 4.0.1 (tags/RELEASE_401/final)]\r\nQt: 5.14.2\r\nPyQt5: 5.14.2\r\nNumPy: 1.18.4\r\nSciPy: 1.4.1\r\nDask: 2.17.2\r\nVisPy: 0.6.5.dev111+g8387ea1a.d20200424\r\n\r\nGL version: 2.1 ATI-3.5.5\r\nMAX_TEXTURE_SIZE: 16384\r\n\r\nPlugins:\r\n- napari-plugin-engine: 0.1.6\r\n- svg: 0.1.3\r\n```\r\n\r\n## Additional context\r\n\r\nThis is low priority since you can exit with the Quit command, or from the system terminal hit CTRL-Z and `kill %1` the app if necessary. However it seems like exiting gracefully is the right behavior long term.\r\n\r\nI tried adding this to our new `ExceptionHandler` class:\r\n```\r\n        # Interpret CTRL-C as a request to quit.\r\n        if isinstance(value, KeyboardInterrupt):\r\n            QApplication.instance().quit()\r\n            return\r\n```\r\nbut while it exits cleanly sometimes, sometimes it bus errors or seg faults.\n", "before_files": [{"content": "import logging\nimport os\nimport traceback\nfrom types import TracebackType\nfrom typing import Optional, Type\n\nfrom qtpy.QtCore import QObject, Signal\n\nfrom .qt_error_notification import NapariNotification\n\n\nclass ExceptionHandler(QObject):\n    \"\"\"General class to handle all uncaught exceptions in the Qt event loop.\n\n    Parameters\n    ----------\n    parent : QObject, optional\n        parent object, by default None\n    gui_exceptions : bool, optional\n        Whether to show exceptions as, by default True.  May be overriden by\n        environment variable: ``NAPARI_CATCH_ERRORS=1`\n        Note: this ``False`` by default in ``gui_qt()`` (the main\n        instantiator of this class), but it is ``True`` in ``napari.__main__``.\n        As a result, exceptions will be shown in the GUI only (mostly) when\n        running napari as ``napari`` or ``python -m napari`` from the command\n        line.\n    \"\"\"\n\n    error = Signal(tuple)\n    message: Optional[NapariNotification] = None\n\n    def __init__(self, parent=None, *, gui_exceptions=True):\n        super().__init__(parent)\n        if os.getenv(\"NAPARI_CATCH_ERRORS\") in ('0', 'False'):\n            self.gui_exceptions = False\n        else:\n            self.gui_exceptions = gui_exceptions\n\n    def handle(\n        self,\n        etype: Type[BaseException],\n        value: BaseException,\n        tb: TracebackType,\n    ):\n        \"\"\"Our sys.excepthook override.\n\n        This function handles uncaught exceptions and can delegate to a\n        secondary handler, whether it be a GUI dialog, or an IPython traceback\n        printout.  The override to ``sys.excepthook`` happens in\n        :func:`napari.gui_qt`, and therefore this is only active when the qt\n        event loop has been started by napari.\n\n        The three parameters here are what would be returned from\n        :func:`sys.exc_info()`.\n\n        Parameters\n        ----------\n        etype : Type[BaseException]\n            The type of error raised\n        value : BaseException\n            The error instance\n        tb : TracebackType\n            The traceback object associated with the error.\n        \"\"\"\n        if self.gui_exceptions:\n            self._show_error_dialog(value)\n        else:\n            text = \"\".join(traceback.format_exception(etype, value, tb))\n            logging.error(\"Unhandled exception:\\n%s\", text)\n        self.error.emit((etype, value, tb))\n\n    def _show_error_dialog(self, exception: BaseException):\n        self.message = NapariNotification.from_exception(exception)\n        self.message.show()\n", "path": "napari/_qt/exceptions.py"}], "after_files": [{"content": "import logging\nimport os\nimport sys\nimport traceback\nfrom types import TracebackType\nfrom typing import Optional, Type\n\nfrom qtpy.QtCore import QObject, Signal\n\nfrom .qt_error_notification import NapariNotification\n\n\nclass ExceptionHandler(QObject):\n    \"\"\"General class to handle all uncaught exceptions in the Qt event loop.\n\n    Parameters\n    ----------\n    parent : QObject, optional\n        parent object, by default None\n    gui_exceptions : bool, optional\n        Whether to show exceptions as, by default True.  May be overriden by\n        environment variable: ``NAPARI_CATCH_ERRORS=1`\n        Note: this ``False`` by default in ``gui_qt()`` (the main\n        instantiator of this class), but it is ``True`` in ``napari.__main__``.\n        As a result, exceptions will be shown in the GUI only (mostly) when\n        running napari as ``napari`` or ``python -m napari`` from the command\n        line.\n    \"\"\"\n\n    error = Signal(tuple)\n    message: Optional[NapariNotification] = None\n\n    def __init__(self, parent=None, *, gui_exceptions=True):\n        super().__init__(parent)\n        if os.getenv(\"NAPARI_CATCH_ERRORS\") in ('0', 'False'):\n            self.gui_exceptions = False\n        else:\n            self.gui_exceptions = gui_exceptions\n\n    def handle(\n        self,\n        etype: Type[BaseException],\n        value: BaseException,\n        tb: TracebackType,\n    ):\n        \"\"\"Our sys.excepthook override.\n\n        This function handles uncaught exceptions and can delegate to a\n        secondary handler, whether it be a GUI dialog, or an IPython traceback\n        printout.  The override to ``sys.excepthook`` happens in\n        :func:`napari.gui_qt`, and therefore this is only active when the qt\n        event loop has been started by napari.\n\n        The three parameters here are what would be returned from\n        :func:`sys.exc_info()`.\n\n        Parameters\n        ----------\n        etype : Type[BaseException]\n            The type of error raised\n        value : BaseException\n            The error instance\n        tb : TracebackType\n            The traceback object associated with the error.\n        \"\"\"\n        # etype.__module__ contains the module raising the error\n        # Custom exception classes can have different behavior\n        # can add custom exception handlers here ...\n        if isinstance(value, KeyboardInterrupt):\n            print(\"Closed by KeyboardInterrupt\", file=sys.stderr)\n            sys.exit(1)\n        if self.gui_exceptions:\n            self._show_error_dialog(value)\n        else:\n            text = \"\".join(traceback.format_exception(etype, value, tb))\n            logging.error(\"Unhandled exception:\\n%s\", text)\n        self.error.emit((etype, value, tb))\n\n    def _show_error_dialog(self, exception: BaseException):\n        self.message = NapariNotification.from_exception(exception)\n        self.message.show()\n", "path": "napari/_qt/exceptions.py"}]}