sopel.cli.plugins   C
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 507
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 55
eloc 324
dl 0
loc 507
rs 6
c 0
b 0
f 0

10 Functions

Rating   Name   Duplication   Size   Complexity  
B handle_configure() 0 30 5
B handle_show() 0 47 8
B build_parser() 0 136 1
A _handle_disable_plugin() 0 26 4
C handle_list() 0 73 11
B handle_disable() 0 54 5
A handle_enable() 0 37 3
C _handle_enable_plugin() 0 43 10
A display_unknown_plugins() 0 7 1
B main() 0 20 7

How to fix   Complexity   

Complexity

Complex classes like sopel.cli.plugins 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
# coding=utf-8
2
"""Sopel Plugins Command Line Interface (CLI): ``sopel-plugins``"""
3
from __future__ import unicode_literals, absolute_import, print_function, division
4
5
import argparse
6
import inspect
7
import operator
8
9
from sopel import plugins, tools
10
11
from . import utils
12
13
14
def build_parser():
15
    """Configure an argument parser for ``sopel-plugins``"""
16
    parser = argparse.ArgumentParser(
17
        description='Sopel plugins tool')
18
19
    # Subparser: sopel-plugins <sub-parser> <sub-options>
20
    subparsers = parser.add_subparsers(
21
        help='Action to perform',
22
        dest='action')
23
24
    # sopel-plugins show <name>
25
    show_parser = subparsers.add_parser(
26
        'show',
27
        formatter_class=argparse.RawTextHelpFormatter,
28
        help="Show plugin details",
29
        description="Show detailed information about a plugin.")
30
    utils.add_common_arguments(show_parser)
31
    show_parser.add_argument('name', help='Plugin name')
32
33
    # sopel-plugins configure <name>
34
    config_parser = subparsers.add_parser(
35
        'configure',
36
        formatter_class=argparse.RawTextHelpFormatter,
37
        help="Configure plugin with a config wizard",
38
        description=inspect.cleandoc("""
39
            Run a config wizard to configure a plugin.
40
41
            This can be used whether the plugin is enabled or not.
42
        """))
43
    utils.add_common_arguments(config_parser)
44
    config_parser.add_argument('name', help='Plugin name')
45
46
    # sopel-plugins list
47
    list_parser = subparsers.add_parser(
48
        'list',
49
        formatter_class=argparse.RawTextHelpFormatter,
50
        help="List available Sopel plugins",
51
        description=inspect.cleandoc("""
52
            List available Sopel plugins from all possible sources.
53
54
            Plugin sources are: built-in, from ``sopel_modules.*``,
55
            from ``sopel.plugins`` entry points, or Sopel's plugin directories.
56
57
            Enabled plugins are displayed in green; disabled, in red.
58
        """))
59
    utils.add_common_arguments(list_parser)
60
    list_parser.add_argument(
61
        '-C', '--no-color',
62
        help='Disable colors',
63
        dest='no_color',
64
        action='store_true',
65
        default=False)
66
    list_enable = list_parser.add_mutually_exclusive_group(required=False)
67
    list_enable.add_argument(
68
        '-e', '--enabled-only',
69
        help='Display only enabled plugins',
70
        dest='enabled_only',
71
        action='store_true',
72
        default=False)
73
    list_enable.add_argument(
74
        '-d', '--disabled-only',
75
        help='Display only disabled plugins',
76
        dest='disabled_only',
77
        action='store_true',
78
        default=False)
79
    list_parser.add_argument(
80
        '-n', '--name-only',
81
        help='Display only plugin names',
82
        dest='name_only',
83
        action='store_true',
84
        default=False)
85
86
    # sopel-plugins disable
87
    disable_parser = subparsers.add_parser(
88
        'disable',
89
        formatter_class=argparse.RawTextHelpFormatter,
90
        help="Disable a Sopel plugins",
91
        description=inspect.cleandoc("""
92
            Disable a Sopel plugin by its name, no matter where it comes from.
93
94
            It is not possible to disable the ``coretasks`` plugin.
95
        """))
96
    utils.add_common_arguments(disable_parser)
97
    disable_parser.add_argument(
98
        'names', metavar='name', nargs='+',
99
        help=inspect.cleandoc("""
100
            Name of the plugin to disable.
101
            Can be used multiple times to disable multiple plugins at once.
102
            In case of error, configuration is not modified.
103
        """))
