We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Backtrace when run without gtk installed
If you try to bring up the graphical interface without gtk installed, you get a backtrace:
```
Traceback (most recent call last):
  File "/usr/bin/solaar", line 57, in <module>
    solaar.gtk.main()
  File "/usr/lib/python3.7/site-packages/solaar/gtk.py", line 90, in main
    gi.require_version('Gtk', '3.0')
  File "/usr/lib64/python3.7/site-packages/gi/__init__.py", line 127, in require_version
    raise ValueError('Namespace %s not available' % namespace)
ValueError: Namespace Gtk not available
```
It does appear that there is code to handle missing modules, but gtk doesn't get imported directly so it wouldn't trigger.  Maybe something like this?
```
diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py
index f728420..20683af 100644
--- a/lib/solaar/gtk.py
+++ b/lib/solaar/gtk.py
@@ -87,8 +87,11 @@ def main():
        if not args: return
        if args.action: return _cli.run(args.action, args.hidraw_path)

-       gi = _require('gi', 'python-gi')
-       gi.require_version('Gtk', '3.0')
+       try:
+               gi.require_version('Gtk', '3.0')
+       except ValueError:
+               import sys
+               sys.exit("%s: Gtk (version 3) must be installed in order to run the graphical interface." % (NAME))
        _require('gi.repository.Gtk', 'gir1.2-gtk-3.0')

        try:
```
Can send a PR if desired, but I don't know if that's the right approach.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `lib/solaar/gtk.py`
Content:
```
1 #!/usr/bin/env python3
2 # -*- python-mode -*-
3 # -*- coding: UTF-8 -*-
4 
5 ## Copyright (C) 2012-2013  Daniel Pavel
6 ##
7 ## This program 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 2 of the License, or
10 ## (at your option) any later version.
11 ##
12 ## This program 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 along
18 ## with this program; if not, write to the Free Software Foundation, Inc.,
19 ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 
21 from __future__ import absolute_import, division, print_function, unicode_literals
22 
23 import importlib
24 
25 
26 from solaar import __version__, NAME
27 import solaar.i18n as _i18n
28 import solaar.cli as _cli
29 
30 #
31 #
32 #
33 
34 def _require(module, os_package):
35 	try:
36 		return importlib.import_module(module)
37 	except ImportError:
38 		import sys
39 		sys.exit("%s: missing required system package %s" % (NAME, os_package))
40 
41 
42 def _parse_arguments():
43 	import argparse
44 	arg_parser = argparse.ArgumentParser(prog=NAME.lower())
45 	arg_parser.add_argument('-d', '--debug', action='count', default=0,
46 							help='print logging messages, for debugging purposes (may be repeated for extra verbosity)')
47 	arg_parser.add_argument('-D', '--hidraw', action='store', dest='hidraw_path', metavar='PATH',
48 							help='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2')
49 	arg_parser.add_argument('--restart-on-wake-up', action='store_true',
50 							help='restart Solaar on sleep wake-up (experimental)')
51 	arg_parser.add_argument('-w', '--window', choices=('show','hide','only'), help='start with window showing / hidden / only (no tray icon)')
52 	arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
53 	arg_parser.add_argument('--help-actions', action='store_true',
54 							help='print help for the optional actions')
55 	arg_parser.add_argument('action', nargs=argparse.REMAINDER, choices=_cli.actions,
56 							help='optional actions to perform')
57 
58 	args = arg_parser.parse_args()
59 
60 	if args.help_actions:
61 		_cli.print_help()
62 		return
63 
64 	if args.window is None:
65 		args.window = 'show' # default behaviour is to show main window
66 
67 	import logging
68 	if args.debug > 0:
69 		log_level = logging.WARNING - 10 * args.debug
70 		log_format='%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
71 		logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S')
72 	else:
73 		logging.root.addHandler(logging.NullHandler())
74 		logging.root.setLevel(logging.ERROR)
75 
76 	if not args.action:
77 		if logging.root.isEnabledFor(logging.INFO):
78 			logging.info("language %s (%s), translations path %s", _i18n.language, _i18n.encoding, _i18n.path)
79 
80 	return args
81 
82 
83 def main():
84 	_require('pyudev', 'python3-pyudev')
85 
86 	# handle ^C in console
87 	import signal
88 	signal.signal(signal.SIGINT, signal.SIG_DFL)
89 
90 	args = _parse_arguments()
91 	if not args: return
92 	if args.action:
93 		# if any argument, run comandline and exit
94 		return _cli.run(args.action, args.hidraw_path)
95 
96 	gi = _require('gi', 'python3-gi or python3-gobject')
97 	gi.require_version('Gtk', '3.0')
98 	_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')
99 
100 	try:
101 		import solaar.ui as ui
102 		import solaar.listener as listener
103 		listener.setup_scanner(ui.status_changed, ui.error_dialog)
104 
105 		import solaar.upower as _upower
106 		if args.restart_on_wake_up:
107 			_upower.watch(listener.start_all, listener.stop_all)
108 		else:
109 			_upower.watch(lambda: listener.ping_all(True))
110 
111 		# main UI event loop
112 		ui.run_loop(listener.start_all, listener.stop_all, args.window!='only', args.window!='hide')
113 	except Exception as e:
114 		import sys
115 		sys.exit('%s: error: %s' % (NAME.lower(), e))
116 
117 
118 if __name__ == '__main__':
119 	main()
120
```
--- 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()

