load_config()   F
last analyzed

Complexity

Conditions 12

Size

Total Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
c 0
b 0
f 0
dl 0
loc 69
rs 2.4705

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like load_config() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Entry point and related functionality"""
2
3
from __future__ import print_function
4
5
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
6
__license__ = "GNU GPL 2.0 or later"
7
8
import errno, logging, os, subprocess, sys
9
from ConfigParser import RawConfigParser
10
11
try:
12
    import pygtk
13
    pygtk.require('2.0')
14
except ImportError:
15
    pass  # Apparently Travis-CI's build environment doesn't add this
16
17
import gtk, wnck
18
19
from . import gtkexcepthook
20
gtkexcepthook.enable()
21
22
from . import commands, layout
23
from .util import fmt_table, XInitError
24
from .version import __version__
25
from .wm import WindowManager
26
27
# Allow MyPy to work without depending on the `typing` package
28
# (And silence complaints from only using the imported types in comments)
29
MYPY = False
30
if MYPY:
31
    # pylint: disable=unused-import
32
    from typing import Callable, Dict, Union  # NOQA
33
del MYPY
34
35
#: Location for config files (determined at runtime).
36
XDG_CONFIG_DIR = os.environ.get('XDG_CONFIG_HOME',
37
                                os.path.expanduser('~/.config'))
38
39
#: Default content for the config file
40
DEFAULTS = {
41
    'general': {
42
        # Use Ctrl+Alt as the default base for key combinations
43
        'ModMask': '<Ctrl><Alt>',
44
        'UseWorkarea': True,
45
        'MovementsWrap': True,
46
        'ColumnCount': 3
47
    },
48
    'keys': {
49
        "KP_Enter": "monitor-switch",
50
        "KP_0": "maximize",
51
        "KP_1": "bottom-left",
52
        "KP_2": "bottom",
53
        "KP_3": "bottom-right",
54
        "KP_4": "left",
55
        "KP_5": "middle",
56
        "KP_6": "right",
57
        "KP_7": "top-left",
58
        "KP_8": "top",
59
        "KP_9": "top-right",
60
        "V": "vertical-maximize",
61
        "H": "horizontal-maximize",
62
        "C": "move-to-center",
63
    }
64
}  # type: Dict[str, Dict[str, Union[str, int, float, bool, None]]]
65
66
KEYLOOKUP = {
67
    ',': 'comma',
68
    '.': 'period',
69
    '+': 'plus',
70
    '-': 'minus',
71
}  #: Used for resolving certain keysyms
72
73
74
# TODO: Move this to a more appropriate place
75
wnck.set_client_type(wnck.CLIENT_TYPE_PAGER)  # pylint: disable=no-member
76
77
# TODO: Audit all of my TODOs and API docs for accuracy and staleness.
78
79
class QuickTileApp(object):
80
    """The basic Glib application itself."""
81
82
    keybinder = None
83
    dbus_name = None
84
    dbus_obj = None
85
86
    def __init__(self, winman,  # type: WindowManager
87
                 commands,      # type: commands.CommandRegistry
0 ignored issues
show
Comprehensibility Bug introduced by
commands is re-defining a name which is already available in the outer-scope (previously defined on line 22).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
88
                 keys=None,     # type: Dict[str, Callable]
89
                 modmask=None   # type: str
90
                 ):             # type: (...) -> None
91
        """Populate the instance variables.
92
93
        @param keys: A dict mapping X11 keysyms to L{CommandRegistry}
94
            command names.
95
        @param modmask: A modifier mask to prefix to all keybindings.
96
        @type winman: The L{WindowManager} instance to use.
97
        @type keys: C{dict}
98
        """
99
        self.winman = winman
100
        self.commands = commands
101
        self._keys = keys or {}
102
        self._modmask = modmask or ''
103
104
    def run(self):  # type: () -> bool
105
        """Initialize keybinding and D-Bus if available, then call
106
        C{gtk.main()}.
107
108
        @returns: C{False} if none of the supported backends were available.
109
        @rtype: C{bool}
110
111
        @todo 1.0.0: Retire the C{doCommand} name. (API-breaking change)