104
    disable_parser.add_argument(
105
        '-f', '--force', action='store_true', default=False,
106
        help=inspect.cleandoc("""
107
            Force exclusion of the plugin.
108
            When ``core.enable`` is defined, a plugin may be disabled without
109
            being excluded. In this case, use this option to force
110
            its exclusion.
111
        """))
112
    disable_parser.add_argument(
113
        '-r', '--remove', action='store_true', default=False,
114
        help="Remove from ``core.enable`` list if applicable.")
115
116
    # sopel-plugins enable
117
    enable_parser = subparsers.add_parser(
118
        'enable',
119
        formatter_class=argparse.RawTextHelpFormatter,
120
        help="Enable a Sopel plugin",
121
        description=inspect.cleandoc("""
122
            Enable a Sopel plugin by its name, no matter where it comes from.
123
124
            The ``coretasks`` plugin is always enabled.
125
126
            By default, a plugin that is not excluded is enabled, unless at
127
            least one plugin is defined in the ``core.enable`` list.
128
            In that case, Sopel uses an "allow-only" policy for plugins, and
129
            all desired plugins must be added to this list.
130
        """))
131
    utils.add_common_arguments(enable_parser)
132
    enable_parser.add_argument(
133
        'names', metavar='name', nargs='+',
134
        help=inspect.cleandoc("""
135
            Name of the plugin to enable.
136
            Can be used multiple times to enable multiple plugins at once.
137
            In case of error, configuration is not modified.
138
        """))
139
    enable_parser.add_argument(
140
        '-a', '--allow-only',
141
        dest='allow_only',
142
        action='store_true',
143
        default=False,
144
        help=inspect.cleandoc("""
145
            Enforce allow-only policy.
146
            It makes sure the plugin is added to the ``core.enable`` list.
147
        """))
148
149
    return parser
150
151
152
def handle_list(options):
153
    """List Sopel plugins"""
154
    settings = utils.load_settings(options)
155
    no_color = options.no_color
156
    name_only = options.name_only
157
    enabled_only = options.enabled_only
158
    disabled_only = options.disabled_only
159
160
    # get usable plugins
161
    items = (
162
        (name, info[0], info[1])
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable name does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable info does not seem to be defined.
Loading history...
163
        for name, info in plugins.get_usable_plugins(settings).items()
164
    )
165
    items = (
166
        (name, plugin, is_enabled)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable plugin does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable is_enabled does not seem to be defined.
Loading history...
167
        for name, plugin, is_enabled in items
168
    )
169
    # filter on enabled/disabled if required
170
    if enabled_only:
171
        items = (
172
            (name, plugin, is_enabled)
173
            for name, plugin, is_enabled in items
174
            if is_enabled
175
        )
176
    elif disabled_only:
177
        items = (
178
            (name, plugin, is_enabled)
179
            for name, plugin, is_enabled in items
180
            if not is_enabled
181
        )
182
    # sort plugins
183
    items = sorted(items, key=operator.itemgetter(0))
184
185
    for name, plugin, is_enabled in items:
186
        description = {
187
            'name': name,
188
            'status': 'enabled' if is_enabled else 'disabled',
189
        }
190
191
        # optional meta description from the plugin itself
192
        try:
193
            plugin.load()
194
            description.update(plugin.get_meta_description())
195
196
            # colorize name for display purpose
197
            if not no_color:
198
                if is_enabled:
199
                    description['name'] = utils.green(name)
200
                else:
201
                    description['name'] = utils.red(name)
202
        except Exception as error:
203
            label = ('%s' % error) or 'unknown loading exception'
204
            error_status = 'error'
205
            description.update({
206
                'label': 'Error: %s' % label,
207
                'type': 'unknown',
208
                'source': 'unknown',
209
                'status': error_status,
210
            })
211
            if not no_color:
212
                if is_enabled:
213
                    # yellow instead of green
214
                    description['name'] = utils.yellow(name)
215
                else:
216
                    # keep it red for disabled plugins
217
                    description['name'] = utils.red(name)
218
                description['status'] = utils.red(error_status)
219
220
        template = '{name}/{type} {label} ({source}) [{status}]'
221
        if name_only:
222
            template = '{name}'
223
224
        print(template.format(**description))
225
226
227
def handle_show(options):
228
    """Show plugin details"""
229
    plugin_name = options.name
230
    settings = utils.load_settings(options)
231
    usable_plugins = plugins.get_usable_plugins(settings)
232
233
    # plugin does not exist
234
    if plugin_name not in usable_plugins:
235
        tools.stderr('No plugin named %s' % plugin_name)
236
        return 1
237
238
    plugin, is_enabled = usable_plugins[plugin_name]
239
    description = {
240
        'name': plugin_name,
241
        'status': 'enabled' if is_enabled else 'disabled',
242
    }
243
244
    # optional meta description from the plugin itself
245
    loaded = False
246
    try:
247
        plugin.load()
248
        description.update(plugin.get_meta_description())
249
        loaded = True
250
    except Exception as error:
251
        label = ('%s' % error) or 'unknown loading exception'
252
        error_status = 'error'
253
        description.update({
254
            'label': 'Error: %s' % label,
255
            'type': 'unknown',
256
            'source': 'unknown',
257
            'status': error_status,
258
        })
259
260
    print('Plugin:', description['name'])
261
    print('Status:', description['status'])
262
    print('Type:', description['type'])
263
    print('Source:', description['source'])
264
    print('Label:', description['label'])
265
266
    if not loaded:
267
        print('Loading failed')
268
        return 1
269
270
    print('Loaded successfully')
271
    print('Setup:', 'yes' if plugin.has_setup() else 'no')
272
    print('Shutdown:', 'yes' if plugin.has_shutdown() else 'no')
273
    print('Configure:', 'yes' if plugin.has_configure() else 'no')
274
275
276
def handle_configure(options):
277
    """Configure a Sopel plugin with a config wizard"""
278
    plugin_name = options.name
279
    settings = utils.load_settings(options)
280
    usable_plugins = plugins.get_usable_plugins(settings)
281
282
    # plugin does not exist
283
    if plugin_name not in usable_plugins:
284
        tools.stderr('No plugin named %s' % plugin_name)
285
        return 1
286
287
    plugin, is_enabled = usable_plugins[plugin_name]
288
    try:
289
        plugin.load()
290
    except Exception as error:
291
        tools.stderr('Cannot load plugin %s: %s' % (plugin_name, error))
292
        return 1
293
294
    if not plugin.has_configure():
295
        tools.stderr('Nothing to configure for plugin %s' % plugin_name)
296
        return 0  # nothing to configure is not exactly an error case
297
298
    print('Configure %s' % plugin.get_label())
299
    plugin.configure(settings)
300
    settings.save()
301
302
    if not is_enabled:
303
        tools.stderr(
304
            "Plugin {0} has been configured but is not enabled. "
305
            "Use 'sopel-plugins enable {0}' to enable it".format(plugin_name)
306
        )
307
308
309
def _handle_disable_plugin(settings, plugin_name, force):
310
    excluded = settings.core.exclude
311
    # nothing left to do if already excluded
312
    if plugin_name in excluded:
313
        tools.stderr('Plugin %s already disabled.' % plugin_name)
314
        return False
315
316
    # recalculate state: at the moment, the plugin is not in the excluded list
317
    # however, with ensure_remove, the enable list may be empty, so we have
318
    # to compute the plugin's state here, and not use what comes from
319
    # plugins.get_usable_plugins
320
    is_enabled = (
321
        not settings.core.enable or
322
        plugin_name in settings.core.enable
323
    )
324
325
    # if not enabled at this point, exclude if options.force is used
326
    if not is_enabled and not force:
327
        tools.stderr(
328
            'Plugin %s is disabled but not excluded; '
329
            'use -f/--force to force its exclusion.'
330
            % plugin_name)
331
        return False
332
333
    settings.core.exclude = excluded + [plugin_name]
334
    return True
335
336
337
def display_unknown_plugins(unknown_plugins):
338
    # at least one of the plugins does not exist
339
    tools.stderr(utils.get_many_text(
340
        unknown_plugins,
341
        one='No plugin named {item}.',
342
        two='No plugin named {first} or {second}.',
343
        many='No plugin named {left}, or {last}.'
344
    ))
345
346
347
def handle_disable(options):
348
    """Disable Sopel plugins"""
349
    plugin_names = options.names
350
    force = options.force
351
    ensure_remove = options.remove
352
    settings = utils.load_settings(options)
353
    usable_plugins = plugins.get_usable_plugins(settings)
354
    actually_disabled = []
355
356
    # coretasks is sacred
357
    if 'coretasks' in plugin_names:
358
        tools.stderr('Plugin coretasks cannot be disabled.')
359
        return 1  # do nothing and return an error code
