Completed
Push — dev-4.1-unstable ( e0128e...426fcf )
by Felipe A.
01:03
created

MimetypeActionPluginManager   A

Complexity

Total Complexity 11

Size/Duplication

Total Lines 64
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
dl 0
loc 64
rs 10
c 2
b 1
f 0
wmc 11

7 Methods

Rating   Name   Duplication   Size   Complexity  
A link_class() 0 4 1
A button_class() 0 4 1
A get_actions() 0 3 1
A javascript_class() 0 4 1
B register_action() 0 31 5
A style_class() 0 4 1
A _widget() 0 4 1
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 werkzeug.utils import cached_property
11
12
from . import mimetype
13
from .compat import deprecated
14
15
16
def defaultsnamedtuple(name, fields, defaults=None):
17
    '''
18
    Generate namedtuple with default values.
19
20
    :param name: name
21
    :param fields: iterable with field names
22
    :param defaults: iterable or mapping with field defaults
23
    :returns: defaultdict with given fields and given defaults
24
    :rtype: collections.defaultdict
25
    '''
26
    nt = collections.namedtuple(name, fields)
27
    nt.__new__.__defaults__ = (None,) * len(nt._fields)
28
    if isinstance(defaults, collections.Mapping):
29
        nt.__new__.__defaults__ = tuple(nt(**defaults))
30
    elif defaults:
31
        nt.__new__.__defaults__ = tuple(nt(*defaults))
32
    return nt
33
34
35
class PluginNotFoundError(ImportError):
36
    pass
37
38
39
class WidgetException(Exception):
40
    pass
41
42
43
class WidgetParameterException(WidgetException):
44
    pass
45
46
47
class InvalidArgumentError(ValueError):
48
    pass
49
50
51
class PluginManagerBase(object):
52
    '''
53
    Base plugin manager for plugin module loading and Flask extension logic.
54
    '''
55
56
    @property
57
    def namespaces(self):
58
        '''
59
        List of plugin namespaces taken from app config.
60
        '''
61
        return self.app.config['plugin_namespaces'] if self.app else []
62
63
    def __init__(self, app=None):
64
        if app is None:
65
            self.clear()
66
        else:
67
            self.init_app(app)
68
69
    def init_app(self, app):
70
        '''
71
        Initialize this Flask extension for given app.
72
        '''
73
        self.app = app
74
        if not hasattr(app, 'extensions'):
75
            app.extensions = {}
76
        app.extensions['plugin_manager'] = self
77
        self.reload()
78
79
    def reload(self):
80
        '''
81
        Clear plugin manager state and reload plugins.
82
        '''
83
        self.clear()
84
        for plugin in self.app.config.get('plugin_modules', ()):
85
            self.load_plugin(plugin)
86
87
    def clear(self):
88
        '''
89
        Clear plugin manager state.
90
        '''
91
        pass
92
93
    def import_plugin(self, plugin):
94
        '''
95
        Import plugin by given name, looking at :attr:`namespaces`.
96
97
        :param plugin: plugin module name
98
        :type plugin: str
99
        :raises PluginNotFoundError: if not found on any namespace
100
        '''
101
        names = [
102
            '%s.%s' % (namespace, plugin) if namespace else plugin
103
            for namespace in self.namespaces
104
            ]
105
106
        for name in names:
107
            if name in sys.modules:
108
                return sys.modules[name]
109
110
        for name in names:
111
            try:
112
                __import__(name)
113
                return sys.modules[name]
114
            except (ImportError, KeyError):
115
                pass
116
117
        raise PluginNotFoundError(
118
            'No plugin module %r found, tried %r' % (plugin, names),
119
            plugin, names)
120
121
    def load_plugin(self, plugin):
122
        '''
123
        Import plugin (see :meth:`import_plugin`) and load related data.
124
125
        :param plugin: plugin module name
126
        :type plugin: str
127
        :raises PluginNotFoundError: if not found on any namespace
128
        '''
129
        return self.import_plugin(plugin)
130
131
132
class RegistrablePluginManager(PluginManagerBase):
133
    '''
134
    Base plugin manager for plugin registration via :func:`register_plugin`
135
    functions at plugin module level.
136
    '''