```

diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py
--- a/lib/solaar/gtk.py
+++ b/lib/solaar/gtk.py
@@ -31,10 +31,12 @@
 #
 #
 
-def _require(module, os_package):
+def _require(module, os_package, gi=None, gi_package=None, gi_version=None):
 	try:
+		if gi is not None:
+			gi.require_version(gi_package,gi_version)
 		return importlib.import_module(module)
-	except ImportError:
+	except (ImportError, ValueError):
 		import sys
 		sys.exit("%s: missing required system package %s" % (NAME, os_package))
 
@@ -94,8 +96,7 @@
 		return _cli.run(args.action, args.hidraw_path)
 
 	gi = _require('gi', 'python3-gi or python3-gobject')
-	gi.require_version('Gtk', '3.0')
-	_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')
+	_require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0')
 
 	try:
 		import solaar.ui as ui

{"golden_diff": "diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py\n--- a/lib/solaar/gtk.py\n+++ b/lib/solaar/gtk.py\n@@ -31,10 +31,12 @@\n #\n #\n \n-def _require(module, os_package):\n+def _require(module, os_package, gi=None, gi_package=None, gi_version=None):\n \ttry:\n+\t\tif gi is not None:\n+\t\t\tgi.require_version(gi_package,gi_version)\n \t\treturn importlib.import_module(module)\n-\texcept ImportError:\n+\texcept (ImportError, ValueError):\n \t\timport sys\n \t\tsys.exit(\"%s: missing required system package %s\" % (NAME, os_package))\n \n@@ -94,8 +96,7 @@\n \t\treturn _cli.run(args.action, args.hidraw_path)\n \n \tgi = _require('gi', 'python3-gi or python3-gobject')\n-\tgi.require_version('Gtk', '3.0')\n-\t_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')\n+\t_require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0')\n \n \ttry:\n \t\timport solaar.ui as ui\n", "issue": "Backtrace when run without gtk installed\nIf you try to bring up the graphical interface without gtk installed, you get a backtrace:\r\n```\r\nTraceback (most recent call last):\r\n  File \"/usr/bin/solaar\", line 57, in <module>\r\n    solaar.gtk.main()\r\n  File \"/usr/lib/python3.7/site-packages/solaar/gtk.py\", line 90, in main\r\n    gi.require_version('Gtk', '3.0')\r\n  File \"/usr/lib64/python3.7/site-packages/gi/__init__.py\", line 127, in require_version\r\n    raise ValueError('Namespace %s not available' % namespace)\r\nValueError: Namespace Gtk not available\r\n```\r\nIt does appear that there is code to handle missing modules, but gtk doesn't get imported directly so it wouldn't trigger.  Maybe something like this?\r\n```\r\ndiff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py\r\nindex f728420..20683af 100644\r\n--- a/lib/solaar/gtk.py\r\n+++ b/lib/solaar/gtk.py\r\n@@ -87,8 +87,11 @@ def main():\r\n        if not args: return\r\n        if args.action: return _cli.run(args.action, args.hidraw_path)\r\n\r\n-       gi = _require('gi', 'python-gi')\r\n-       gi.require_version('Gtk', '3.0')\r\n+       try:\r\n+               gi.require_version('Gtk', '3.0')\r\n+       except ValueError:\r\n+               import sys\r\n+               sys.exit(\"%s: Gtk (version 3) must be installed in order to run the graphical interface.\" % (NAME))\r\n        _require('gi.repository.Gtk', 'gir1.2-gtk-3.0')\r\n\r\n        try:\r\n```\r\nCan send a PR if desired, but I don't know if that's the right approach.\n", "before_files": [{"content": "#!/usr/bin/env python3\n# -*- python-mode -*-\n# -*- coding: UTF-8 -*-\n\n## Copyright (C) 2012-2013  Daniel Pavel\n##\n## This program 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 2 of the License, or\n## (at your option) any later version.\n##\n## This program 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 along\n## with this program; if not, write to the Free Software Foundation, Inc.,\n## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport importlib\n\n\nfrom solaar import __version__, NAME\nimport solaar.i18n as _i18n\nimport solaar.cli as _cli\n\n#\n#\n#\n\ndef _require(module, os_package):\n\ttry:\n\t\treturn importlib.import_module(module)\n\texcept ImportError:\n\t\timport sys\n\t\tsys.exit(\"%s: missing required system package %s\" % (NAME, os_package))\n\n\ndef _parse_arguments():\n\timport argparse\n\targ_parser = argparse.ArgumentParser(prog=NAME.lower())\n\targ_parser.add_argument('-d', '--debug', action='count', default=0,\n\t\t\t\t\t\t\thelp='print logging messages, for debugging purposes (may be repeated for extra verbosity)')\n\targ_parser.add_argument('-D', '--hidraw', action='store', dest='hidraw_path', metavar='PATH',\n\t\t\t\t\t\t\thelp='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2')\n\targ_parser.add_argument('--restart-on-wake-up', action='store_true',\n\t\t\t\t\t\t\thelp='restart Solaar on sleep wake-up (experimental)')\n\targ_parser.add_argument('-w', '--window', choices=('show','hide','only'), help='start with window showing / hidden / only (no tray icon)')\n\targ_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)\n\targ_parser.add_argument('--help-actions', action='store_true',\n\t\t\t\t\t\t\thelp='print help for the optional actions')\n\targ_parser.add_argument('action', nargs=argparse.REMAINDER, choices=_cli.actions,\n\t\t\t\t\t\t\thelp='optional actions to perform')\n\n\targs = arg_parser.parse_args()\n\n\tif args.help_actions:\n\t\t_cli.print_help()\n\t\treturn\n\n\tif args.window is None:\n\t\targs.window = 'show' # default behaviour is to show main window\n\n\timport logging\n\tif args.debug > 0:\n\t\tlog_level = logging.WARNING - 10 * args.debug\n\t\tlog_format='%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'\n\t\tlogging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S')\n\telse:\n\t\tlogging.root.addHandler(logging.NullHandler())\n\t\tlogging.root.setLevel(logging.ERROR)\n\n\tif not args.action:\n\t\tif logging.root.isEnabledFor(logging.INFO):\n\t\t\tlogging.info(\"language %s (%s), translations path %s\", _i18n.language, _i18n.encoding, _i18n.path)\n\n\treturn args\n\n\ndef main():\n\t_require('pyudev', 'python3-pyudev')\n\n\t# handle ^C in console\n\timport signal\n\tsignal.signal(signal.SIGINT, signal.SIG_DFL)\n\n\targs = _parse_arguments()\n\tif not args: return\n\tif args.action:\n\t\t# if any argument, run comandline and exit\n\t\treturn _cli.run(args.action, args.hidraw_path)\n\n\tgi = _require('gi', 'python3-gi or python3-gobject')\n\tgi.require_version('Gtk', '3.0')\n\t_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')\n\n\ttry:\n\t\timport solaar.ui as ui\n\t\timport solaar.listener as listener\n\t\tlistener.setup_scanner(ui.status_changed, ui.error_dialog)\n\n\t\timport solaar.upower as _upower\n\t\tif args.restart_on_wake_up:\n\t\t\t_upower.watch(listener.start_all, listener.stop_all)\n\t\telse:\n\t\t\t_upower.watch(lambda: listener.ping_all(True))\n\n\t\t# main UI event loop\n\t\tui.run_loop(listener.start_all, listener.stop_all, args.window!='only', args.window!='hide')\n\texcept Exception as e:\n\t\timport sys\n\t\tsys.exit('%s: error: %s' % (NAME.lower(), e))\n\n\nif __name__ == '__main__':\n\tmain()\n", "path": "lib/solaar/gtk.py"}], "after_files": [{"content": "#!/usr/bin/env python3\n# -*- python-mode -*-\n# -*- coding: UTF-8 -*-\n\n## Copyright (C) 2012-2013  Daniel Pavel\n##\n## This program 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 2 of the License, or\n## (at your option) any later version.\n##\n## This program 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 along\n## with this program; if not, write to the Free Software Foundation, Inc.,\n## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport importlib\n\n\nfrom solaar import __version__, NAME\nimport solaar.i18n as _i18n\nimport solaar.cli as _cli\n\n#\n#\n#\n\ndef _require(module, os_package, gi=None, gi_package=None, gi_version=None):\n\ttry:\n\t\tif gi is not None:\n\t\t\tgi.require_version(gi_package,gi_version)\n\t\treturn importlib.import_module(module)\n\texcept (ImportError, ValueError):\n\t\timport sys\n\t\tsys.exit(\"%s: missing required system package %s\" % (NAME, os_package))\n\n\ndef _parse_arguments():\n\timport argparse\n\targ_parser = argparse.ArgumentParser(prog=NAME.lower())\n\targ_parser.add_argument('-d', '--debug', action='count', default=0,\n\t\t\t\t\t\t\thelp='print logging messages, for debugging purposes (may be repeated for extra verbosity)')\n\targ_parser.add_argument('-D', '--hidraw', action='store', dest='hidraw_path', metavar='PATH',\n\t\t\t\t\t\t\thelp='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2')\n\targ_parser.add_argument('--restart-on-wake-up', action='store_true',\n\t\t\t\t\t\t\thelp='restart Solaar on sleep wake-up (experimental)')\n\targ_parser.add_argument('-w', '--window', choices=('show','hide','only'), help='start with window showing / hidden / only (no tray icon)')\n\targ_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)\n\targ_parser.add_argument('--help-actions', action='store_true',\n\t\t\t\t\t\t\thelp='print help for the optional actions')\n\targ_parser.add_argument('action', nargs=argparse.REMAINDER, choices=_cli.actions,\n\t\t\t\t\t\t\thelp='optional actions to perform')\n\n\targs = arg_parser.parse_args()\n\n\tif args.help_actions:\n\t\t_cli.print_help()\n\t\treturn\n\n\tif args.window is None:\n\t\targs.window = 'show' # default behaviour is to show main window\n\n\timport logging\n\tif args.debug > 0:\n\t\tlog_level = logging.WARNING - 10 * args.debug\n\t\tlog_format='%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'\n\t\tlogging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S')\n\telse:\n\t\tlogging.root.addHandler(logging.NullHandler())\n\t\tlogging.root.setLevel(logging.ERROR)\n\n\tif not args.action:\n\t\tif logging.root.isEnabledFor(logging.INFO):\n\t\t\tlogging.info(\"language %s (%s), translations path %s\", _i18n.language, _i18n.encoding, _i18n.path)\n\n\treturn args\n\n\ndef main():\n\t_require('pyudev', 'python3-pyudev')\n\n\t# handle ^C in console\n\timport signal\n\tsignal.signal(signal.SIGINT, signal.SIG_DFL)\n\n\targs = _parse_arguments()\n\tif not args: return\n\tif args.action:\n\t\t# if any argument, run comandline and exit\n\t\treturn _cli.run(args.action, args.hidraw_path)\n\n\tgi = _require('gi', 'python3-gi or python3-gobject')\n\t_require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0')\n\n\ttry:\n\t\timport solaar.ui as ui\n\t\timport solaar.listener as listener\n\t\tlistener.setup_scanner(ui.status_changed, ui.error_dialog)\n\n\t\timport solaar.upower as _upower\n\t\tif args.restart_on_wake_up:\n\t\t\t_upower.watch(listener.start_all, listener.stop_all)\n\t\telse:\n\t\t\t_upower.watch(lambda: listener.ping_all(True))\n\n\t\t# main UI event loop\n\t\tui.run_loop(listener.start_all, listener.stop_all, args.window!='only', args.window!='hide')\n\texcept Exception as e:\n\t\timport sys\n\t\tsys.exit('%s: error: %s' % (NAME.lower(), e))\n\n\nif __name__ == '__main__':\n\tmain()\n", "path": "lib/solaar/gtk.py"}]}