Completed
Push — 5.2-unstable ( ee49a9...946858 )
by Felipe A.
01:27
created

PluginManagerBase.import_plugin()   B

Complexity

Conditions 5

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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