137
    def load_plugin(self, plugin):
138
        '''
139
        Import plugin (see :meth:`import_plugin`) and load related data.
140
141
        If available, plugin's module-level :func:`register_plugin` function
142
        will be called with current plugin manager instance as first argument.
143
144
        :param plugin: plugin module name
145
        :type plugin: str
146
        :raises PluginNotFoundError: if not found on any namespace
147
        '''
148
        module = super(RegistrablePluginManager, self).load_plugin(plugin)
149
        if hasattr(module, 'register_plugin'):
150
            module.register_plugin(self)
151
        return module
152
153
154
class BlueprintPluginManager(RegistrablePluginManager):
155
    '''
156
    Manager for blueprint registration via :meth:`register_plugin` calls.
157
158
    Note: blueprints are not removed on `clear` nor reloaded on `reload`
159
    as flask does not allow it.
160
    '''
161
    def __init__(self, app=None):
162
        self._blueprint_known = set()
163
        super(BlueprintPluginManager, self).__init__(app=app)
164
165
    def register_blueprint(self, blueprint):
166
        '''
167
        Register given blueprint on curren app.
168
169
        This method is provided for using inside plugin's module-level
170
        :func:`register_plugin` functions.
171
172
        :param blueprint: blueprint object with plugin endpoints
173
        :type blueprint: flask.Blueprint
174
        '''
175
        if blueprint not in self._blueprint_known:
176
            self.app.register_blueprint(blueprint)
177
            self._blueprint_known.add(blueprint)
178
179
180
class WidgetPluginManager(RegistrablePluginManager):
181
    '''
182
    Plugin manager for widget registration.
183
184
    This class provides a dictionary of widget types at its
185
    :attr:`widget_types` attribute. They can be referenced by their keys on
186
    both :meth:`create_widget` and :meth:`register_widget` methods' `type`
187
    parameter, or instantiated directly and passed to :meth:`register_widget`
188
    via `widget` parameter.
189
    '''
190
    widget_types = {
191
        'base': defaultsnamedtuple(
192
            'Widget',
193
            ('place', 'type')),
194
        'link': defaultsnamedtuple(
195
            'Link',
196
            ('place', 'type', 'css', 'icon', 'text', 'endpoint', 'href'),
197
            {
198
                'type': 'link',
199
                'text': lambda f: f.name,
200
                'icon': lambda f: f.category
201
            }),
202
        'button': defaultsnamedtuple(
203
            'Button',
204
            ('place', 'type', 'css', 'text', 'endpoint', 'href'),
205
            {'type': 'button'}),
206
        'upload': defaultsnamedtuple(
207
            'Upload',
208
            ('place', 'type', 'css', 'text', 'endpoint', 'action'),
209
            {'type': 'upload'}),
210
        'stylesheet': defaultsnamedtuple(
211
            'Stylesheet',
212
            ('place', 'type', 'endpoint', 'filename', 'href'),
213
            {'type': 'stylesheet'}),
214
        'script': defaultsnamedtuple(
215
            'Script',
216
            ('place', 'type', 'endpoint', 'filename', 'src'),
217
            {'type': 'script'}),
218
        'html': defaultsnamedtuple(
219
            'Html',
220
            ('place', 'type', 'html'),
221
            {'type': 'html'}),
222
    }
223
224
    def clear(self):
225
        '''
226
        Clear plugin manager state.
227
228
        Registered widgets will be disposed after calling this method.
229
        '''
230
        self._widgets = []
231
        super(WidgetPluginManager, self).clear()
232
233
    def get_widgets(self, file=None, place=None):
234
        '''
235
        List registered widgets, optionally matching given criteria.
236
237
        :param file: optional file object will be passed to widgets' filter
238
                     functions.
239
        :type file: browsepy.file.Node or None
240
        :param place: optional template place hint.
241
        :type place: str
242
        :returns: list of widget instances
243
        :rtype: list of objects
244
        '''
245
        return list(self.iter_widgets(file, place))
246
247
    @classmethod
248
    def _resolve_widget(cls, file, widget):