112
        """
113
114
        # Attempt to set up the global hotkey support
115
        try:
116
            from . import keybinder
117
        except ImportError:
118
            logging.error("Could not find python-xlib. Cannot bind keys.")
119
        else:
120
            self.keybinder = keybinder.init(
121
                self._modmask, self._keys, self.commands, self.winman)
122
123
        # Attempt to set up the D-Bus API
124
        try:
125
            from . import dbus_api
126
        except ImportError:
127
            logging.warn("Could not load DBus backend. "
128
                         "Is python-dbus installed?")
129
        else:
130
            self.dbus_name, self.dbus_obj = dbus_api.init(
131
                self.commands, self.winman)
132
133
        # If either persistent backend loaded, start the GTK main loop.
134
        if self.keybinder or self.dbus_obj:
135
            try:
136
                gtk.main()  # pylint: disable=no-member
137
            except KeyboardInterrupt:
138
                pass
139
            return True
140
        else:
141
            return False
142
143
    def show_binds(self):  # type: () -> None
144
        """Print a formatted readout of defined keybindings and the modifier
145
        mask to stdout.
146
147
        @todo: Look into moving this into L{KeyBinder}
148
        """
149
150
        print("Keybindings defined for use with --daemonize:\n")
151
        print("Modifier: %s\n" % (self._modmask or '(none)'))
152
        print(fmt_table(self._keys, ('Key', 'Action')))
153
154
def attach_glib_log_filter():
155
    """Attach a copy of grep to our stderr to filter out _OB_WM errors.
156
157
    Hook up grep to filter out spurious libwnck error messages that we
158
    can't filter properly because PyGTK doesn't expose g_log_set_handler()
159
    """
160
    glib_log_filter = subprocess.Popen(
161
            ['grep', '-v', 'Unhandled action type _OB_WM'],
162
            stdin=subprocess.PIPE)
163
164
    # Redirect stderr through grep
165
    if not glib_log_filter.stdin:
166
        raise AssertionError("Did not receive stdin for log filter")
167
    os.dup2(glib_log_filter.stdin.fileno(), sys.stderr.fileno())
168
169
def load_config(path):  # type: (str) -> RawConfigParser
170
    """Load the config file from the given path, applying fixes as needed.
171
172
    @todo: Refactor all this
