Completed
Push — dev-0.5.2 ( 071035...16c7f4 )
by Felipe A.
59s
created

CachePluginManager.clear()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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