1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2009       Benny Malengier
5# Copyright (C) 2011       Tim G L Lyons
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22"""
23This module provides the base class for plugin registration.
24It provides an object containing data about the plugin (version, filename, ...)
25and a register for the data of all plugins .
26"""
27#-------------------------------------------------------------------------
28#
29# Standard Python modules
30#
31#-------------------------------------------------------------------------
32import os
33import sys
34import re
35import traceback
36
37#-------------------------------------------------------------------------
38#
39# Gramps modules
40#
41#-------------------------------------------------------------------------
42from ...version import VERSION as GRAMPSVERSION, VERSION_TUPLE
43from ..const import IMAGE_DIR
44from ..const import GRAMPS_LOCALE as glocale
45_ = glocale.translation.gettext
46import logging
47LOG = logging.getLogger('._manager')
48
49#-------------------------------------------------------------------------
50#
51# PluginData
52#
53#-------------------------------------------------------------------------
54
55#a plugin is stable or unstable
56STABLE = 0
57UNSTABLE = 1
58STATUS = [STABLE, UNSTABLE]
59STATUSTEXT = {STABLE: _('Stable'), UNSTABLE: _('Unstable')}
60#possible plugin types
61REPORT = 0
62QUICKREPORT = 1 # deprecated
63QUICKVIEW = 1
64TOOL = 2
65IMPORT = 3
66EXPORT = 4
67DOCGEN = 5
68GENERAL = 6
69MAPSERVICE = 7
70VIEW = 8
71RELCALC = 9
72GRAMPLET = 10
73SIDEBAR = 11
74DATABASE = 12
75RULE = 13
76PTYPE = [REPORT, QUICKREPORT, TOOL, IMPORT, EXPORT, DOCGEN, GENERAL,
77         MAPSERVICE, VIEW, RELCALC, GRAMPLET, SIDEBAR, DATABASE, RULE]
78PTYPE_STR = {
79        REPORT: _('Report') ,
80        QUICKREPORT: _('Quickreport'),
81        TOOL: _('Tool'),
82        IMPORT: _('Importer'),
83        EXPORT: _('Exporter'),
84        DOCGEN: _('Doc creator'),
85        GENERAL: _('Plugin lib'),
86        MAPSERVICE: _('Map service'),
87        VIEW: _('Gramps View'),
88        RELCALC: _('Relationships'),
89        GRAMPLET: _('Gramplet'),
90        SIDEBAR: _('Sidebar'),
91        DATABASE: _('Database'),
92        RULE: _('Rule')
93        }
94
95#possible report categories
96CATEGORY_TEXT = 0
97CATEGORY_DRAW = 1
98CATEGORY_CODE = 2
99CATEGORY_WEB = 3
100CATEGORY_BOOK = 4
101CATEGORY_GRAPHVIZ = 5
102CATEGORY_TREE = 6
103REPORT_CAT = [ CATEGORY_TEXT, CATEGORY_DRAW, CATEGORY_CODE,
104                        CATEGORY_WEB, CATEGORY_BOOK, CATEGORY_GRAPHVIZ,
105                        CATEGORY_TREE]
106#possible tool categories
107TOOL_DEBUG = -1
108TOOL_ANAL = 0
109TOOL_DBPROC = 1
110TOOL_DBFIX = 2
111TOOL_REVCTL = 3
112TOOL_UTILS = 4
113TOOL_CAT = [ TOOL_DEBUG, TOOL_ANAL, TOOL_DBPROC, TOOL_DBFIX, TOOL_REVCTL,
114                TOOL_UTILS]
115
116#possible quickreport categories
117CATEGORY_QR_MISC = -1
118CATEGORY_QR_PERSON = 0
119CATEGORY_QR_FAMILY = 1
120CATEGORY_QR_EVENT = 2
121CATEGORY_QR_SOURCE = 3
122CATEGORY_QR_PLACE = 4
123CATEGORY_QR_REPOSITORY = 5
124CATEGORY_QR_NOTE = 6
125CATEGORY_QR_DATE = 7
126CATEGORY_QR_MEDIA = 8
127CATEGORY_QR_CITATION = 9
128CATEGORY_QR_SOURCE_OR_CITATION = 10
129
130# Modes for generating reports
131REPORT_MODE_GUI = 1    # Standalone report using GUI
132REPORT_MODE_BKI = 2    # Book Item interface using GUI
133REPORT_MODE_CLI = 4    # Command line interface (CLI)
134REPORT_MODES = [REPORT_MODE_GUI, REPORT_MODE_BKI, REPORT_MODE_CLI]
135
136# Modes for running tools
137TOOL_MODE_GUI = 1    # Standard tool using GUI
138TOOL_MODE_CLI = 2    # Command line interface (CLI)
139TOOL_MODES = [TOOL_MODE_GUI, TOOL_MODE_CLI]
140
141# possible view orders
142START = 1
143END = 2
144
145#-------------------------------------------------------------------------
146#
147# Functions and classes
148#
149#-------------------------------------------------------------------------
150def myint(s):
151    """
152    Protected version of int()
153    """
154    try:
155        v = int(s)
156    except:
157        v = s
158    return v
159
160def version(sversion):
161    """
162    Return the tuple version of a string version.
163    """
164    return tuple([myint(x or "0") for x in (sversion + "..").split(".")])
165
166def valid_plugin_version(plugin_version_string):
167    """
168    Checks to see if string is a valid version string for this version
169    of Gramps.
170    """
171    if not isinstance(plugin_version_string, str): return False
172    dots = plugin_version_string.count(".")
173    if dots == 1:
174        plugin_version = tuple(map(int, plugin_version_string.split(".", 1)))
175        return plugin_version == VERSION_TUPLE[:2]
176    elif dots == 2:
177        plugin_version = tuple(map(int, plugin_version_string.split(".", 2)))
178        return (plugin_version[:2] == VERSION_TUPLE[:2] and
179                plugin_version <= VERSION_TUPLE)
180    return False
181
182class PluginData:
183    """
184    This is the base class for all plugin data objects.
185    The workflow is:
186
187    1. plugin manager reads all register files, and stores plugin data
188       objects in a plugin register
189    2. when plugin is needed, the plugin register creates the plugin, and
190       the manager stores this, after which it can be executed.
191
192    Attributes present for all plugins
193
194    .. attribute:: id
195       A unique identifier for the plugin. This is eg used to store the plugin
196       settings.  MUST be in ASCII, with only "_- ().,'" special characters.
197    .. attribute:: name
198       A friendly name to call this plugin (normally translated)
199    .. attribute:: name_accell
200       A friendly name to call this plugin (normally translated), with an
201       accellerator present (eg '_Descendant report', with D to be accellerator
202       key
203    .. attribute:: description
204       A friendly description of what the plugin does
205    .. attribute:: version
206       The version of the plugin
207    .. attribute:: status
208       The status of the plugin, STABLE or UNSTABLE
209       UNSTABLE is only visible in development code, not in release
210    .. attribute:: fname
211       The python file where the plugin implementation can be found
212    .. attribute:: fpath
213       The python path where the plugin implementation can be found
214    .. attribute:: ptype
215       The plugin type. One of REPORT , QUICKREPORT, TOOL, IMPORT,
216        EXPORT, DOCGEN, GENERAL, MAPSERVICE, VIEW, GRAMPLET, DATABASE, RULE
217    .. attribute:: authors
218       List of authors of the plugin, default=[]
219    .. attribute:: authors_email
220       List of emails of the authors of the plugin, default=[]
221    .. attribute:: supported
222       Bool value indicating if the plugin is still supported, default=True
223    .. attribute:: load_on_reg
224       bool value, if True, the plugin is loaded on Gramps startup. Some
225       plugins. Only set this value if for testing you want the plugin to be
226       loaded immediately on startup. default=False
227    .. attribute: icons
228       New stock icons to register. A list of tuples (stock_id, icon_label),
229       eg:
230            [('gramps_myplugin', _('My Plugin')),
231            ('gramps_myplugin_open', _('Open Plugin')]
232       The icon directory must contain the directories scalable, 48x48, 22x22
233       and 16x16 with the icons, eg:
234            scalable/gramps_myplugin.svg
235            48x48/gramps_myplugin.png
236            22x22/gramps_myplugin.png
237    .. attribute: icondir
238       The directory to use for the icons. If icondir is not set or None, it
239       reverts to the plugindirectory itself.
240
241    Attributes for RELCALC plugins:
242
243    .. attribute:: relcalcclass
244       The class in the module that is the relationcalc class
245    .. attribute:: lang_list
246       List of languages this plugin handles
247
248    Attributes for REPORT plugins:
249
250    .. attribute:: require_active
251       Bool, If the reports requries an active person to be set or not
252    .. attribute:: reportclass
253       The class in the module that is the report class
254    .. attribute:: report_modes
255       The report modes: list of REPORT_MODE_GUI ,REPORT_MODE_BKI,REPORT_MODE_CLI
256
257    Attributes for REPORT and TOOL and QUICKREPORT and VIEW plugins
258
259    .. attribute:: category
260       Or the report category the plugin belongs to, default=CATEGORY_TEXT
261       or the tool category a plugin belongs to, default=TOOL_UTILS
262       or the quickreport category a plugin belongs to, default=CATEGORY_QR_PERSON
263       or the view category a plugin belongs to,
264           default=("Miscellaneous", _("Miscellaneous"))
265
266    Attributes for REPORT and TOOL and DOCGEN plugins
267
268    .. attribute:: optionclass
269       The class in the module that is the option class
270
271    Attributes for TOOL plugins
272
273    .. attribute:: toolclass
274       The class in the module that is the tool class
275    .. attribute:: tool_modes
276       The tool modes: list of TOOL_MODE_GUI, TOOL_MODE_CLI
277
278    Attributes for DOCGEN plugins
279
280    .. attribute :: docclass
281       The class in the module that is the BaseDoc defined
282    .. attribute :: paper
283       bool, Indicates whether the plugin uses paper or not, default=True
284    .. attribute :: style
285       bool, Indicates whether the plugin uses styles or not, default=True
286
287    Attribute for DOCGEN, EXPORT plugins
288
289    .. attribute :: extension
290       str, The file extension to use for output produced by the docgen/export,
291       default=''
292
293    Attributes for QUICKREPORT plugins
294
295    .. attribute:: runfunc
296       The function that executes the quick report
297
298    Attributes for MAPSERVICE plugins
299
300    .. attribute:: mapservice
301       The class in the module that is a mapservice
302
303    Attributes for EXPORT plugins
304
305    .. attribute:: export_function
306       Function that produces the export
307    .. attribute:: export_options
308       Class to set options
309    .. attribute:: export_options_title
310       Title for the option page
311
312    Attributes for IMPORT plugins
313
314    .. attribute:: import_function
315       Function that starts an import
316
317    Attributes for GRAMPLET plugins
318
319    .. attribute:: gramplet
320       The function or class that defines the gramplet.
321    .. attribute:: height
322       The height the gramplet should have in a column on GrampletView,
323       default = 200
324    .. attribute:: detached_height
325       The height the gramplet should have detached, default 300
326    .. attribute:: detached_width
327       The width the gramplet should have detached, default 400
328    .. attribute:: expand
329       If the attributed should be expanded on start, default False
330    .. attribute:: gramplet_title
331       Title to use for the gramplet, default = _('Gramplet')
332    .. attribute:: navtypes
333       Navigation types that the gramplet is appropriate for, default = []
334    .. attribute:: help_url
335       The URL where documentation for the URL can be found
336
337    Attributes for VIEW plugins
338
339    .. attribute:: viewclass
340       A class of type ViewCreator that holds the needed info of the
341       view to be created: icon, viewclass that derives from pageview, ...
342    .. attribute:: stock_icon
343       The icon in the toolbar or sidebar used to select the view
344
345    Attributes for SIDEBAR plugins
346
347    .. attribute:: sidebarclass
348       The class that defines the sidebar.
349    .. attribute:: menu_label
350       A label to use on the seltion menu.
351
352    Attributes for VIEW and SIDEBAR plugins
353
354    .. attribute:: order
355       order can be START or END. Default is END. For END, on registering,
356       the plugin is appended to the list of plugins. If START, then the
357       plugin is prepended. Only set START if you want a plugin to be the
358       first in the order of plugins
359
360    Attributes for DATABASE plugins
361
362    .. attribute:: databaseclass
363       The class in the module that is the database class
364    .. attribute:: reset_system
365       Boolean to indicate that the system (sys.modules) should
366       be reset.
367
368    Attributes for RULE plugins
369
370    .. attribute:: namespace
371       The class (Person, Event, Media, etc.) the rule applies to.
372    .. attribute:: ruleclass
373       The exact class name of the rule; ex: HasSourceParameter
374    """
375
376    def __init__(self):
377        #read/write attribute
378        self.directory = None
379        #base attributes
380        self._id = None
381        self._name = None
382        self._name_accell = None
383        self._version = None
384        self._gramps_target_version = None
385        self._description = None
386        self._status = UNSTABLE
387        self._fname = None
388        self._fpath = None
389        self._ptype = None
390        self._authors = []
391        self._authors_email = []
392        self._supported = True
393        self._load_on_reg = False
394        self._icons = []
395        self._icondir = None
396        self._depends_on = []
397        self._include_in_listing = True
398        #derived var
399        self.mod_name = None
400        #RELCALC attr
401        self._relcalcclass = None
402        self._lang_list = None
403        #REPORT attr
404        self._reportclass = None
405        self._require_active = True
406        self._report_modes = [REPORT_MODE_GUI]
407        #REPORT and TOOL and GENERAL attr
408        self._category = None
409        #REPORT and TOOL attr
410        self._optionclass = None
411        #TOOL attr
412        self._toolclass = None
413        self._tool_modes = [TOOL_MODE_GUI]
414        #DOCGEN attr
415        self._paper = True
416        self._style = True
417        self._extension = ''
418        #QUICKREPORT attr
419        self._runfunc = None
420        #MAPSERVICE attr
421        self._mapservice = None
422        #EXPORT attr
423        self._export_function = None
424        self._export_options = None
425        self._export_options_title = ''
426        #IMPORT attr
427        self._import_function = None
428        #GRAMPLET attr
429        self._gramplet = None
430        self._height = 200
431        self._detached_height = 300
432        self._detached_width = 400
433        self._expand = False
434        self._gramplet_title = _('Gramplet')
435        self._navtypes = []
436        self._orientation = None
437        self._help_url = None
438        #VIEW attr
439        self._viewclass = None
440        self._stock_icon = None
441        #SIDEBAR attr
442        self._sidebarclass = None
443        self._menu_label = ''
444        #VIEW and SIDEBAR attr
445        self._order = END
446        #DATABASE attr
447        self._databaseclass = None
448        self._reset_system = False
449        #GENERAL attr
450        self._data = []
451        self._process = None
452        #RULE attr
453        self._ruleclass = None
454        self._namespace = None
455
456    def _set_id(self, id):
457       self._id = id
458
459    def _get_id(self):
460        return self._id
461
462    def _set_name(self, name):
463        self._name = name
464
465    def _get_name(self):
466        return self._name
467
468    def _set_name_accell(self, name):
469        self._name_accell = name
470
471    def _get_name_accell(self):
472        if self._name_accell is None:
473            return self._name
474        else:
475            return self._name_accell
476
477    def _set_description(self, description):
478        self._description = description
479
480    def _get_description(self):
481        return self._description
482
483    def _set_version(self, version):
484       self._version = version
485
486    def _get_version(self):
487        return self._version
488
489    def _set_gramps_target_version(self, version):
490       self._gramps_target_version = version
491
492    def _get_gramps_target_version(self):
493        return self._gramps_target_version
494
495    def _set_status(self, status):
496        if status not in STATUS:
497            raise ValueError('plugin status cannot be %s' % str(status))
498        self._status = status
499
500    def _get_status(self):
501        return self._status
502
503    def _set_fname(self, fname):
504        self._fname = fname
505
506    def _get_fname(self):
507        return self._fname
508
509    def _set_fpath(self, fpath):
510        self._fpath = fpath
511
512    def _get_fpath(self):
513        return self._fpath
514
515    def _set_ptype(self, ptype):
516        if ptype not in PTYPE:
517            raise ValueError('Plugin type cannot be %s' % str(ptype))
518        elif self._ptype is not None:
519            raise ValueError('Plugin type may not be changed')
520        self._ptype = ptype
521        if self._ptype == REPORT:
522            self._category = CATEGORY_TEXT
523        elif self._ptype == TOOL:
524            self._category = TOOL_UTILS
525        elif self._ptype == QUICKREPORT:
526            self._category = CATEGORY_QR_PERSON
527        elif self._ptype == VIEW:
528            self._category = ("Miscellaneous", _("Miscellaneous"))
529        #if self._ptype == DOCGEN:
530        #    self._load_on_reg = True
531
532    def _get_ptype(self):
533        return self._ptype
534
535    def _set_authors(self, authors):
536        if not authors or not isinstance(authors, list):
537            return
538        self._authors = authors
539
540    def _get_authors(self):
541        return self._authors
542
543    def _set_authors_email(self, authors_email):
544        if not authors_email or not isinstance(authors_email, list):
545            return
546        self._authors_email = authors_email
547
548    def _get_authors_email(self):
549        return self._authors_email
550
551    def _set_supported(self, supported):
552        if not isinstance(supported, bool):
553            raise ValueError('Plugin must have supported=True or False')
554        self._supported = supported
555
556    def _get_supported(self):
557        return self._supported
558
559    def _set_load_on_reg(self, load_on_reg):
560        if not isinstance(load_on_reg, bool):
561            raise ValueError('Plugin must have load_on_reg=True or False')
562        self._load_on_reg = load_on_reg
563
564    def _get_load_on_reg(self):
565        return self._load_on_reg
566
567    def _get_icons(self):
568        return self._icons
569
570    def _set_icons(self, icons):
571        if not isinstance(icons, list):
572            raise ValueError('Plugin must have icons as a list')
573        self._icons = icons
574
575    def _get_icondir(self):
576        return self._icondir
577
578    def _set_icondir(self, icondir):
579        self._icondir = icondir
580
581    def _get_depends_on(self):
582        return self._depends_on
583
584    def _set_depends_on(self, depends):
585        if not isinstance(depends, list):
586            raise ValueError('Plugin must have depends_on as a list')
587        self._depends_on = depends
588
589    def _get_include_in_listing(self):
590        return self._include_in_listing
591
592    def _set_include_in_listing(self, include):
593        if not isinstance(include, bool):
594            raise ValueError('Plugin must have include_in_listing as a bool')
595        self._include_in_listing = include
596
597    id = property(_get_id, _set_id)
598    name = property(_get_name, _set_name)
599    name_accell = property(_get_name_accell, _set_name_accell)
600    description = property(_get_description, _set_description)
601    version = property(_get_version, _set_version)
602    gramps_target_version = property(_get_gramps_target_version,
603                                     _set_gramps_target_version)
604    status = property(_get_status, _set_status)
605    fname = property(_get_fname, _set_fname)
606    fpath = property(_get_fpath, _set_fpath)
607    ptype = property(_get_ptype, _set_ptype)
608    authors = property(_get_authors, _set_authors)
609    authors_email = property(_get_authors_email, _set_authors_email)
610    supported = property(_get_supported, _set_supported)
611    load_on_reg = property(_get_load_on_reg, _set_load_on_reg)
612    icons = property(_get_icons, _set_icons)
613    icondir = property(_get_icondir, _set_icondir)
614    depends_on = property(_get_depends_on, _set_depends_on)
615    include_in_listing = property(_get_include_in_listing, _set_include_in_listing)
616
617    def statustext(self):
618        return STATUSTEXT[self.status]
619
620    #type specific plugin attributes
621
622    #RELCALC attributes
623    def _set_relcalcclass(self, relcalcclass):
624        if not self._ptype == RELCALC:
625            raise ValueError('relcalcclass may only be set for RELCALC plugins')
626        self._relcalcclass = relcalcclass
627
628    def _get_relcalcclass(self):
629        return self._relcalcclass
630
631    def _set_lang_list(self, lang_list):
632        if not self._ptype == RELCALC:
633            raise ValueError('relcalcclass may only be set for RELCALC plugins')
634        self._lang_list = lang_list
635
636    def _get_lang_list(self):
637        return self._lang_list
638
639    relcalcclass = property(_get_relcalcclass, _set_relcalcclass)
640    lang_list = property(_get_lang_list, _set_lang_list)
641
642    #REPORT attributes
643    def _set_require_active(self, require_active):
644        if not self._ptype == REPORT:
645            raise ValueError('require_active may only be set for REPORT plugins')
646        if not isinstance(require_active, bool):
647            raise ValueError('Report must have require_active=True or False')
648        self._require_active = require_active
649
650    def _get_require_active(self):
651        return self._require_active
652
653    def _set_reportclass(self, reportclass):
654        if not self._ptype == REPORT:
655            raise ValueError('reportclass may only be set for REPORT plugins')
656        self._reportclass = reportclass
657
658    def _get_reportclass(self):
659        return self._reportclass
660
661    def _set_report_modes(self, report_modes):
662        if not self._ptype == REPORT:
663            raise ValueError('report_modes may only be set for REPORT plugins')
664        if not isinstance(report_modes, list):
665            raise ValueError('report_modes must be a list')
666        self._report_modes = [x for x in report_modes if x in REPORT_MODES]
667        if not self._report_modes:
668            raise ValueError('report_modes not a valid list of modes')
669
670    def _get_report_modes(self):
671        return self._report_modes
672
673    #REPORT or TOOL or QUICKREPORT or GENERAL attributes
674    def _set_category(self, category):
675        if self._ptype not in [REPORT, TOOL, QUICKREPORT, VIEW, GENERAL]:
676            raise ValueError('category may only be set for ' \
677                              'REPORT/TOOL/QUICKREPORT/VIEW/GENERAL plugins')
678        self._category = category
679
680    def _get_category(self):
681        return self._category
682
683    #REPORT OR TOOL attributes
684    def _set_optionclass(self, optionclass):
685        if not (self._ptype == REPORT or self.ptype == TOOL or self._ptype == DOCGEN):
686            raise ValueError('optionclass may only be set for REPORT/TOOL/DOCGEN plugins')
687        self._optionclass = optionclass
688
689    def _get_optionclass(self):
690        return self._optionclass
691
692    #TOOL attributes
693    def _set_toolclass(self, toolclass):
694        if not self._ptype == TOOL:
695            raise ValueError('toolclass may only be set for TOOL plugins')
696        self._toolclass = toolclass
697
698    def _get_toolclass(self):
699        return self._toolclass
700
701    def _set_tool_modes(self, tool_modes):
702        if not self._ptype == TOOL:
703            raise ValueError('tool_modes may only be set for TOOL plugins')
704        if not isinstance(tool_modes, list):
705            raise ValueError('tool_modes must be a list')
706        self._tool_modes = [x for x in tool_modes if x in TOOL_MODES]
707        if not self._tool_modes:
708            raise ValueError('tool_modes not a valid list of modes')
709
710    def _get_tool_modes(self):
711        return self._tool_modes
712
713    require_active = property(_get_require_active, _set_require_active)
714    reportclass = property(_get_reportclass, _set_reportclass)
715    report_modes = property(_get_report_modes, _set_report_modes)
716    category = property(_get_category, _set_category)
717    optionclass = property(_get_optionclass, _set_optionclass)
718    toolclass = property(_get_toolclass, _set_toolclass)
719    tool_modes = property(_get_tool_modes, _set_tool_modes)
720
721    #DOCGEN attributes
722    def _set_paper(self, paper):
723        if not self._ptype == DOCGEN:
724            raise ValueError('paper may only be set for DOCGEN plugins')
725        if not isinstance(paper, bool):
726            raise ValueError('Plugin must have paper=True or False')
727        self._paper = paper
728
729    def _get_paper(self):
730        return self._paper
731
732    def _set_style(self, style):
733        if not self._ptype == DOCGEN:
734            raise ValueError('style may only be set for DOCGEN plugins')
735        if not isinstance(style, bool):
736            raise ValueError('Plugin must have style=True or False')
737        self._style = style
738
739    def _get_style(self):
740        return self._style
741
742    def _set_extension(self, extension):
743        if not (self._ptype == DOCGEN or self._ptype == EXPORT
744                or self._ptype == IMPORT):
745            raise ValueError('extension may only be set for DOCGEN/EXPORT/'\
746                              'IMPORT plugins')
747        self._extension = extension
748
749    def _get_extension(self):
750        return self._extension
751
752    paper = property(_get_paper, _set_paper)
753    style = property(_get_style, _set_style)
754    extension = property(_get_extension, _set_extension)
755
756    #QUICKREPORT attributes
757    def _set_runfunc(self, runfunc):
758        if not self._ptype == QUICKREPORT:
759            raise ValueError('runfunc may only be set for QUICKREPORT plugins')
760        self._runfunc = runfunc
761
762    def _get_runfunc(self):
763        return self._runfunc
764
765    runfunc = property(_get_runfunc, _set_runfunc)
766
767    #MAPSERVICE attributes
768    def _set_mapservice(self, mapservice):
769        if not self._ptype == MAPSERVICE:
770            raise ValueError('mapservice may only be set for MAPSERVICE plugins')
771        self._mapservice = mapservice
772
773    def _get_mapservice(self):
774        return self._mapservice
775
776    mapservice = property(_get_mapservice, _set_mapservice)
777
778    #EXPORT attributes
779    def _set_export_function(self, export_function):
780        if not self._ptype == EXPORT:
781            raise ValueError('export_function may only be set for EXPORT plugins')
782        self._export_function = export_function
783
784    def _get_export_function(self):
785        return self._export_function
786
787    def _set_export_options(self, export_options):
788        if not self._ptype == EXPORT:
789            raise ValueError('export_options may only be set for EXPORT plugins')
790        self._export_options = export_options
791
792    def _get_export_options(self):
793        return self._export_options
794
795    def _set_export_options_title(self, export_options_title):
796        if not self._ptype == EXPORT:
797            raise ValueError('export_options_title may only be set for EXPORT plugins')
798        self._export_options_title = export_options_title
799
800    def _get_export_options_title(self):
801        return self._export_options_title
802
803    export_function = property(_get_export_function, _set_export_function)
804    export_options = property(_get_export_options, _set_export_options)
805    export_options_title = property(_get_export_options_title,
806                                    _set_export_options_title)
807
808    #IMPORT attributes
809    def _set_import_function(self, import_function):
810        if not self._ptype == IMPORT:
811            raise ValueError('import_function may only be set for IMPORT plugins')
812        self._import_function = import_function
813
814    def _get_import_function(self):
815        return self._import_function
816
817    import_function = property(_get_import_function, _set_import_function)
818
819    #GRAMPLET attributes
820    def _set_gramplet(self, gramplet):
821        if not self._ptype == GRAMPLET:
822            raise ValueError('gramplet may only be set for GRAMPLET plugins')
823        self._gramplet = gramplet
824
825    def _get_gramplet(self):
826        return self._gramplet
827
828    def _set_height(self, height):
829        if not self._ptype == GRAMPLET:
830            raise ValueError('height may only be set for GRAMPLET plugins')
831        if not isinstance(height, int):
832            raise ValueError('Plugin must have height an integer')
833        self._height = height
834
835    def _get_height(self):
836        return self._height
837
838    def _set_detached_height(self, detached_height):
839        if not self._ptype == GRAMPLET:
840            raise ValueError('detached_height may only be set for GRAMPLET plugins')
841        if not isinstance(detached_height, int):
842            raise ValueError('Plugin must have detached_height an integer')
843        self._detached_height = detached_height
844
845    def _get_detached_height(self):
846        return self._detached_height
847
848    def _set_detached_width(self, detached_width):
849        if not self._ptype == GRAMPLET:
850            raise ValueError('detached_width may only be set for GRAMPLET plugins')
851        if not isinstance(detached_width, int):
852            raise ValueError('Plugin must have detached_width an integer')
853        self._detached_width = detached_width
854
855    def _get_detached_width(self):
856        return self._detached_width
857
858    def _set_expand(self, expand):
859        if not self._ptype == GRAMPLET:
860            raise ValueError('expand may only be set for GRAMPLET plugins')
861        if not isinstance(expand, bool):
862            raise ValueError('Plugin must have expand as a bool')
863        self._expand = expand
864
865    def _get_expand(self):
866        return self._expand
867
868    def _set_gramplet_title(self, gramplet_title):
869        if not self._ptype == GRAMPLET:
870            raise ValueError('gramplet_title may only be set for GRAMPLET plugins')
871        if not isinstance(gramplet_title, str):
872            raise ValueError('gramplet_title is type %s, string or unicode required' % type(gramplet_title))
873        self._gramplet_title = gramplet_title
874
875    def _get_gramplet_title(self):
876        return self._gramplet_title
877
878    def _set_help_url(self, help_url):
879        if not self._ptype == GRAMPLET:
880            raise ValueError('help_url may only be set for GRAMPLET plugins')
881        self._help_url = help_url
882
883    def _get_help_url(self):
884        return self._help_url
885
886    def _set_navtypes(self, navtypes):
887        if not self._ptype == GRAMPLET:
888            raise ValueError('navtypes may only be set for GRAMPLET plugins')
889        self._navtypes = navtypes
890
891    def _get_navtypes(self):
892        return self._navtypes
893
894    def _set_orientation(self, orientation):
895        if not self._ptype == GRAMPLET:
896            raise ValueError('orientation may only be set for GRAMPLET plugins')
897        self._orientation = orientation
898
899    def _get_orientation(self):
900        return self._orientation
901
902    gramplet = property(_get_gramplet, _set_gramplet)
903    height = property(_get_height, _set_height)
904    detached_height = property(_get_detached_height, _set_detached_height)
905    detached_width = property(_get_detached_width, _set_detached_width)
906    expand = property(_get_expand, _set_expand)
907    gramplet_title = property(_get_gramplet_title, _set_gramplet_title)
908    navtypes = property(_get_navtypes, _set_navtypes)
909    orientation = property(_get_orientation, _set_orientation)
910    help_url = property(_get_help_url, _set_help_url)
911
912    def _set_viewclass(self, viewclass):
913        if not self._ptype == VIEW:
914            raise ValueError('viewclass may only be set for VIEW plugins')
915        self._viewclass = viewclass
916
917    def _get_viewclass(self):
918        return self._viewclass
919
920    def _set_stock_icon(self, stock_icon):
921        if not self._ptype == VIEW:
922            raise ValueError('stock_icon may only be set for VIEW plugins')
923        self._stock_icon = stock_icon
924
925    def _get_stock_icon(self):
926        return self._stock_icon
927
928    viewclass = property(_get_viewclass, _set_viewclass)
929    stock_icon = property(_get_stock_icon, _set_stock_icon)
930
931    #SIDEBAR attributes
932    def _set_sidebarclass(self, sidebarclass):
933        if not self._ptype == SIDEBAR:
934            raise ValueError('sidebarclass may only be set for SIDEBAR plugins')
935        self._sidebarclass = sidebarclass
936
937    def _get_sidebarclass(self):
938        return self._sidebarclass
939
940    def _set_menu_label(self, menu_label):
941        if not self._ptype == SIDEBAR:
942            raise ValueError('menu_label may only be set for SIDEBAR plugins')
943        self._menu_label = menu_label
944
945    def _get_menu_label(self):
946        return self._menu_label
947
948    sidebarclass = property(_get_sidebarclass, _set_sidebarclass)
949    menu_label = property(_get_menu_label, _set_menu_label)
950
951    #VIEW and SIDEBAR attributes
952    def _set_order(self, order):
953        if not self._ptype in (VIEW, SIDEBAR):
954            raise ValueError('order may only be set for VIEW and SIDEBAR plugins')
955        self._order = order
956
957    def _get_order(self):
958        return self._order
959
960    order = property(_get_order, _set_order)
961
962    #DATABASE attributes
963    def _set_databaseclass(self, databaseclass):
964        if not self._ptype == DATABASE:
965            raise ValueError('databaseclass may only be set for DATABASE plugins')
966        self._databaseclass = databaseclass
967
968    def _get_databaseclass(self):
969        return self._databaseclass
970
971    def _set_reset_system(self, reset_system):
972        if not self._ptype == DATABASE:
973            raise ValueError('reset_system may only be set for DATABASE plugins')
974        self._reset_system = reset_system
975
976    def _get_reset_system(self):
977        return self._reset_system
978
979    databaseclass = property(_get_databaseclass, _set_databaseclass)
980    reset_system = property(_get_reset_system, _set_reset_system)
981
982    #GENERAL attr
983    def _set_data(self, data):
984        if not self._ptype in (GENERAL,):
985            raise ValueError('data may only be set for GENERAL plugins')
986        self._data = data
987
988    def _get_data(self):
989        return self._data
990
991    def _set_process(self, process):
992        if not self._ptype in (GENERAL,):
993            raise ValueError('process may only be set for GENERAL plugins')
994        self._process = process
995
996    def _get_process(self):
997        return self._process
998
999    data = property(_get_data, _set_data)
1000    process = property(_get_process, _set_process)
1001
1002    #RULE attr
1003    def _set_ruleclass(self, data):
1004        if self._ptype != RULE:
1005            raise ValueError('ruleclass may only be set for RULE plugins')
1006        self._ruleclass = data
1007
1008    def _get_ruleclass(self):
1009        return self._ruleclass
1010
1011    def _set_namespace(self, data):
1012        if self._ptype != RULE:
1013            raise ValueError('namespace may only be set for RULE plugins')
1014        self._namespace = data
1015
1016    def _get_namespace(self):
1017        return self._namespace
1018
1019    ruleclass = property(_get_ruleclass, _set_ruleclass)
1020    namespace = property(_get_namespace, _set_namespace)
1021
1022def newplugin():
1023    """
1024    Function to create a new plugindata object, add it to list of
1025    registered plugins
1026
1027    :returns: a newly created PluginData which is already part of the register
1028    """
1029    gpr = PluginRegister.get_instance()
1030    pgd = PluginData()
1031    gpr.add_plugindata(pgd)
1032    return pgd
1033
1034def register(ptype, **kwargs):
1035    """
1036    Convenience function to register a new plugin using a dictionary as input.
1037    The register functions will call newplugin() function, and use the
1038    dictionary kwargs to assign data to the PluginData newplugin() created,
1039    as in: plugindata.key = data
1040
1041    :param ptype: the plugin type, one of REPORT, TOOL, ...
1042    :param kwargs: dictionary with keys attributes of the plugin, and data
1043                   the value
1044    :returns: a newly created PluginData which is already part of the register
1045              and which has kwargs assigned as attributes
1046    """
1047    plg = newplugin()
1048    plg.ptype = ptype
1049    for prop in kwargs:
1050        #check it is a valid attribute with getattr
1051        getattr(plg, prop)
1052        #set the value
1053        setattr(plg, prop, kwargs[prop])
1054    return plg
1055
1056def make_environment(**kwargs):
1057    env = {
1058        'newplugin': newplugin,
1059        'register': register,
1060        'STABLE': STABLE,
1061        'UNSTABLE': UNSTABLE,
1062        'REPORT': REPORT,
1063        'QUICKREPORT': QUICKREPORT,
1064        'TOOL': TOOL,
1065        'IMPORT': IMPORT,
1066        'EXPORT': EXPORT,
1067        'DOCGEN': DOCGEN,
1068        'GENERAL': GENERAL,
1069        'RULE': RULE,
1070        'MAPSERVICE': MAPSERVICE,
1071        'VIEW': VIEW,
1072        'RELCALC': RELCALC,
1073        'GRAMPLET': GRAMPLET,
1074        'SIDEBAR': SIDEBAR,
1075        'CATEGORY_TEXT': CATEGORY_TEXT,
1076        'CATEGORY_DRAW': CATEGORY_DRAW,
1077        'CATEGORY_CODE': CATEGORY_CODE,
1078        'CATEGORY_WEB': CATEGORY_WEB,
1079        'CATEGORY_BOOK': CATEGORY_BOOK,
1080        'CATEGORY_GRAPHVIZ': CATEGORY_GRAPHVIZ,
1081        'CATEGORY_TREE': CATEGORY_TREE,
1082        'TOOL_DEBUG': TOOL_DEBUG,
1083        'TOOL_ANAL': TOOL_ANAL,
1084        'TOOL_DBPROC': TOOL_DBPROC,
1085        'TOOL_DBFIX': TOOL_DBFIX,
1086        'TOOL_REVCTL': TOOL_REVCTL,
1087        'TOOL_UTILS': TOOL_UTILS,
1088        'CATEGORY_QR_MISC': CATEGORY_QR_MISC,
1089        'CATEGORY_QR_PERSON': CATEGORY_QR_PERSON,
1090        'CATEGORY_QR_FAMILY': CATEGORY_QR_FAMILY,
1091        'CATEGORY_QR_EVENT': CATEGORY_QR_EVENT,
1092        'CATEGORY_QR_SOURCE': CATEGORY_QR_SOURCE,
1093        'CATEGORY_QR_CITATION': CATEGORY_QR_CITATION,
1094        'CATEGORY_QR_SOURCE_OR_CITATION': CATEGORY_QR_SOURCE_OR_CITATION,
1095        'CATEGORY_QR_PLACE': CATEGORY_QR_PLACE,
1096        'CATEGORY_QR_MEDIA': CATEGORY_QR_MEDIA,
1097        'CATEGORY_QR_REPOSITORY': CATEGORY_QR_REPOSITORY,
1098        'CATEGORY_QR_NOTE': CATEGORY_QR_NOTE,
1099        'CATEGORY_QR_DATE': CATEGORY_QR_DATE,
1100        'REPORT_MODE_GUI': REPORT_MODE_GUI,
1101        'REPORT_MODE_BKI': REPORT_MODE_BKI,
1102        'REPORT_MODE_CLI': REPORT_MODE_CLI,
1103        'TOOL_MODE_GUI': TOOL_MODE_GUI,
1104        'TOOL_MODE_CLI': TOOL_MODE_CLI,
1105        'DATABASE': DATABASE,
1106        'GRAMPSVERSION': GRAMPSVERSION,
1107        'START': START,
1108        'END': END,
1109        'IMAGE_DIR': IMAGE_DIR,
1110        }
1111    env.update(kwargs)
1112    return env
1113
1114#-------------------------------------------------------------------------
1115#
1116# PluginRegister
1117#
1118#-------------------------------------------------------------------------
1119class PluginRegister:
1120    """
1121    PluginRegister is a Singleton which holds plugin data
1122
1123    .. attribute : stable_only
1124        Bool, include stable plugins only or not. Default True
1125    """
1126    __instance = None
1127
1128    def get_instance():
1129        """ Use this function to get the instance of the PluginRegister """
1130        if PluginRegister.__instance is None:
1131            PluginRegister.__instance = 1 # Set to 1 for __init__()
1132            PluginRegister.__instance = PluginRegister()
1133        return PluginRegister.__instance
1134    get_instance = staticmethod(get_instance)
1135
1136    def __init__(self):
1137        """ This function should only be run once by get_instance() """
1138        if PluginRegister.__instance != 1:
1139            raise Exception("This class is a singleton. "
1140                            "Use the get_instance() method")
1141        self.stable_only = True
1142        if __debug__:
1143            self.stable_only = False
1144        self.__plugindata = []
1145        self.__id_to_pdata = {}
1146
1147    def add_plugindata(self, plugindata):
1148        """ This is used to add an entry to the registration list.  The way it
1149        is used, this entry is not yet filled in, so we cannot use the id to
1150        add to the __id_to_pdata dict at this time. """
1151        self.__plugindata.append(plugindata)
1152
1153
1154    def scan_dir(self, dir, filenames, uistate=None):
1155        """
1156        The dir name will be scanned for plugin registration code, which will
1157        be loaded in :class:`PluginData` objects if they satisfy some checks.
1158
1159        :returns: A list with :class:`PluginData` objects
1160        """
1161        # if the directory does not exist, do nothing
1162        if not (os.path.isdir(dir) or os.path.islink(dir)):
1163            return []
1164
1165        ext = r".gpr.py"
1166        extlen = -len(ext)
1167        pymod = re.compile(r"^(.*)\.py$")
1168
1169        for filename in filenames:
1170            if not filename[extlen:] == ext:
1171                continue
1172            lenpd = len(self.__plugindata)
1173            full_filename = os.path.join(dir, filename)
1174            try:
1175                with open(full_filename, "r", encoding='utf-8') as fd:
1176                    stream = fd.read()
1177            except Exception as msg:
1178                print(_('ERROR: Failed reading plugin registration %(filename)s') % \
1179                            {'filename' : filename})
1180                print(msg)
1181                continue
1182            if os.path.exists(os.path.join(os.path.dirname(full_filename),
1183                                           'locale')):
1184                try:
1185                    local_gettext = glocale.get_addon_translator(full_filename).gettext
1186                except ValueError:
1187                    print(_('WARNING: Plugin %(plugin_name)s has no translation'
1188                            ' for any of your configured languages, using US'
1189                            ' English instead') %
1190                          {'plugin_name' : filename.split('.')[0] })
1191                    local_gettext = glocale.translation.gettext
1192            else:
1193                local_gettext = glocale.translation.gettext
1194            try:
1195                exec (compile(stream, filename, 'exec'),
1196                      make_environment(_=local_gettext), {'uistate': uistate})
1197                for pdata in self.__plugindata[lenpd:]:
1198                    # should not be duplicate IDs in different plugins
1199                    assert pdata.id not in self.__id_to_pdata
1200                    # if pdata.id in self.__id_to_pdata:
1201                    #     print("Error: %s is duplicated!" % pdata.id)
1202                    self.__id_to_pdata[pdata.id] = pdata
1203            except ValueError as msg:
1204                print(_('ERROR: Failed reading plugin registration %(filename)s') % \
1205                            {'filename' : filename})
1206                print(msg)
1207                self.__plugindata = self.__plugindata[:lenpd]
1208            except:
1209                print(_('ERROR: Failed reading plugin registration %(filename)s') % \
1210                            {'filename' : filename})
1211                print("".join(traceback.format_exception(*sys.exc_info())))
1212                self.__plugindata = self.__plugindata[:lenpd]
1213            #check if:
1214            #  1. plugin exists, if not remove, otherwise set module name
1215            #  2. plugin not stable, if stable_only=True, remove
1216            #  3. TOOL_DEBUG only if __debug__ True
1217            rmlist = []
1218            ind = lenpd-1
1219            for plugin in self.__plugindata[lenpd:]:
1220                #LOG.warning("\nPlugin scanned %s at registration", plugin.id)
1221                ind += 1
1222                plugin.directory = dir
1223                if not valid_plugin_version(plugin.gramps_target_version):
1224                    print(_('ERROR: Plugin file %(filename)s has a version of '
1225                            '"%(gramps_target_version)s" which is invalid for Gramps '
1226                            '"%(gramps_version)s".' %
1227                            {'filename': os.path.join(dir, plugin.fname),
1228                             'gramps_version': GRAMPSVERSION,
1229                             'gramps_target_version': plugin.gramps_target_version,}
1230                            ))
1231                    rmlist.append(ind)
1232                    continue
1233                if not plugin.status == STABLE and self.stable_only:
1234                    rmlist.append(ind)
1235                    continue
1236                if plugin.ptype == TOOL and plugin.category == TOOL_DEBUG \
1237                and not __debug__:
1238                    rmlist.append(ind)
1239                    continue
1240                if plugin.fname is None:
1241                    continue
1242                match = pymod.match(plugin.fname)
1243                if not match:
1244                    rmlist.append(ind)
1245                    print(_('ERROR: Wrong python file %(filename)s in register file '
1246                            '%(regfile)s')  % {
1247                               'filename': os.path.join(dir, plugin.fname),
1248                               'regfile': os.path.join(dir, filename)
1249                            })
1250                    continue
1251                if not os.path.isfile(os.path.join(dir, plugin.fname)):
1252                    rmlist.append(ind)
1253                    print(_('ERROR: Python file %(filename)s in register file '
1254                            '%(regfile)s does not exist')  % {
1255                               'filename': os.path.join(dir, plugin.fname),
1256                               'regfile': os.path.join(dir, filename)
1257                            })
1258                    continue
1259                module = match.groups()[0]
1260                plugin.mod_name = module
1261                plugin.fpath = dir
1262                #LOG.warning("\nPlugin added %s at registration", plugin.id)
1263            rmlist.reverse()
1264            for ind in rmlist:
1265                del self.__id_to_pdata[self.__plugindata[ind].id]
1266                del self.__plugindata[ind]
1267
1268    def get_plugin(self, id):
1269        """
1270        Return the :class:`PluginData` for the plugin with id
1271        """
1272        assert(len(self.__id_to_pdata) == len(self.__plugindata))
1273        # if len(self.__id_to_pdata) != len(self.__plugindata):
1274        #     print(len(self.__id_to_pdata), len(self.__plugindata))
1275        return self.__id_to_pdata.get(id, None)
1276
1277    def type_plugins(self, ptype):
1278        """
1279        Return a list of :class:`PluginData` that are of type ptype
1280        """
1281        return [x for x in self.__plugindata if x.ptype == ptype]
1282
1283    def report_plugins(self, gui=True):
1284        """
1285        Return a list of gui or cli :class:`PluginData` that are of type REPORT
1286
1287        :param gui: bool, if True then gui plugin, otherwise cli plugin
1288        """
1289        if gui:
1290            return [x for x in self.type_plugins(REPORT) if REPORT_MODE_GUI
1291                                        in x.report_modes]
1292        else:
1293            return [x for x in self.type_plugins(REPORT) if REPORT_MODE_CLI
1294                                        in x.report_modes]
1295
1296    def tool_plugins(self, gui=True):
1297        """
1298        Return a list of :class:`PluginData` that are of type TOOL
1299        """
1300        if gui:
1301            return [x for x in self.type_plugins(TOOL) if TOOL_MODE_GUI
1302                                        in x.tool_modes]
1303        else:
1304            return [x for x in self.type_plugins(TOOL) if TOOL_MODE_CLI
1305                                        in x.tool_modes]
1306
1307
1308    def bookitem_plugins(self):
1309        """
1310        Return a list of REPORT :class:`PluginData` that are can be used as
1311        bookitem
1312        """
1313        return [x for x in self.type_plugins(REPORT) if REPORT_MODE_BKI
1314                                        in x.report_modes]
1315
1316    def quickreport_plugins(self):
1317        """
1318        Return a list of :class:`PluginData` that are of type QUICKREPORT
1319        """
1320        return self.type_plugins(QUICKREPORT)
1321
1322    def import_plugins(self):
1323        """
1324        Return a list of :class:`PluginData` that are of type IMPORT
1325        """
1326        return self.type_plugins(IMPORT)
1327
1328    def export_plugins(self):
1329        """
1330        Return a list of :class:`PluginData` that are of type EXPORT
1331        """
1332        return self.type_plugins(EXPORT)
1333
1334    def docgen_plugins(self):
1335        """
1336        Return a list of :class:`PluginData` that are of type DOCGEN
1337        """
1338        return self.type_plugins(DOCGEN)
1339
1340    def general_plugins(self, category=None):
1341        """
1342        Return a list of :class:`PluginData` that are of type GENERAL
1343        """
1344        plugins = self.type_plugins(GENERAL)
1345        if category:
1346            return [plugin for plugin in plugins
1347                    if plugin.category == category]
1348        return plugins
1349
1350    def mapservice_plugins(self):
1351        """
1352        Return a list of :class:`PluginData` that are of type MAPSERVICE
1353        """
1354        return self.type_plugins(MAPSERVICE)
1355
1356    def view_plugins(self):
1357        """
1358        Return a list of :class:`PluginData` that are of type VIEW
1359        """
1360        return self.type_plugins(VIEW)
1361
1362    def relcalc_plugins(self):
1363        """
1364        Return a list of :class:`PluginData` that are of type RELCALC
1365        """
1366        return self.type_plugins(RELCALC)
1367
1368    def gramplet_plugins(self):
1369        """
1370        Return a list of :class:`PluginData` that are of type GRAMPLET
1371        """
1372        return self.type_plugins(GRAMPLET)
1373
1374    def sidebar_plugins(self):
1375        """
1376        Return a list of :class:`PluginData` that are of type SIDEBAR
1377        """
1378        return self.type_plugins(SIDEBAR)
1379
1380    def database_plugins(self):
1381        """
1382        Return a list of :class:`PluginData` that are of type DATABASE
1383        """
1384        return self.type_plugins(DATABASE)
1385
1386    def rule_plugins(self):
1387        """
1388        Return a list of :class:`PluginData` that are of type RULE
1389        """
1390        return self.type_plugins(RULE)
1391
1392    def filter_load_on_reg(self):
1393        """
1394        Return a list of :class:`PluginData` that have load_on_reg == True
1395        """
1396        return [x for x in self.__plugindata if x.load_on_reg == True]
1397