Completed
Push — file-actions ( ff0f8a...be2f57 )
by Felipe A.
25s
created

PluginManagerBase._expected_import_error()   C

Complexity

Conditions 7

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
dl 0
loc 25
rs 5.5
c 0
b 0
f 0
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
import re
5
import sys
6
import argparse
7
import warnings
8
import collections
9
10
from flask import current_app
11
from werkzeug.utils import cached_property
12
13
from . import mimetype
14
from . import compat
15
from .compat import deprecated, usedoc
16
17
18
def defaultsnamedtuple(name, fields, defaults=None):
19
    '''
20
    Generate namedtuple with default values.
21
22
    :param name: name
23
    :param fields: iterable with field names
24
    :param defaults: iterable or mapping with field defaults
25
    :returns: defaultdict with given fields and given defaults
26
    :rtype: collections.defaultdict
27
    '''
28
    nt = collections.namedtuple(name, fields)
29
    nt.__new__.__defaults__ = (None,) * len(nt._fields)
30
    if isinstance(defaults, collections.Mapping):
31
        nt.__new__.__defaults__ = tuple(nt(**defaults))
32
    elif defaults:
33
        nt.__new__.__defaults__ = tuple(nt(*defaults))
34
    return nt
35
36
37
class PluginNotFoundError(ImportError):
38
    pass
39
40
41
class WidgetException(Exception):
42
    pass
43
44
45
class WidgetParameterException(WidgetException):
46
    pass
47
48
49
class InvalidArgumentError(ValueError):
50
    pass
51
52
53
class PluginManagerBase(object):
54
    '''
55
    Base plugin manager for plugin module loading and Flask extension logic.
56
    '''
57
58
    @property
59
    def namespaces(self):
60
        '''
61
        List of plugin namespaces taken from app config.
62
        '''
63
        return self.app.config['plugin_namespaces'] if self.app else []
64
65
    def __init__(self, app=None):
66
        if app is None:
67
            self.clear()
68
        else:
69
            self.init_app(app)
70
71
    def init_app(self, app):
72
        '''
73
        Initialize this Flask extension for given app.
74
        '''
75
        self.app = app
76
        if not hasattr(app, 'extensions'):
77
            app.extensions = {}
78
        app.extensions['plugin_manager'] = self
79
        self.reload()
80
81
    def reload(self):
82
        '''
83
        Clear plugin manager state and reload plugins.
84
85
        This method will make use of :meth:`clear` and :meth:`load_plugin`,
86
        so all internal state will be cleared, and all plugins defined in
87
        :data:`self.app.config['plugin_modules']` will be loaded.
88
        '''
89
        self.clear()
90
        for plugin in self.app.config.get('plugin_modules', ()):
91
            self.load_plugin(plugin)
92
93
    def clear(self):
94
        '''
95
        Clear plugin manager state.
96
        '''
97
        pass
98
99
    def _expected_import_error(self, error, name):
100
        '''
101
        Check if error could be expected when importing given module by name.
102
103
        :param error: exception
104
        :type error: BaseException
105
        :param name: module name
106
        :type name: str
107
        :returns: True if error corresponds to module name, False otherwise
108
        :rtype: bool
109
        '''
110
        if error.args:
111
            message = error.args[0]
112
            components = (
113
                name.split('.')
114
                if isinstance(error, ImportError) else
115
                (name,)
116
                )
117
            for finish in range(1, len(components) + 1):
118
                for start in range(0, finish):
119
                    part = '.'.join(components[start:finish])
120
                    for fmt in (' \'%s\'', ' "%s"', ' %s'):
121
                        if message.endswith(fmt % part):
122
                            return True
123
        return False
124
125
    def import_plugin(self, plugin):
126
        '''
127
        Import plugin by given name, looking at :attr:`namespaces`.
128
129
        :param plugin: plugin module name
130
        :type plugin: str
131
        :raises PluginNotFoundError: if not found on any namespace
132
        '''
133
        plugin = plugin.replace('-', '_')
134
        names = [
135
            '%s%s%s' % (namespace, '' if namespace[-1] == '_' else '.', plugin)
136
            if namespace else
137
            plugin
138
            for namespace in self.namespaces
139
            ]
140
141
        for name in names:
142
            if name in sys.modules:
143
                return sys.modules[name]
144
145
        for name in names:
146
            try:
147
                __import__(name)
148
                return sys.modules[name]
149
            except (ImportError, KeyError) as e:
150
                if not self._expected_import_error(e, name):
