Completed
Push — 5.2-unstable ( 1c6b27 )
by Felipe A.
01:00
created

PluginManager._default_event_handlers()   F

Complexity

Conditions 10

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
c 0
b 0
f 0
dl 0
loc 31
rs 3.1304

2 Methods

Rating   Name   Duplication   Size   Complexity  
B PluginManager.event_urlpaths() 0 8 5
A PluginManager.clear_browse_cache() 0 9 4

How to fix   Complexity   

Complexity

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