173
    """
174
    first_run = not os.path.exists(path)
175
176
    config = RawConfigParser()
177
178
    # Make keys case-sensitive because keysyms must be
179
    config.optionxform = str  # type: ignore # (Cannot assign to a method)
180
181
    # TODO: Maybe switch to two config files so I can have only the keys in the
182
    #       keymap case-sensitive?
183
    config.read(path)
184
    dirty = False
185
186
    if not config.has_section('general'):
187
        config.add_section('general')
188
        # Change this if you make backwards-incompatible changes to the
189
        # section and key naming in the config file.
190
        config.set('general', 'cfg_schema', 1)
191
        dirty = True
192
193
    for key, val in DEFAULTS['general'].items():
194
        if not config.has_option('general', key):
195
            config.set('general', key, str(val))
196
            dirty = True
197
198
    mk_raw = modkeys = config.get('general', 'ModMask')
199
    if ' ' in modkeys.strip() and '<' not in modkeys:
200
        modkeys = '<%s>' % '><'.join(modkeys.strip().split())
201
        logging.info("Updating modkeys format:\n %r --> %r", mk_raw, modkeys)
202
        config.set('general', 'ModMask', modkeys)
203
        dirty = True
204
205
    # Either load the keybindings or use and save the defaults
206
    if config.has_section('keys'):
207
        keymap = dict(config.items('keys'))
208
    else:
209
        keymap = DEFAULTS['keys']
210
        config.add_section('keys')
211
        for row in keymap.items():
212
            config.set('keys', row[0], row[1])
213
        dirty = True
214
215
    # Migrate from the deprecated syntax for punctuation keysyms
216
    for key in keymap:
217
        # Look up unrecognized shortkeys in a hardcoded dict and
218
        # replace with valid names like ',' -> 'comma'
219
        if key in KEYLOOKUP:
220
            logging.warn("Updating config file from deprecated keybind syntax:"
221
                    "\n\t%r --> %r", key, KEYLOOKUP[key])
222
            config.remove_option('keys', key)
223
            config.set('keys', KEYLOOKUP[key], keymap[key])
224
            dirty = True
225
226
    if dirty:
227
        cfg_file = open(path, 'wb')
228
229
        # TODO: REPORT: Argument 1 to "write" of "RawConfigParser" has
230
        #               incompatible type BinaryIO"; expected "file"
231
        config.write(cfg_file)  # type: ignore
232
233
        cfg_file.close()
234
        if first_run:
235
            logging.info("Wrote default config file to %s", path)
236
237
    return config
238
239
def main():  # type: () -> None
240
    """setuptools entry point"""
241
    # TODO: Switch to argparse
242
    from optparse import OptionParser, OptionGroup
0 ignored issues
show
Deprecated Code introduced by
Uses of a deprecated module 'optparse'
Loading history...
243
    parser = OptionParser(usage="%prog [options] [action] ...",
244
            version="%%prog v%s" % __version__)
245
    parser.add_option('-d', '--daemonize', action="store_true",
246
        dest="daemonize", default=False, help="Attempt to set up global "
247
        "keybindings using python-xlib and a D-Bus service using dbus-python. "
248
        "Exit if neither succeeds")
249
    parser.add_option('-b', '--bindkeys', action="store_true",
250
        dest="daemonize", default=False,
251
        help="Deprecated alias for --daemonize")
252
    parser.add_option('--debug', action="store_true", dest="debug",
253
        default=False, help="Display debug messages")
254
    parser.add_option('--no-workarea', action="store_true", dest="no_workarea",
255
        default=False, help="Overlap panels but work better with "
256
        "non-rectangular desktops")
257
258
    help_group = OptionGroup(parser, "Additional Help")
259
    help_group.add_option('--show-bindings', action="store_true",
260
        dest="show_binds", default=False, help="List all configured keybinds")
261
    help_group.add_option('--show-actions', action="store_true",
262
        dest="show_args", default=False, help="List valid arguments for use "
263
        "without --daemonize")
264
    parser.add_option_group(help_group)
265
266
    opts, args = parser.parse_args()
267
268
    if not opts.debug:
269
        attach_glib_log_filter()
270
271
    # Set up the output verbosity
272
    logging.basicConfig(level=logging.DEBUG if opts.debug else logging.INFO,
273
                        format='%(levelname)s: %(message)s')
274
275
    cfg_path = os.path.join(XDG_CONFIG_DIR, 'quicktile.cfg')
276
    first_run = not os.path.exists(cfg_path)
277
    config = load_config(cfg_path)
278
279
    ignore_workarea = ((not config.getboolean('general', 'UseWorkarea')) or
280
                       opts.no_workarea)
281
282
    # TODO: Rearchitect so this hack isn't needed
283
    commands.cycle_dimensions = commands.commands.add_many(
284
        layout.make_winsplit_positions(config.getint('general', 'ColumnCount'))
285
    )(commands.cycle_dimensions)
286
    commands.commands.extra_state = {'config': config}
287
288
    try:
289
        winman = WindowManager(ignore_workarea=ignore_workarea)
290
    except XInitError as err:
291
        logging.critical("%s", err)
292
        sys.exit(1)
293
    app = QuickTileApp(winman,
294
                       commands.commands,
295
                       keys=dict(config.items('keys')),
296
                       modmask=config.get('general', 'ModMask'))
297
298
    if opts.show_binds:
299
        app.show_binds()
300
    if opts.show_args:
301
        print(commands.commands)
302
303
    if opts.daemonize:
304
        if not app.run():
305
            logging.critical("Neither the Xlib nor the D-Bus backends were "
306
                             "available")
307
            sys.exit(errno.ENOENT)
308
            # FIXME: What's the proper exit code for "library not found"?
309
    elif not first_run:
310
        if args:
311
            winman.screen.force_update()
312
313
            for arg in args:
314
                commands.commands.call(arg, winman)
315
            while gtk.events_pending():  # pylint: disable=no-member
316
                gtk.main_iteration()  # pylint: disable=no-member
317
        elif not opts.show_args and not opts.show_binds:
318
            print(commands.commands)
319
            print("\nUse --help for a list of valid options.")
320
            sys.exit(errno.ENOENT)
321
322
if __name__ == '__main__':
323
    main()
324
325
# vim: set sw=4 sts=4 expandtab :
326