151
                    raise e
152
153
        raise PluginNotFoundError(
154
            'No plugin module %r found, tried %r' % (plugin, names),
155
            plugin, names)
156
157
    def load_plugin(self, plugin):
158
        '''
159
        Import plugin (see :meth:`import_plugin`) and load related data.
160
161
        :param plugin: plugin module name
162
        :type plugin: str
163
        :raises PluginNotFoundError: if not found on any namespace
164
        '''
165
        return self.import_plugin(plugin)
166
167
168
class RegistrablePluginManager(PluginManagerBase):
169
    '''
170
    Base plugin manager for plugin registration via :func:`register_plugin`
171
    functions at plugin module level.
172
    '''
173
    def load_plugin(self, plugin):
174
        '''
175
        Import plugin (see :meth:`import_plugin`) and load related data.
176
177
        If available, plugin's module-level :func:`register_plugin` function
178
        will be called with current plugin manager instance as first argument.
179
180
        :param plugin: plugin module name
181
        :type plugin: str
182
        :raises PluginNotFoundError: if not found on any namespace
183
        '''
184
        module = super(RegistrablePluginManager, self).load_plugin(plugin)
185
        if hasattr(module, 'register_plugin'):
186
            module.register_plugin(self)
187
        return module
188
189
190
class BlueprintPluginManager(PluginManagerBase):
191
    '''
192
    Manager for blueprint registration via :meth:`register_plugin` calls.
193
194
    Note: blueprints are not removed on `clear` nor reloaded on `reload`
195
    as flask does not allow it.
196
    '''
197
    def __init__(self, app=None):
198
        self._blueprint_known = set()
199
        super(BlueprintPluginManager, self).__init__(app=app)
200
201
    def register_blueprint(self, blueprint):
202
        '''
203
        Register given blueprint on curren app.
204
205
        This method is intended to be used on plugin's module-level
206
        :func:`register_plugin` functions.
207
208
        :param blueprint: blueprint object with plugin endpoints
209
        :type blueprint: flask.Blueprint
210
        '''
211
        if blueprint not in self._blueprint_known:
212
            self.app.register_blueprint(blueprint)
213
            self._blueprint_known.add(blueprint)
214
215
216
class ExcludePluginManager(PluginManagerBase):
217
    '''
218
    Manager for exclude-function registration via :meth:`register_exclude_fnc`
219
    calls.
220
    '''
221
    def __init__(self, app=None):
222
        self._exclude_functions = set()
223
        super(ExcludePluginManager, self).__init__(app=app)
224
225
    def register_exclude_function(self, exclude_fnc):
226
        '''
227
        Register given exclude-function on curren app.
228
229
        This method is intended to be used on plugin's module-level
230
        :func:`register_plugin` functions.
231
232
        :param blueprint: blueprint object with plugin endpoints
233
        :type blueprint: flask.Blueprint
234
        '''
235
        self._exclude_functions.add(exclude_fnc)
236
237
    def check_excluded(self, path):
238
        '''
239
        Check if given path is excluded.
240
        '''
241
        exclude_fnc = self.app.config.get('exclude_fnc')
242
        if exclude_fnc and exclude_fnc(path):
243
            return True
244
        for fnc in self._exclude_functions:
245
            if fnc(path):
246
                return True
247
        return False
248
249
    def clear(self):
250
        '''
251
        Clear plugin manager state.
252
253
        Registered exclude functions will be disposed.
254
        '''
255
        self._exclude_functions.clear()
256
        super(ExcludePluginManager, self).clear()
257
258
259
class WidgetPluginManager(PluginManagerBase):
260
    '''
261
    Plugin manager for widget registration.
262
263
    This class provides a dictionary of widget types at its
264
    :attr:`widget_types` attribute. They can be referenced by their keys on
265
    both :meth:`create_widget` and :meth:`register_widget` methods' `type`
266
    parameter, or instantiated directly and passed to :meth:`register_widget`
267
    via `widget` parameter.
268
    '''