249
        '''
250
        Resolve widget callable properties into static ones.
251
252
        :param file: file will be used to resolve callable properties.
253
        :type file: browsepy.file.Node
254
        :param widget: widget instance optionally with callable properties
255
        :type widget: object
256
        :returns: a new widget instance of the same type as widget parameter
257
        :rtype: object
258
        '''
259
        return widget.__class__(*[
260
            value(file) if callable(value) else value
261
            for value in widget
262
            ])
263
264
    def iter_widgets(self, file=None, place=None):
265
        '''
266
        Iterate registered widgets, optionally matching given criteria.
267
268
        :param file: optional file object will be passed to widgets' filter
269
                     functions.
270
        :type file: browsepy.file.Node or None
271
        :param place: optional template place hint.
272
        :type place: str
273
        :yields: widget instances
274
        :ytype: object
275
        '''
276
        for filter, dynamic, cwidget in self._widgets:
277
            try:
278
                if file and filter and not filter(file):
279
                    continue
280
            except BaseException as e:
281
                # Exception is handled  as this method execution is deffered,
282
                # making hard to debug for plugin developers.
283
                warnings.warn(
284
                    'Plugin action filtering failed with error: %s' % e,
285
                    RuntimeWarning
286
                    )
287
                continue
288
            if place and place != cwidget.place:
289
                continue
290
            if file and dynamic:
291
                cwidget = self._resolve_widget(file, cwidget)
292
            yield cwidget
293
294
    def create_widget(self, place, type, file=None, **kwargs):
295
        '''
296
        Create a widget object based on given arguments.
297
298
        If file object is provided, callable arguments will be resolved:
299
        its return value will be used after calling them with file as first
300
        parameter.
301
302
        All extra `kwargs` parameters will be passed to widget constructor.
303
304
        :param place: place hint where widget should be shown.
305
        :type place: str
306
        :param type: widget type name as taken from :attr:`widget_types` dict
307
                     keys.
308
        :type type: str
309
        :param file: optional file object for widget attribute resolving
310
        :type type: browsepy.files.Node or None
311
        :returns: widget instance
312
        :rtype: object
313
        '''
314
        widget_class = self.widget_types.get(type, self.widget_types['base'])
315
        kwargs.update(place=place, type=type)
316
        try:
317
            element = widget_class(**kwargs)
318
        except TypeError as e:
319
            message = e.args[0] if e.args else ''
320
            if (
321
              'unexpected keyword argument' in message or
322
              'required positional argument' in message
323
              ):
324
                raise WidgetParameterException(
325
                    'type %s; %s; available: %r'
326
                    % (type, message, widget_class._fields)
327
                    )
328
            raise e
329
        if file and any(map(callable, element)):
330
            return self._resolve_widget(file, element)
331
        return element
332
333
    def register_widget(self, place=None, type=None, widget=None, filter=None,
334
                        **kwargs):
335
        '''
336
        Create (see :meth:`create_widget`) or use provided widget and register
337
        it.
338
339
        This method provides this dual behavior in order to simplify widget
340
        creation-registration on an functional single step without sacrifycing
341
        the reusability of a object-oriented approach.
342
343
        :param place: where widget should be placed. This param conflicts
344
                      with `widget` argument.
345
        :type place: str or None
346
        :param type: widget type name as taken from :attr:`widget_types` dict
347
                     keys. This param conflicts with `widget` argument.
348
        :type type: str or None
349
        :param widget: optional widget object will be used as is. This param
350
                       conflicts with both place and type arguments.
351
        :type widget: object or None
352
        :raises TypeError: if both widget and place or type are provided at
353
                           the same time (they're mutually exclusive).
354
        :returns: created or given widget object
355
        :rtype: object
356
        '''
357
        if bool(widget) == bool(place or type):
358
            raise InvalidArgumentError(
359
                'register_widget takes either place and type or widget'
360
                )
361
        widget = widget or self.create_widget(place, type, **kwargs)
362
        dynamic = any(map(callable, widget))
363
        self._widgets.append((filter, dynamic, widget))
364
        return widget
365
366
367
class MimetypePluginManager(RegistrablePluginManager):
368
    '''
369
    Plugin manager for mimetype-function registration.
370
    '''
