root/branch/NetworkInventory/umitPlugin/Tree.py @ 3878

Revision 3878, 21.6 kB (checked in by gpolo, 4 years ago)

Merged revisions 3858,3876-3877 via svnmerge from
http://svn.umitproject.org/svnroot/umit/trunk

........

r3858 | nopper | 2008-12-21 10:52:40 -0200 (Sun, 21 Dec 2008) | 1 line


Fixing horror

........

r3876 | getxsick | 2008-12-25 19:31:37 -0200 (Thu, 25 Dec 2008) | 1 line


#191: psyco is off if UMIT_DEVELOPMENT was set

........

r3877 | getxsick | 2008-12-25 20:08:50 -0200 (Thu, 25 Dec 2008) | 1 line


Added missing import of I18N

........

Line 
1# -*- coding: utf-8 -*-
2# Copyright (C) 2008 Adriano Monteiro Marques
3#
4# Author: Francesco Piccinno <stack.box@gmail.com>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
20import os
21import sys
22
23sys.plugins_path = []
24
25from umitCore.I18N import _
26from umitCore.Paths import Path
27from umitCore.UmitLogging import log
28from umitPlugin.Atoms import Version, DepDict, generate_traceback
29from umitPlugin.Core import Core
30
31from distutils.sysconfig import get_config_vars
32
33import __builtin__
34original_import = __builtin__.__import__
35
36def hook_import(lib, globals=None, locals=None, fromlist=None, level=-1):
37    try:
38        return original_import(lib, globals, locals, fromlist, level)
39    except ImportError, err:
40       
41        # This is a hacky fallback to permit loading of modules
42        # with .so or .dll suffix inside .ump files
43       
44        # The .so/.dll libraries are extracted in a temp directory
45        # that is cleaned on UMIT exit/start
46       
47        for reader in sys.plugins_path:
48            path = os.path.join(Path.config_dir, "plugins-temp", \
49                                lib.split('.')[-1] + get_config_vars("SO")[0])
50
51            try:
52                out = open(path, 'wb')
53               
54                out.write(reader.file.read( \
55                    'lib/%s%s' % (lib.replace(".", "/"), get_config_vars("SO")[0])) \
56                )
57                out.close()
58            except Exception, exc:
59                continue
60           
61            path = os.path.dirname(out.name)
62            name = os.path.basename(out.name)
63           
64            try:
65                sys.path.insert(0, path)
66               
67                return original_import(name.replace(get_config_vars("SO")[0], ""), \
68                                         level=0)
69            except Exception, exc:
70                continue
71            finally:
72                sys.path.pop(0)
73
74        raise err
75
76class Package(object):
77    """
78    It represents a Plugin object (used *ONLY FOR TESTING* purpose)
79    """
80
81    def __init__(self, name, n, p, c):
82        self.name = name
83        self.needs = n
84        self.provides = p
85        self.conflicts = c
86
87    def __repr__(self):
88        return self.name
89
90class PluginException(Exception):
91    def __init__(self, txt, summ):
92        Exception.__init__(self, _("Unable to load %s") % txt)
93        self.summary = summ
94
95class PluginsTree(object):
96    """
97    Manages and tracks the loads/unloads of plugins objects
98    """
99
100    def __init__(self):
101        """
102        Create a new PluginsTree
103
104        @param parent a PluginEngine instance
105        """
106
107        # A dict to trace plugin instances
108        self.instances = {}
109        self.modules = {}
110
111        self.who_conflicts, \
112            self.who_provides, \
113            self.who_needs = DepDict(), DepDict(), DepDict()
114        self.pkg_lst = list()
115
116    def dump(self):
117        log.info(">>> dump(): conflicts/provides/needs: %d / %d / %d" % \
118              (
119                  len(self.who_conflicts),
120                  len(self.who_provides),
121                  len(self.who_needs)
122              )
123        )
124
125    def check_conflicts(self, pkg):
126        """
127        Check the conflicts between pkg and global list of provides or
128        loaded plugins.
129
130        The return list contains entry like:
131            (phase-number:int,
132                (p_name:str, p_op:Operator, p_ver:Version) -> Provider
133                (c_name:str, c_op:Operator, c_ver:Version) -> Conflicter
134            )
135
136        @param pkg the plugin to check
137        @return [] if everything is OK or a list of plugins
138                conflicting with pkg
139        """
140
141        lst = []
142
143        #
144        # 1) We should check the cache conflicts agains pkg
145
146        for provide in pkg.provides:
147            try:
148                name, op, ver = Version.extract_version(provide)
149
150                if not name in self.who_conflicts:
151                    continue
152
153                for c_op, c_ver, c_pkg in self.who_conflicts[name]:
154
155                    # We could have
156                    # c_op, c_ver >=, 2.0.0
157                    # ver = 3.0.0
158
159                    # We could have a None if a conflict entry didn't provide
160                    # any version or operator
161
162                    if c_op.op_str == '.' or c_op(ver, c_ver) is True:
163                        lst.append(
164                            (1,
165                             (name, op, ver),   # Provider
166                             (c_pkg, c_op, c_ver)       # Conflicter
167                             )
168                        )
169            except Exception, err:
170                log.error(err)
171                log.error("1) Ignoring conflict entry ...")
172
173                continue
174
175        #
176        # 2) We should check the package conflicts against cache provides
177
178        for conflict in pkg.conflicts:
179            try:
180                name, op, ver = Version.extract_version(conflict)
181
182                if not name in self.who_provides:
183                    continue
184
185                # Only '=' operator presents:
186                # providers['woot'] = [('=', '2.0', pkg1), ('=', '2.1', pkg2), ..]
187
188                for p_op, p_ver, p_pkg in self.who_provides[name]:
189                    # for example we could have
190                    # name, op, ver = dummy, >=, 2.0.0
191                    # p_ver = 2.0.3
192
193                    # strict checking for avoid false results
194                    # if None was returned
195                    if op(p_ver, ver) is True:
196                        # So we have a conflict. Just add to the list
197                        lst.append(
198                            (2,
199                             (p_pkg, p_op, p_ver),   # Provider
200                             (name, op, ver)    # Conflicter
201                             )
202                        )
203
204            except Exception, err:
205                log.error(err)
206                log.error("2) Ignoring conflict entry ...")
207
208                continue
209
210        return lst
211
212    def check_needs(self, pkg):
213        """
214        Check the needs between pkg and global list of provides.
215
216        The return list contains entry like:
217            (name:str, op:Operator, ver:Version) -> need not found
218
219        @param pkg the plugin to check
220        @return [] if everything is OK or a list of not resolved needs
221        """
222
223        # We have passed the conflict stage
224        # so check for needs
225
226        lst = []
227
228        for need in pkg.needs:
229            try:
230                found = False
231                name, op, ver = Version.extract_version(need)
232
233                if not name in self.who_provides:
234                    lst.append((name, op, ver))
235                    continue
236
237                for n_op, n_ver, n_pkg in self.who_provides[name]:
238                    # for example we could have
239                    # name, op, ver = dummy, >=, 2.0.0
240                    # n_ver = 2.0.3
241
242                    # TODO: check me
243                    if op(n_ver, ver) is True or op(n_ver, ver) is None:
244                        found = True
245                        break
246
247                # If we are here not applicable deps were found so add
248                if not found:
249                    lst.append((name, op, ver))
250
251            except Exception, err:
252                log.error(err)
253                log.error("Ignoring need entry ...")
254
255                continue
256
257        return lst
258
259    def add_plugin_to_cache(self, pkg):
260        """
261        Exports the needs/provides/conflicts to the global dicts
262        """
263
264        for attr in ('conflicts', 'provides', 'needs'):
265            for dep in getattr(pkg, attr, []):
266                try:
267                    name, op, ver = Version.extract_version(dep)
268                    d = getattr(self, "who_%s" % attr)
269                    d[name] = (op, ver, pkg)
270                except Exception, err:
271                    log.warning(err)
272                    log.warning("Ignoring %s entry" % dep)
273
274        # Adds plugin to global list
275        self.pkg_lst.append(pkg)
276
277    def load_plugin(self, pkg, force=False):
278        """
279        Load a plugin
280
281        @param pkg the plugin to load
282        @param force if True no checks for deps will performed
283        @return None or raise a PluginException
284        """
285
286        if pkg in self.pkg_lst:
287            raise PluginException(pkg, _("Plugin already loaded."))
288
289        if not force:
290
291            # 1) Check for conflicts
292            ret = self.check_conflicts(pkg)
293
294            if ret:
295                reasons = []
296                for phase, (p_name, p_op, p_ver), (c_name, c_op, c_ver) in ret:
297                    reasons.append( \
298                        "-%d- Plugin '%s' provides %s which conflicts with " \
299                        "%s conflict entry for plugin '%s'" % \
300                        ( \
301                            phase, pkg, \
302                            Version.stringify_version(p_name, p_op, p_ver), \
303                            Version.stringify_version(p_name, c_op, c_ver), \
304                            c_name \
305                        ) \
306                    )
307
308                raise PluginException(pkg, "\n".join(reasons))
309
310
311            # 2) Check for needs
312            ret = self.check_needs(pkg)
313
314            if ret:
315                reasons = []
316                for (n_name, n_op, n_ver) in ret:
317                    reasons.append( \
318                        "-3- Plugin '%s' needs %s, which actually is not " \
319                        "provided by any plugin." % \
320                        ( \
321                            pkg, \
322                            Version.stringify_version(n_name, n_op, n_ver) \
323                        ) \
324                    )
325
326                raise PluginException(pkg, "\n".join(reasons))
327
328        self.__load_hook(pkg)
329
330        # Export to cache
331        self.add_plugin_to_cache(pkg)
332        self.dump()
333
334    def check_list_needs(self, pkg):
335        """
336        Check the needs from list against pkg
337
338        @param pkg the plugin to check
339        @return [] if everything is OK or a list of not used needs (name, op, version)
340        """
341
342        # Clone the list and drops out pkg
343        lst = self.pkg_lst[:]
344        lst.remove(pkg)
345
346        conflicts_lst = []
347
348        for provide in pkg.provides:
349            p_name, p_op, p_ver = Version.extract_version(provide)
350
351            for n_pkg in lst:
352                for need in n_pkg.needs:
353                    n_name, n_op, n_ver = Version.extract_version(need)
354
355                    if n_name != p_name:
356                        continue
357
358                    # It's our need and we have a dep! :D
359                    if n_op(p_ver, n_ver) is True:
360                        conflicts_lst.append((n_pkg, n_op, n_ver))
361
362        return conflicts_lst
363
364    def remove_plugin_from_cache(self, pkg):
365        for attr in ('conflicts', 'provides', 'needs'):
366            for dep in getattr(pkg, attr, []):
367                try:
368                    name, op, ver = Version.extract_version(dep)
369                    d = getattr(self, "who_%s" % attr)
370
371                    # It's more probably that the entry is in the last
372
373                    for i in xrange(len(d[name]) - 1, -1, -1):
374                        _op, _ver, _pkg = d[name][i]
375
376                        if pkg == _pkg and \
377                           ver == _ver and \
378                           op == _op:
379                            del d[name][i]
380
381                    # Remove unused keys.
382                    if not d[name]:
383                        del d[name]
384
385                except Exception, err:
386                    log.warning(err)
387                    log.warning("Ignoring %s entry" % dep)
388
389        # Remove plugin to global list
390        self.pkg_lst.remove(pkg)
391
392    def unload_plugin(self, pkg, force=False):
393        """
394        Unload a plugin
395        @param pkg the plugin to unload
396        @param force if False check the depends
397
398        @return None or raise a PluginException
399        """
400
401        if pkg not in self.pkg_lst:
402            raise Exception("Plugin not loaded.")
403
404        if not force:
405
406            # No conflict check here.
407            # 1) We should check need's list against pkg
408
409            ret = self.check_list_needs(pkg)
410
411            if ret:
412                reasons = []
413
414                for i in ret:
415                    reasons.append(
416                        "Plugin %s requires %s" % \
417                        (i[0], Version.stringify_version(pkg, i[1], i[2]))
418                    )
419
420                raise PluginException(pkg, "\n".join(reasons))
421
422        self.__unload_hook(pkg)
423
424        self.remove_plugin_from_cache(pkg)
425        self.dump()
426
427    def __cache_import(self, pkg):
428        """
429        Use try/except with this
430        """
431
432        if pkg in self.modules:
433            return self.modules[pkg]
434        else:
435           
436            try:
437                __builtin__.__import__ = hook_import
438                module = hook_import(pkg.start_file, level=0)
439            finally:
440                __builtin__.__import__ = original_import
441           
442            self.modules[pkg] = module
443
444            return module
445
446    def load_directory(self, modpath):
447        if not os.environ.get('UMIT_DEVELOPMENT', False):
448            log.error("This method should not be called in release.")
449            return
450
451        start_file = 'main'
452
453        log.warning("You are loading a plugin without checking for needs,provides,conflitcts")
454        log.warning("* You have been warned! *")
455
456        log.warning("Assuming `%s' as start file!" % start_file)
457
458        # Load the plugin
459        sys.path.insert(0, os.path.abspath(modpath))
460
461        if start_file in sys.modules:
462            sys.modules.pop(start_file)
463
464        try:
465            __builtin__.__import__ = hook_import
466            module = hook_import(start_file, level=0)
467
468            if hasattr(module, "__plugins__") and \
469               isinstance(module.__plugins__, list):
470                lst = module.__plugins__
471                ret = []
472
473                for plug in lst:
474                    try:
475                        inst = plug()
476                        inst.start(None)
477
478                        ret.append(inst)
479                    except Exception, err:
480                        log.critical("Error while starting %s:" % (plug))
481                        log.critical(generate_traceback())
482                        log.critical("Ignoring instance.")
483
484                if not ret:
485                    log.error("Not startable plugin defined in main file")
486            else:
487                log.error("No Plugin subclass")
488        finally:
489            __builtin__.__import__ = original_import
490
491
492    def __load_hook(self, pkg):
493        """
494        This is the real load procedure of plugin.
495        We'll use zipmodule to import and a global function expose
496        to provide a simple method to access files inside the zip file
497        to plugin.
498
499        Raise a PluginException on fail
500
501        @return None or raise a PluginException
502        """
503
504        if pkg in self.instances:
505            raise PluginException(pkg, "Already present")
506       
507        # We need to get the start-file field from pkg and then try
508        # to import it
509       
510        modpath = os.path.join(pkg.get_path(), 'lib')
511        sys.path.insert(0, os.path.abspath(modpath))
512
513        # This were removed
514        fname = os.path.join(pkg.get_path(), 'bin', pkg.start_file)
515        sys.path.insert(0, os.path.abspath(os.path.dirname(fname)))
516       
517        if pkg.start_file in sys.modules:
518            sys.modules.pop(pkg.start_file)
519
520        try:
521            # We add to modules to avoid deleting and stop working plugin ;)
522            sys.plugins_path.insert(0, pkg)
523            module = self.__cache_import(pkg)
524       
525        except Exception, err:
526            sys.plugins_path.pop(0)
527            raise PluginException(pkg, str(err))
528       
529        finally:
530            # Check that
531            sys.path.pop(0)
532
533        if hasattr(module, "__plugins__") and \
534           isinstance(module.__plugins__, list):
535            lst = module.__plugins__
536            ret = []
537
538            for plug in lst:
539                try:
540                    inst = plug()
541                    inst.start(pkg)
542
543                    ret.append(inst)
544                except Exception, err:
545                    log.critical("Error while starting %s from %s:" % (plug, pkg))
546                    log.critical(err)
547                    log.critical("Ignoring instance.")
548
549            if not ret:
550                raise PluginException(pkg, \
551                              "No startablePlugin subclass in %s" % pkg)
552
553            self.instances[pkg] = ret
554        else:
555            raise PluginException(pkg, "No Plugin subclass in %s" % pkg)
556
557    def __unload_hook(self, pkg):
558        """
559        This is the real unload procedure of plugin.
560        Raise a PluginException on fail
561
562        @return None or raise a PluginException
563        """
564
565        if not pkg in self.instances:
566            raise PluginException(pkg, "Already unloaded")
567
568        for inst in self.instances[pkg]:
569            try:
570                inst.stop()
571            except Exception, err:
572                log.critical("Error while stopping %s from %s:" % (inst, pkg))
573                log.critical(generate_traceback())
574                log.critical("Ignoring instance.")
575       
576        try:
577            sys.plugins_path.remove(pkg)
578        except Exception:
579            pass
580       
581        del self.instances[pkg]
582
583
584    #
585    # Usefull functions
586    #
587
588    def show_preferences(self, pkg):
589        """
590        Show the preference dialog if it's setted
591
592        We use the field __pref_func__
593
594        @param pkg a PluginReader
595        @return True if it's defined and launched or
596                False if it's not defined
597        """
598
599        pref_func = None
600
601        try:
602            module = self.__cache_import(pkg)
603            pref_func = module.__pref_func__
604        except Exception:
605            pass
606
607        # Instead using types
608        def t(): pass
609       
610        if isinstance(pref_func, type(t)):
611            pref_func()
612            return True
613
614        return False
615
616    def show_about(self, pkg):
617        """
618        Show an about dialog for plugin.
619
620        We use a field __about__
621        """
622
623        about_func = None
624       
625        try:
626            module = self.__cache_import(pkg)
627            about_func = module.__about__
628        except Exception:
629            pass
630       
631        # Instead using types
632        def t(): pass
633       
634        if isinstance(about_func, type(t)):
635            about_func()
636        else:
637            d = Core().about_dialog(pkg)
638           
639            d.run()
640            d.hide()
641            d.destroy()
642
643    def get_provide(self, need, lst_need, need_module=False):
644        """
645        Get the requested package (used by Core)
646
647        @param need is the dependency name
648        @lst_need a list of (op, ver, name) that is the requirement for need
649        """
650
651        if not need in self.who_provides:
652            return None
653
654        ret = [(ver, pkg) for op, ver, pkg in self.who_provides[need]]
655
656        # 1) create the list
657        for p_op, p_ver, p_pkg in self.who_provides[need]:
658            # p_op, p_ver, p_pkg = =, 2.0, dummy
659
660            for op, ver, name in lst_need:
661                # op, ver, name = >, 1.0, dummy
662
663                # TODO: check that
664                if not op(p_ver, ver) and (p_ver, p_pkg) in ret:
665                    ret.remove((p_ver, p_pkg))
666
667        # 2) Choose the better version (major)
668        def compare(x, y):
669            return x[0].__cmp__(y[0])
670
671        ret.sort(compare)
672
673        if not ret:
674            return None
675        else:
676            if need_module:
677                return self.modules[ret[0][1]]
678            else:
679                return self.instances[ret[0][1]]
680
681def _test():
682    """
683    >>> 1
684    1
685    >>> dummy = Package('dummy', [], ['dummy-2.1'], ['woot'])
686    >>> woot = Package('woot', ['>dummy-2.0'], ['=woot-2.0.0'], [])
687    >>> conflicter = Package('conflicter', [], [], ['!dummy-2.0'])
688    >>> tree = PluginsTree()
689
690    >>> tree.load_plugin(conflicter)
691    Succesfully loaded conflicter ;-)
692    c/p/n: 1 / 0 / 0
693    True
694
695    >>> tree.load_plugin(dummy)
696    -1- Plugin 'dummy' provides =dummy-2.1.0 which conflicts with !dummy-2.0.0 conflict entry for plugin 'conflicter'
697    Unable to load dummy :(
698    False
699
700    >>> tree.load_plugin(Package('dummy', [], ['dummy-2.0'], []))
701    Succesfully loaded dummy ;-)
702    c/p/n: 1 / 1 / 0
703    True
704
705    >>> tree.unload_plugin(conflicter)
706    Succesfully unloaded conflicter
707    c/p/n: 0 / 1 / 0
708    True
709
710    >>> tree.load_plugin(woot)
711    -3- Plugin 'woot' needs >dummy-2.0.0, which actually is not provided by any plugin.
712    Unable to load woot :(
713    False
714
715    >>> tree.load_plugin(dummy)
716    Succesfully loaded dummy ;-)
717    c/p/n: 1 / 1 / 0
718    True
719
720    >>> tree.load_plugin(woot)
721    -1- Plugin 'woot' provides =woot-2.0.0 which conflicts with woot-* conflict entry for plugin 'dummy'
722    Unable to load woot :(
723    False
724
725    >>> tree.unload_plugin(dummy)
726    Succesfully unloaded dummy
727    c/p/n: 0 / 1 / 0
728    True
729
730    >>> tree.load_plugin(Package('woot', ['>=dummy-2.0'], [], []))
731    Succesfully loaded woot ;-)
732    c/p/n: 0 / 1 / 1
733    True
734
735    """
736
737    import doctest
738    doctest.testmod()
739
740if __name__ == "__main__":
741    _test()
Note: See TracBrowser for help on using the browser.