269
    widget_types = {
270
        'base': defaultsnamedtuple(
271
            'Widget',
272
            ('place', 'type')),
273
        'link': defaultsnamedtuple(
274
            'Link',
275
            ('place', 'type', 'css', 'icon', 'text', 'endpoint', 'href'),
276
            {
277
                'type': 'link',
278
                'text': lambda f: f.name,
279
                'icon': lambda f: f.category
280
                }),
281
        'button': defaultsnamedtuple(
282
            'Button',
283
            ('place', 'type', 'css', 'text', 'endpoint', 'href'),
284
            {'type': 'button'}),
285
        'upload': defaultsnamedtuple(
286
            'Upload',
287
            ('place', 'type', 'css', 'text', 'endpoint', 'action'),
288
            {'type': 'upload'}),
289
        'stylesheet': defaultsnamedtuple(
290
            'Stylesheet',
291
            ('place', 'type', 'endpoint', 'filename', 'href'),
292
            {'type': 'stylesheet'}),
293
        'script': defaultsnamedtuple(
294
            'Script',
295
            ('place', 'type', 'endpoint', 'filename', 'src'),
296
            {'type': 'script'}),
297
        'html': defaultsnamedtuple(
298
            'Html',
299
            ('place', 'type', 'html'),
300
            {'type': 'html'}),
301
        }
302
303
    def clear(self):
304
        '''
305
        Clear plugin manager state.
306
307
        Registered widgets will be disposed after calling this method.
308
        '''
309
        self._widgets = []
310
        super(WidgetPluginManager, self).clear()
311
312
    def get_widgets(self, file=None, place=None):
313
        '''
314
        List registered widgets, optionally matching given criteria.
315
316
        :param file: optional file object will be passed to widgets' filter
317
                     functions.
318
        :type file: browsepy.file.Node or None
319
        :param place: optional template place hint.
320
        :type place: str
321
        :returns: list of widget instances
322
        :rtype: list of objects
323
        '''
324
        return list(self.iter_widgets(file, place))
325
326
    @classmethod
327
    def _resolve_widget(cls, file, widget):
328
        '''
329
        Resolve widget callable properties into static ones.
330
331
        :param file: file will be used to resolve callable properties.
332
        :type file: browsepy.file.Node
333
        :param widget: widget instance optionally with callable properties
334
        :type widget: object
335
        :returns: a new widget instance of the same type as widget parameter
336
        :rtype: object
337
        '''
338
        return widget.__class__(*[
339
            value(file) if callable(value) else value
340
            for value in widget
341
            ])
342
343
    def iter_widgets(self, file=None, place=None):
344
        '''
345
        Iterate registered widgets, optionally matching given criteria.
346
347
        :param file: optional file object will be passed to widgets' filter
348
                     functions.
349
        :type file: browsepy.file.Node or None
350
        :param place: optional template place hint.
351
        :type place: str
352
        :yields: widget instances
353
        :ytype: object
354
        '''
355
        for filter, dynamic, cwidget in self._widgets:
356
            try:
357
                if file and filter and not filter(file):
358
                    continue
359
            except BaseException as e:
360
                # Exception is handled  as this method execution is deffered,
361
                # making hard to debug for plugin developers.
362
                warnings.warn(
363
                    'Plugin action filtering failed with error: %s' % e,
364
                    RuntimeWarning
365
                    )
366
                continue
367
            if place and place != cwidget.place:
368
                continue
369
            if file and dynamic:
370
                cwidget = self._resolve_widget(file, cwidget)
371
            yield cwidget
372
373
    def create_widget(self, place, type, file=None, **kwargs):
374
        '''
375
        Create a widget object based on given arguments.
376
377
        If file object is provided, callable arguments will be resolved:
378
        its return value will be used after calling them with file as first
379
        parameter.
380
381
        All extra `kwargs` parameters will be passed to widget constructor.
382
383
        :param place: place hint where widget should be shown.
384
        :type place: str
385
        :param type: widget type name as taken from :attr:`widget_types` dict
386
                     keys.
387
        :type type: str
388
        :param file: optional file object for widget attribute resolving
389
        :type type: browsepy.files.Node or None
390
        :returns: widget instance
391
        :rtype: object
392
        '''
393
        widget_class = self.widget_types.get(type, self.widget_types['base'])
394
        kwargs.update(place=place, type=type)
395
        try:
396
            element = widget_class(**kwargs)
397
        except TypeError as e:
398
            message = e.args[0] if e.args else ''
399
            if (
400
              'unexpected keyword argument' in message or
401
              'required positional argument' in message
402
              ):
403
                raise WidgetParameterException(
404
                    'type %s; %s; available: %r'
405
                    % (type, message, widget_class._fields)
406
                    )
407
            raise e
408
        if file and any(map(callable, element)):
409
            return self._resolve_widget(file, element)
410
        return element
411
412
    def register_widget(self, place=None, type=None, widget=None, filter=None,
413
                        **kwargs):