371
    _default_mimetype_functions = (
372
        mimetype.by_python,
373
        mimetype.by_file,
374
        mimetype.by_default,
375
    )
376
377
    def clear(self):
378
        '''
379
        Clear plugin manager state.
380
381
        Registered mimetype functions will be disposed after calling this
382
        method.
383
        '''
384
        self._mimetype_functions = list(self._default_mimetype_functions)
385
        super(MimetypePluginManager, self).clear()
386
387
    def get_mimetype(self, path):
388
        '''
389
        Get mimetype of given path calling all registered mime functions (and
390
        default ones).
391
392
        :param path: filesystem path of file
393
        :type path: str
394
        :returns: mimetype
395
        :rtype: str
396
        '''
397
        for fnc in self._mimetype_functions:
398
            mime = fnc(path)
399
            if mime:
400
                return mime
401
        return mimetype.by_default(path)
402
403
    def register_mimetype_function(self, fnc):
404
        '''
405
        Register mimetype function.
406
407
        Given function must accept a filesystem path as string and return
408
        a mimetype string or None.
409
410
        :param fnc: callable accepting a path string
411
        :type fnc: callable
412
        '''
413
        self._mimetype_functions.insert(0, fnc)
414
415
416
class ArgumentPluginManager(PluginManagerBase):
417
    '''
418
    Plugin manager for command-line argument registration.
419
420
    This function is used by browsepy's :mod:`__main__` module in order
421
    to attach extra arguments at argument-parsing time.
422
423
    This is done by :meth:`load_arguments` which imports all plugin modules
424
    and calls their respective :func:`register_arguments` module-level
425
    function.
426
    '''
427
    _argparse_kwargs = {'add_help': False}
428
    _argparse_arguments = argparse.Namespace()
429
430
    def load_arguments(self, argv, base=None):
431
        '''
432
        Process given argument list based on registered arguments and given
433
        optional base :class:`argparse.ArgumentParser` instance.
434
435
        This method saves processed arguments on itself, and this state won't
436
        be lost after :meth:`clean` calls.
437
438
        Processed argument state will be available via :meth:`get_argument`
439
        method.
440
441
        :param argv: command-line arguments (without command itself)
442
        :type argv: iterable of str
443
        :param base: optional base :class:`argparse.ArgumentParser` instance.
444
        :type base: argparse.ArgumentParser or None
445
        :returns: argparse.Namespace instance with processed arguments as
446
                  given by :meth:`argparse.ArgumentParser.parse_args`.
447
        :rtype: argparse.Namespace
448
        '''
449
        plugin_parser = argparse.ArgumentParser(add_help=False)
450
        plugin_parser.add_argument(
451
            '--plugin',
452
            type=lambda x: x.split(',') if x else [],
453
            default=[]
454
            )
455
        parser = argparse.ArgumentParser(
456
            parents=(base or plugin_parser,),
457
            add_help=False
458
            )
459
        for plugin in plugin_parser.parse_known_args(argv)[0].plugin:
460
            module = self.import_plugin(plugin)
461
            if hasattr(module, 'register_arguments'):
462
                manager = ArgumentPluginManager()
463
                module.register_arguments(manager)
464
                group = parser.add_argument_group('%s arguments' % plugin)
465
                for argargs, argkwargs in manager._argparse_argkwargs:
466
                    group.add_argument(*argargs, **argkwargs)
467
        self._argparse_arguments = parser.parse_args(argv)
468
        return self._argparse_arguments
469
470
    def clear(self):
471
        '''
472
        Clear plugin manager state.
473
474
        Registered command-line arguments will be disposed after calling this
475
        method.
476
        '''
477
        self._argparse_argkwargs = []
478
        super(ArgumentPluginManager, self).clear()
479
480
    def register_argument(self, *args, **kwargs):
481
        '''
482
        Register command-line argument.
483
484
        All given arguments will be passed directly to
485
        :meth:`argparse.ArgumentParser.add_argument` calls by
486
        :meth:`load_arguments` method.
487
488
        See :meth:`argparse.ArgumentParser.add_argument` documentation for
489
        further information.
490
        '''
491
        self._argparse_argkwargs.append((args, kwargs))
492
493
    def get_argument(self, name, default=None):