360
361
    unknown_plugins = [
362
        name
363
        for name in plugin_names
364
        if name not in usable_plugins
365
    ]
366
    if unknown_plugins:
367
        display_unknown_plugins(unknown_plugins)
368
        return 1  # do nothing and return an error code
369
370
    # remove from enabled if asked
371
    if ensure_remove:
372
        settings.core.enable = [
373
            name
374
            for name in settings.core.enable
375
            if name not in plugin_names
376
        ]
377
        settings.save()
378
379
    # disable plugin (when needed)
380
    actually_disabled = tuple(
381
        name
382
        for name in plugin_names
383
        if _handle_disable_plugin(settings, name, force)
384
    )
385
386
    # save if required
387
    if actually_disabled:
388
        settings.save()
389
    else:
390
        return 0  # nothing to disable or save, but not an error case
391
392
    # display plugins actually disabled by the command
393
    print(utils.get_many_text(
394
        actually_disabled,
395
        one='Plugin {item} disabled.',
396
        two='Plugins {first} and {second} disabled.',
397
        many='Plugins {left}, and {last} disabled.'
398
    ))
399
400
    return 0
401
402
403
def _handle_enable_plugin(settings, usable_plugins, plugin_name, allow_only):
404
    enabled = settings.core.enable
405
    excluded = settings.core.exclude
406
407
    # coretasks is sacred
408
    if plugin_name == 'coretasks':
409
        tools.stderr('Plugin coretasks is always enabled.')
410
        return False
411
412
    # is it already enabled, but should we enforce anything?
413
    is_enabled = usable_plugins[plugin_name][1]
414
    if is_enabled and not allow_only:
415
        # already enabled, and no allow-only option: all good
416
        if plugin_name in enabled:
417
            tools.stderr('Plugin %s is already enabled.' % plugin_name)
418
        else:
419
            # suggest to use --allow-only option
420
            tools.stderr(
421
                'Plugin %s is enabled; '
422
                'use option -a/--allow-only to enforce allow only policy.'
423
                % plugin_name)
424
425
        return False
426
427
    # not enabled, or option allow_only to enforce
428
    if plugin_name in excluded:
429
        # remove from excluded
430
        settings.core.exclude = [
431
            name
432
            for name in settings.core.exclude
433
            if plugin_name != name
434
        ]
435
    elif plugin_name in enabled:
436
        # not excluded, and already in enabled list: all good
437
        tools.stderr('Plugin %s is already enabled' % plugin_name)
438
        return False
439
440
    if plugin_name not in enabled and (enabled or allow_only):
441
        # not excluded, but not enabled either: allow-only mode required
442
        # either because of the current configuration, or by request
443
        settings.core.enable = enabled + [plugin_name]
444
445
    return True
446
447
448
def handle_enable(options):
449
    """Enable a Sopel plugin"""
450
    plugin_names = options.names
451
    allow_only = options.allow_only
452
    settings = utils.load_settings(options)
453
    usable_plugins = plugins.get_usable_plugins(settings)
454
455
    # plugin does not exist
456
    unknown_plugins = [
457
        name
458
        for name in plugin_names
459
        if name not in usable_plugins
460
    ]
461
    if unknown_plugins:
462
        display_unknown_plugins(unknown_plugins)
463
        return 1  # do nothing and return an error code
464
465
    actually_enabled = tuple(
466
        name
467
        for name in plugin_names
468
        if _handle_enable_plugin(settings, usable_plugins, name, allow_only)
469
    )
470
471
    # save if required
472
    if actually_enabled:
473
        settings.save()
474
    else:
475
        return 0  # nothing to disable or save, but not an error case
476
477
    # display plugins actually disabled by the command
478
    print(utils.get_many_text(
479
        actually_enabled,
480
        one='Plugin {item} enabled.',
481
        two='Plugins {first} and {second} enabled.',
482
        many='Plugins {left}, and {last} enabled.'
483
    ))
484
    return 0
485
486
487
def main():
488
    """Console entry point for ``sopel-plugins``"""
489
    parser = build_parser()
490
    options = parser.parse_args()
491
    action = options.action
492
493
    if not action:
494
        parser.print_help()
495
        return
496
497
    if action == 'list':
498
        return handle_list(options)
499
    elif action == 'show':
500
        return handle_show(options)
501
    elif action == 'configure':
502
        return handle_configure(options)
503
    elif action == 'disable':
504
        return handle_disable(options)
505
    elif action == 'enable':
506
        return handle_enable(options)
507