414
        '''
415
        Create (see :meth:`create_widget`) or use provided widget and register
416
        it.
417
418
        This method provides this dual behavior in order to simplify widget
419
        creation-registration on an functional single step without sacrifycing
420
        the reusability of a object-oriented approach.
421
422
        :param place: where widget should be placed. This param conflicts
423
                      with `widget` argument.
424
        :type place: str or None
425
        :param type: widget type name as taken from :attr:`widget_types` dict
426
                     keys. This param conflicts with `widget` argument.
427
        :type type: str or None
428
        :param widget: optional widget object will be used as is. This param
429
                       conflicts with both place and type arguments.
430
        :type widget: object or None
431
        :raises TypeError: if both widget and place or type are provided at
432
                           the same time (they're mutually exclusive).
433
        :returns: created or given widget object
434
        :rtype: object
435
        '''
436
        if bool(widget) == bool(place or type):
437
            raise InvalidArgumentError(
438
                'register_widget takes either place and type or widget'
439
                )
440
        widget = widget or self.create_widget(place, type, **kwargs)
441
        dynamic = any(map(callable, widget))
442
        self._widgets.append((filter, dynamic, widget))
443
        return widget
444
445
446
class MimetypePluginManager(RegistrablePluginManager):
447
    '''
448
    Plugin manager for mimetype-function registration.
449
    '''
450
    _default_mimetype_functions = (
451
        mimetype.by_python,
452
        mimetype.by_file,
453
        mimetype.by_default,
454
        )
455
456
    def clear(self):
457
        '''
458
        Clear plugin manager state.
459
460
        Registered mimetype functions will be disposed after calling this
461
        method.
462
        '''
463
        self._mimetype_functions = list(self._default_mimetype_functions)
464
        super(MimetypePluginManager, self).clear()
465
466
    def get_mimetype(self, path):
467
        '''
468
        Get mimetype of given path calling all registered mime functions (and
469
        default ones).
470
471
        :param path: filesystem path of file
472
        :type path: str
473
        :returns: mimetype
474
        :rtype: str
475
        '''
476
        for fnc in self._mimetype_functions:
477
            mime = fnc(path)
478
            if mime:
479
                return mime
480
        return mimetype.by_default(path)
481
482
    def register_mimetype_function(self, fnc):
483
        '''
484
        Register mimetype function.
485
486
        Given function must accept a filesystem path as string and return
487
        a mimetype string or None.
488
489
        :param fnc: callable accepting a path string
490
        :type fnc: callable
491
        '''
492
        self._mimetype_functions.insert(0, fnc)
493
494
495
class ArgumentPluginManager(PluginManagerBase):
496
    '''
497
    Plugin manager for command-line argument registration.
498
499
    This function is used by browsepy's :mod:`__main__` module in order
500
    to attach extra arguments at argument-parsing time.
501
502
    This is done by :meth:`load_arguments` which imports all plugin modules
503
    and calls their respective :func:`register_arguments` module-level
504
    function.
505
    '''
506
    _argparse_kwargs = {'add_help': False}
507
    _argparse_arguments = argparse.Namespace()
508
509
    def extract_plugin_arguments(self, plugin):
510
        '''
511
        Given a plugin name, extracts its registered_arguments as an
512
        iterable of (args, kwargs) tuples.
513
514
        :param plugin: plugin name
515
        :type plugin: str
516
        :returns: iterable if (args, kwargs) tuples.
517
        :rtype: iterable
518
        '''
519
        module = self.import_plugin(plugin)
520
        if hasattr(module, 'register_arguments'):
521
            manager = ArgumentPluginManager()
522
            module.register_arguments(manager)
523
            return manager._argparse_argkwargs
524
        return ()
525
526
    def load_arguments(self, argv, base=None):
527
        '''
528
        Process given argument list based on registered arguments and given
529
        optional base :class:`argparse.ArgumentParser` instance.
530
531
        This method saves processed arguments on itself, and this state won't
532
        be lost after :meth:`clean` calls.
533
534
        Processed argument state will be available via :meth:`get_argument`
535
        method.
536
537
        :param argv: command-line arguments (without command itself)
538
        :type argv: iterable of str
539
        :param base: optional base :class:`argparse.ArgumentParser` instance.
540
        :type base: argparse.ArgumentParser or None
541
        :returns: argparse.Namespace instance with processed arguments as
542
                  given by :meth:`argparse.ArgumentParser.parse_args`.
543
        :rtype: argparse.Namespace
544
        '''