494
        '''
495
        Get argument value from last :meth:`load_arguments` call.
496
497
        Keep in mind :meth:`argparse.ArgumentParser.parse_args` generates
498
        its own command-line arguments if `dest` kwarg is not provided,
499
        so ie. `--my-option` became available as `my_option`.
500
501
        :param name: command-line argument name
502
        :type name: str
503
        :param default: default value if parameter is not found
504
        :returns: command-line argument or default value
505
        '''
506
        return getattr(self._argparse_arguments, name, default)
507
508
509
class MimetypeActionPluginManager(WidgetPluginManager, MimetypePluginManager):
510
    '''
511
    Deprecated plugin API
512
    '''
513
    @cached_property
514
    def _widget(self):
515
        from . import widget
516
        return widget
517
518
    @cached_property
519
    @deprecated('Deprecated attribute style_class')
520
    def style_class(self):
521
        return self._widget.StyleWidget
522
523
    @cached_property
524
    @deprecated('Deprecated attribute button_class')
525
    def button_class(self):
526
        return self._widget.ButtonWidget
527
528
    @cached_property
529
    @deprecated('Deprecated attribute javascript_class')
530
    def javascript_class(self):
531
        return self._widget.JavascriptWidget
532
533
    @cached_property
534
    @deprecated('Deprecated attribute link_class')
535
    def link_class(self):
536
        return self._widget.LinkWidget
537
538
    @deprecated('Deprecated method register_action')
539
    def register_action(self, endpoint, widget, mimetypes=(), **kwargs):
540
        deprecated_places = {
541
            'javascript': 'scripts',
542
            'style': 'styles',
543
            'button': 'entry-actions',
544
            'link': 'entry-link',
545
            }
546
        widget_mimetype_re = re.compile(
547
            '^%s$' % '$|^'.join(
548
                map(re.escape, mimetypes)
549
                ).replace('\\*', '[^/]+')
550
            )
551
        widget_type = getattr(widget, '_type', 'base')
552
        widget_fields = self.widget_types[widget_type]._fields
553
        widget_props = {
554
            name: getattr(widget, name)
555
            for name in widget_fields
556
            if hasattr(widget, name)
557
            }
558
        widget_props.update(
559
            type=widget_type,
560
            place=deprecated_places.get(
561
                widget_props['place'],
562
                widget_props['place']
563
                ),
564
            filter=lambda f: widget_mimetype_re.match(f.type) is not None
565
            )
566
        if 'endpoint' in widget_fields:
567
            widget_props['endpoint'] = endpoint
568
        self.register_widget(**widget_props)
569
570
    @deprecated('Deprecated method get_actions')
571
    def get_actions(self, file):
572
        return self.get_widgets(file=file)
573
574
575
class PluginManager(MimetypeActionPluginManager,
576
                    BlueprintPluginManager, WidgetPluginManager,
577
                    MimetypePluginManager, ArgumentPluginManager):
578
    '''
579
    Main plugin manager
580
581
    Provides:
582
        * Plugin module loading and Flask extension logic.
583
        * Plugin registration via :func:`register_plugin` functions at plugin
584
          module level.
585
        * Plugin blueprint registration via :meth:`register_plugin` calls.
586
        * Widget registration via :meth:`register_widget` method.
587
        * Mimetype function registration via :meth:`register_mimetype_function`
588
          method.
589
        * Command-line argument registration calling :func:`register_arguments`
590
          at plugin module level and providing :meth:`register_argument`
591
          method.
592
593
    This class also provides a dictionary of widget types at its
594
    :attr:`widget_types` attribute. They can be referenced by their keys on
595
    both :meth:`create_widget` and :meth:`register_widget` methods' `type`
596
    parameter, or instantiated directly and passed to :meth:`register_widget`
597
    via `widget` parameter.
598
    '''
599
    def clear(self):
600
        '''
601
        Clear plugin manager state.
602
603
        Registered widgets will be disposed after calling this method.
604
605
        Registered mimetype functions will be disposed after calling this
606
        method.
607
608
        Registered command-line arguments will be disposed after calling this
609
        method.
610
        '''
611
        super(PluginManager, self).clear()
612