545
        plugin_parser = argparse.ArgumentParser(add_help=False)
546
        plugin_parser.add_argument('--plugin', action='append', default=[])
547
        parent = base or plugin_parser
548
        parser = argparse.ArgumentParser(
549
            parents=(parent,),
550
            add_help=False,
551
            **getattr(parent, 'defaults', {})
552
            )
553
        plugins = [
554
            plugin
555
            for plugins in plugin_parser.parse_known_args(argv)[0].plugin
556
            for plugin in plugins.split(',')
557
            ]
558
        for plugin in sorted(set(plugins), key=plugins.index):
559
            arguments = self.extract_plugin_arguments(plugin)
560
            if arguments:
561
                group = parser.add_argument_group('%s arguments' % plugin)
562
                for argargs, argkwargs in arguments:
563
                    group.add_argument(*argargs, **argkwargs)
564
        self._argparse_arguments = parser.parse_args(argv)
565
        return self._argparse_arguments
566
567
    def clear(self):
568
        '''
569
        Clear plugin manager state.
570
571
        Registered command-line arguments will be disposed after calling this
572
        method.
573
        '''
574
        self._argparse_argkwargs = []
575
        super(ArgumentPluginManager, self).clear()
576
577
    def register_argument(self, *args, **kwargs):
578
        '''
579
        Register command-line argument.
580
581
        All given arguments will be passed directly to
582
        :meth:`argparse.ArgumentParser.add_argument` calls by
583
        :meth:`load_arguments` method.
584
585
        See :meth:`argparse.ArgumentParser.add_argument` documentation for
586
        further information.
587
        '''
588
        self._argparse_argkwargs.append((args, kwargs))
589
590
    def get_argument(self, name, default=None):
591
        '''
592
        Get argument value from last :meth:`load_arguments` call.
593
594
        Keep in mind :meth:`argparse.ArgumentParser.parse_args` generates
595
        its own command-line arguments if `dest` kwarg is not provided,
596
        so ie. `--my-option` became available as `my_option`.
597
598
        :param name: command-line argument name
599
        :type name: str
600
        :param default: default value if parameter is not found
601
        :returns: command-line argument or default value
602
        '''
603
        return getattr(self._argparse_arguments, name, default)
604
605
606
class MimetypeActionPluginManager(WidgetPluginManager, MimetypePluginManager):
607
    '''
608
    Deprecated plugin API
609
    '''
610
611
    _deprecated_places = {
612
        'javascript': 'scripts',
613
        'style': 'styles',
614
        'button': 'entry-actions',
615
        'link': 'entry-link',
616
        }
617
618
    @classmethod
619
    def _mimetype_filter(cls, mimetypes):
620
        widget_mimetype_re = re.compile(
621
            '^%s$' % '$|^'.join(
622
                map(re.escape, mimetypes)
623
                ).replace('\\*', '[^/]+')
624
            )
625
626
        def handler(f):
627
            return widget_mimetype_re.match(f.type) is not None
628
629
        return handler
630
631
    def _widget_attrgetter(self, widget, name):
632
        def handler(f):
633
            app = f.app or self.app or current_app
634
            with app.app_context():
635
                return getattr(widget.for_file(f), name)
636
        return handler
637
638
    def _widget_props(self, widget, endpoint=None, mimetypes=(),
639
                      dynamic=False):
640
        type = getattr(widget, '_type', 'base')
641
        fields = self.widget_types[type]._fields
642
        with self.app.app_context():
643
            props = {
644
                name: self._widget_attrgetter(widget, name)
645
                for name in fields
646
                if hasattr(widget, name)
647
                }
648
        props.update(
649
            type=type,
650
            place=self._deprecated_places.get(widget.place),
651
            )
652
        if dynamic:
653
            props['filter'] = self._mimetype_filter(mimetypes)
654
        if 'endpoint' in fields:
655
            props['endpoint'] = endpoint
656
        return props
657
658
    @usedoc(WidgetPluginManager.__init__)
659
    def __init__(self, app=None):
660
        self._action_widgets = []
661
        super(MimetypeActionPluginManager, self).__init__(app=app)
662
663
    @usedoc(WidgetPluginManager.clear)
664
    def clear(self):
665
        self._action_widgets[:] = ()
666
        super(MimetypeActionPluginManager, self).clear()
667
668
    @cached_property
669
    def _widget(self):
670
        with warnings.catch_warnings():
671
            warnings.filterwarnings('ignore', category=DeprecationWarning)
672
            from . import widget
673
        return widget
674
675
    @cached_property
676
    @deprecated('Deprecated attribute action_class')
677
    def action_class(self):
678
        return collections.namedtuple(
679
            'MimetypeAction',
680
            ('endpoint', 'widget')
681
            )
682
683
    @cached_property
684
    @deprecated('Deprecated attribute style_class')
685
    def style_class(self):
686
        return self._widget.StyleWidget
687
688
    @cached_property
689
    @deprecated('Deprecated attribute button_class')
690
    def button_class(self):
691
        return self._widget.ButtonWidget
692
693
    @cached_property
694
    @deprecated('Deprecated attribute javascript_class')
695
    def javascript_class(self):
696
        return self._widget.JavascriptWidget
697
698
    @cached_property
699
    @deprecated('Deprecated attribute link_class')
700
    def link_class(self):
701
        return self._widget.LinkWidget
702
703
    @deprecated('Deprecated method register_action')
704
    def register_action(self, endpoint, widget, mimetypes=(), **kwargs):
705
        props = self._widget_props(widget, endpoint, mimetypes, True)
706
        self.register_widget(**props)
707
        self._action_widgets.append((widget, props['filter'], endpoint))
708
709
    @deprecated('Deprecated method get_actions')
710
    def get_actions(self, file):
711
        return [
712
            self.action_class(endpoint, deprecated.for_file(file))
713
            for deprecated, filter, endpoint in self._action_widgets
714
            if endpoint and filter(file)
715
            ]
716
717
    @usedoc(WidgetPluginManager.register_widget)
718
    def register_widget(self, place=None, type=None, widget=None, filter=None,
719
                        **kwargs):
720
        if isinstance(place or widget, self._widget.WidgetBase):
721
            warnings.warn(
722
                'Deprecated use of register_widget',
723
                category=DeprecationWarning
724
                )
725
            widget = place or widget
726
            props = self._widget_props(widget)
727
            self.register_widget(**props)
728
            self._action_widgets.append((widget, None, None))
729
            return
730
        return super(MimetypeActionPluginManager, self).register_widget(
731
            place=place, type=type, widget=widget, filter=filter, **kwargs)
732
733
    @usedoc(WidgetPluginManager.get_widgets)
734
    def get_widgets(self, file=None, place=None):
735
        if isinstance(file, compat.basestring) or \
736
          place in self._deprecated_places:
737
            warnings.warn(
738
                'Deprecated use of get_widgets',
739
                category=DeprecationWarning
740
                )
741
            place = file or place
742
            return [
743
                widget
744
                for widget, filter, endpoint in self._action_widgets
745
                if not (filter or endpoint) and place == widget.place
746
                ]
747
        return super(MimetypeActionPluginManager, self).get_widgets(
748
            file=file, place=place)
749
750
751
class PluginManager(MimetypeActionPluginManager,
752
                    BlueprintPluginManager,
753
                    ExcludePluginManager,
754
                    WidgetPluginManager,
755
                    MimetypePluginManager,
756
                    ArgumentPluginManager):
757
    '''
758
    Main plugin manager
759
760
    Provides:
761
        * Plugin module loading and Flask extension logic.
762
        * Plugin registration via :func:`register_plugin` functions at plugin
763
          module level.
764
        * Plugin blueprint registration via :meth:`register_plugin` calls.
765
        * Plugin app-level file exclusion via exclude-function registration
766
          via :meth:`register_exclude_fnc`.
767
        * Widget registration via :meth:`register_widget` method.
768
        * Mimetype function registration via :meth:`register_mimetype_function`
769
          method.
770
        * Command-line argument registration calling :func:`register_arguments`
771
          at plugin module level and providing :meth:`register_argument`
772
          method.
773
774
    This class also provides a dictionary of widget types at its
775
    :attr:`widget_types` attribute. They can be referenced by their keys on
776
    both :meth:`create_widget` and :meth:`register_widget` methods' `type`
777
    parameter, or instantiated directly and passed to :meth:`register_widget`
778
    via `widget` parameter.
779
    '''
780
    def clear(self):
781
        '''
782
        Clear plugin manager state.
783
784
        Registered widgets will be disposed after calling this method.
785
786
        Registered mimetype functions will be disposed after calling this
787
        method.
788
789
        Registered command-line arguments will be disposed after calling this
790
        method.
791
        '''
792
        super(PluginManager, self).clear()
793