root/branch/QuickScan/umit/plugin/Containers.py @ 4946

Revision 4946, 18.4 kB (checked in by cassiano, 4 years ago)

Merged revisions 4909,4930-4932,4934-4935 via svnmerge from
http://svn.umitproject.org/svnroot/umit/trunk

................

r4909 | nopper | 2009-06-22 11:55:24 -0300 (Mon, 22 Jun 2009) | 21 lines


Merged revisions 4781-4783,4908 via svnmerge from
http://svn.umitproject.org/svnroot/umit/branch/UmitPlugins


........

r4781 | nopper | 2009-05-08 21:26:32 +0200 (ven, 08 mag 2009) | 1 line


Switching to new schema see UmitPlugins.xsd in PM branch. Switched also from xml.minidom to sax to speed up things.

........

r4782 | nopper | 2009-05-09 16:11:38 +0200 (sab, 09 mag 2009) | 1 line


Switched update process to new xml schema file.

........

r4783 | nopper | 2009-05-09 17:02:06 +0200 (sab, 09 mag 2009) | 1 line


Updating documentation to the new schema

........

r4908 | nopper | 2009-06-22 16:43:59 +0200 (lun, 22 giu 2009) | 1 line


Fixing a typo

........

................

r4930 | luis | 2009-06-27 11:33:59 -0300 (Sat, 27 Jun 2009) | 1 line


Fixing #334 - import error when umit is installed from package

................

r4931 | luis | 2009-06-27 22:38:54 -0300 (Sat, 27 Jun 2009) | 1 line


Fixed #331 - Permission denied writing umit config file

................

r4932 | luis | 2009-06-27 23:09:05 -0300 (Sat, 27 Jun 2009) | 1 line


Typo in Flow Analyzer plugin

................

r4934 | luis | 2009-06-27 23:30:31 -0300 (Sat, 27 Jun 2009) | 3 lines


Initialized merge tracking via "svnmerge" with revisions "1-4699" from
http://svn.umitproject.org/svnroot/umit/branch/radialnet

................

r4935 | ignotus | 2009-06-27 23:54:28 -0300 (Sat, 27 Jun 2009) | 3 lines


Fix accents problem in header.

................

Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Copyright (C) 2008 Adriano Monteiro Marques
4#
5# Author: Francesco Piccinno <stack.box@gmail.com>
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
21import os
22import os.path
23import sys
24
25from fnmatch import fnmatch
26from zipfile import ZipFile, BadZipfile, ZIP_DEFLATED
27
28from StringIO import StringIO
29
30from xml.sax import handler, make_parser
31from xml.sax.saxutils import XMLGenerator
32from xml.sax.xmlreader import AttributesImpl
33
34from tempfile import mktemp
35
36from umit.plugin.Parser import Parser
37from umit.plugin.Atoms import StringFile
38
39from umit.core.Paths import Path
40from umit.core.UmitLogging import log
41
42# For setup functionality
43from distutils.dist import Distribution
44
45from distutils.core import setup as dist_setup
46from distutils.core import setup_keywords, extension_keywords
47
48from distutils.command.install import install as installcmd
49from distutils.command.install_lib import install_lib as install_libcmd
50
51try:
52    from distutils.command.install_egg_info import install_egg_info as install_egginfocmd
53
54    class PlugEggInstaller(install_egginfocmd):
55        def run(self):
56            pass
57
58except ImportError:
59    pass
60
61# For removing directory trees we need
62import shutil
63
64SIGNATURE = "UmitPlugin"
65
66class ManifestObject(object):
67    def __init__(self):
68
69        # Ok here we're using list object because py2.5 seems to not support
70        # index() for tuple. Stupid 2.5 :)
71
72        self.elements = [
73            ['name', 'version', 'description', 'url'],
74            ['start_file', 'update'],
75            ['provide', 'need', 'conflict'],
76            ['license', 'copyright', 'author',
77             'contributor', 'translator', 'artist']
78        ]
79
80        self.containers = [SIGNATURE, 'runtime', 'deptree', 'credits']
81
82        self.name = ''
83        self.version = ''
84        self.description = ''
85        self.url = ''
86
87        self.start_file = ''
88        self.update = []
89
90        self.provide = []
91        self.need = []
92        self.conflict = []
93
94        self.license = []
95        self.copyright = []
96        self.author = []
97        self.contributor = []
98        self.translator = []
99        self.artist = []
100
101        self.attr_type = ''
102
103    def check_validity(self, use_print=False):
104        """
105        Checks the fields presents and validity
106
107        @return True if it's ok
108        """
109
110        # This fields should be present and not null
111        fields = ('name', 'version', 'description', 'url', 'start_file',
112                  'license', 'copyright', 'author')
113
114        for element in fields:
115            if not getattr(self, element, None):
116                txt = 'Element named %s should not be null.' % (element)
117
118                if use_print:
119                    print txt
120                else:
121                    log.warning(txt)
122
123                return False
124
125        return True
126
127    def get_provides(self): return self.provide
128    def get_conflicts(self): return self.conflict
129    def get_needs(self): return self.need
130
131    provides = property(get_provides)
132    conflicts = property(get_conflicts)
133    needs = property(get_needs)
134
135class ManifestLoader(handler.ContentHandler, ManifestObject):
136    def __init__(self):
137        ManifestObject.__init__(self)
138
139        self.element_idx = 0
140        self.parsing_pass = -1
141        self.current_element = None
142        self.data = None
143
144    def startElement(self, name, attrs):
145        try:
146            self.element_idx = self.elements[self.parsing_pass].index(name)
147            self.current_element = \
148                self.elements[self.parsing_pass][self.element_idx]
149
150        except IndexError:
151            log.debug('Element named `%s` is not in %s' % \
152                      (name, self.elements[self.parsing_pass]))
153
154        except ValueError:
155            try:
156                idx = self.containers.index(name)
157
158                if self.parsing_pass < idx:
159                    self.parsing_pass = idx
160                else:
161                    log.warning('Element `%s` is not valid at this point. ' \
162                                'Should compare before %s' % (name,
163                                            self.containers[self.parsing_pass]))
164
165                if self.parsing_pass == 0:
166                    if type in attrs.keys():
167                        self.attr_type = attrs.get('type')
168                    else:
169                        self.attr_type = 'ui'
170
171            except ValueError:
172                log.debug('Element named `%s` not excepted.' % name)
173
174    def characters(self, ch):
175        if not self.current_element:
176            return
177
178        if not self.data:
179            self.data = ch
180        else:
181            self.data += ch
182
183    def endElement(self, name):
184        if self.current_element == name:
185            try:
186                attr = getattr(self, name)
187
188                if isinstance(attr, basestring):
189                    setattr(self, name, self.data)
190                elif isinstance(attr, list):
191                    attr.append(self.data)
192            finally:
193                self.current_element = None
194                self.data = None
195
196class ManifestWriter(object):
197    def startElement(self, names, attrs):
198        self.depth_idx += 1
199        self.writer.characters('  ' * self.depth_idx)
200        self.writer.startElement(names, attrs)
201
202    def endElement(self, name):
203        self.writer.endElement(name)
204        self.writer.characters('\n')
205        self.depth_idx -= 1
206
207    def __init__(self, manifest):
208        assert isinstance(manifest, ManifestObject)
209
210        self.output = StringIO()
211        self.depth_idx = -1
212        self.manifest = manifest
213        self.writer = XMLGenerator(self.output, 'utf-8')
214        self.writer.startDocument()
215
216        attr_vals = {
217            'xmlns' : 'http://www.umitproject.org',
218            'xsi:schemaLocation' : 'http://www.umitproject.org UmitPlugins.xsd',
219            'xmlns:xsi' : 'http://www.w3.org/2001/XMLSchema-instance',
220            'type' : manifest.attr_type or 'ui'
221        }
222
223        self.startElement('UmitPlugin', AttributesImpl(attr_vals)),
224        self.writer.characters('\n')
225
226        # First phase saving
227        for elem in manifest.elements[0]:
228            self.add_element(elem)
229
230        # Runtime block
231        self.startElement('runtime', {})
232        self.writer.characters('\n')
233
234        self.add_element('start_file')
235        self.add_element('update')
236
237        self.writer.characters('  ' * self.depth_idx)
238        self.endElement('runtime')
239
240        # Deptree block
241        if manifest.provide or manifest.need or manifest.conflict:
242            self.startElement('deptree', {})
243            self.writer.characters('\n')
244
245            self.add_element('provide')
246            self.add_element('need')
247            self.add_element('conflict')
248
249            self.writer.characters('  ' * self.depth_idx)
250            self.endElement('deptree')
251
252        # Credits block
253        self.startElement('credits', {})
254        self.writer.characters('\n')
255
256        for elem in manifest.elements[3]:
257            self.add_element(elem)
258
259        self.writer.characters('  ' * self.depth_idx)
260        self.endElement('credits')
261
262        self.endElement('UmitPlugin')
263        self.writer.endDocument()
264
265    def add_element(self, name):
266        value = getattr(self.manifest, name, None)
267
268        if not value:
269            return
270
271        if isinstance(value, basestring):
272            self.startElement(name, {})
273            self.writer.characters(value)
274            self.endElement(name)
275        elif isinstance(value, list):
276            for item in value:
277                self.startElement(name, {})
278                self.writer.characters(item)
279                self.endElement(name)
280
281    def get_output(self):
282        return self.output.getvalue()
283
284class BadPlugin(Exception):
285    "Used to track exceptions while loading Plugin"
286    pass
287
288class PluginReader(ManifestLoader):
289    def __init__(self, file):
290        ManifestLoader.__init__(self)
291
292        self.path = file
293        self.enabled = False
294        self.hasprefs = False
295        self.parser = None
296
297        try:
298            self.file = ZipFile(file, "r")
299        except:
300            raise BadPlugin("Not a valid umit plugin format")
301
302        if not self.parse_manifest():
303            raise BadPlugin("Not a valid umit plugin manifest")
304
305        if not self.check_validity():
306            raise BadPlugin("Validation phase not passed")
307
308        # Needs some testing
309        self.parse_preferences()
310
311    def parse_manifest(self):
312        """
313        Parse the Manifest.xml inside the zip file and set the fields
314
315        @return
316                False if the Manifest is not in the proper format
317                True if everything is ok
318        """
319
320        try:
321            # TODO: add validation of the manifest
322
323            # Py2.5 doesn't have open on ZipFile object
324            fileobj = StringIO(self.file.read('Manifest.xml'))
325
326            parser = make_parser()
327            parser.setContentHandler(self)
328
329            parser.parse(fileobj)
330        except Exception, err:
331            log.debug('Exception in parse_manifest(): %s' % str(err))
332            return False
333
334        return True
335
336    def parse_preferences(self):
337        try:
338            data = self.file.read('data/preferences.xml')
339
340            self.parser = Parser()
341            self.parser.parse_string(data)
342        except Exception, err:
343            return
344
345    def __repr__(self):
346        #FIXME: that
347        return "[%s::Plugin]" % self.name
348
349    def get_logo(self, w=64, h=64):
350        "@return a gtk.dk.Pixbuf"
351
352        try:
353            # TODO: eliminate the mktemp workaround
354
355            name = mktemp('.png')
356            f = open(name, 'wb')
357            f.write(self.file.read('data/logo.png'))
358            f.close()
359
360            import gtk
361
362            p = gtk.gdk.pixbuf_new_from_file_at_size(name, w, h)
363
364            os.remove(name)
365
366            return p
367        except Exception, err:
368
369            from umit.gui.Icons import get_pixbuf
370
371            return get_pixbuf('extension_normal', w, h)
372
373    def get_path(self):
374        return self.path
375
376    def extract_dir(self, zip_path, maxdepth=0):
377        """
378        Extract a dir full recursive.
379        @param zip_path the directory to extract (for example data/test/)
380        @param maxdepth the max depth. Set 0 for fully recursive extraction.
381        @return a list containing extracted files or []
382        """
383        ret = []
384        if zip_path[-1] != '/':
385            zip_path += '/'
386        if zip_path[0] == '/':
387            zip_path = zip_path[1:]
388
389        sep_len = zip_path.count('/')
390
391        log.debug("Extracting files contained in %s" % zip_path)
392
393        for i in self.file.namelist():
394            if i.startswith(zip_path):
395                if maxdepth > 0 and \
396                   i.count('/') - sep_len - maxdepth + 1 != 0:
397
398                   log.debug("Skipping %s for maxdepth %d" % (i, maxdepth))
399                   continue
400
401                p = self.extract_file(i, keep_path=True)
402
403                if p: ret.append(p)
404
405        return ret
406
407    def extract_file(self, zip_path, keep_path=False):
408        if zip_path not in self.file.namelist():
409            log.debug("The file %s seems to not exists in the zip file" % zip_path)
410            return None
411
412        plug_subdir = os.path.join(Path.config_dir, 'plugins-temp', self.name)
413
414        if not os.path.exists(plug_subdir):
415            os.mkdir(plug_subdir)
416
417        if keep_path:
418            # Recursive reconstruct the entire path
419            full_path = os.path.join(plug_subdir, os.path.dirname(zip_path))
420            if not os.path.isdir(full_path):
421                os.makedirs(full_path)
422            plug_subdir = full_path
423
424        log.debug("Extracting %s into %s " % (zip_path, plug_subdir))
425
426        name = os.path.join(plug_subdir,
427                            os.path.basename(zip_path))
428
429        f = open(name, 'wb+')
430        f.write(self.file.read(zip_path))
431        f.close()
432
433        return name
434
435    # Code ripped from gettext
436    def expand_lang(self, locale):
437        from locale import normalize
438        locale = normalize(locale)
439        COMPONENT_CODESET   = 1 << 0
440        COMPONENT_TERRITORY = 1 << 1
441        COMPONENT_MODIFIER  = 1 << 2
442        # split up the locale into its base components
443        mask = 0
444        pos = locale.find('@')
445        if pos >= 0:
446            modifier = locale[pos:]
447            locale = locale[:pos]
448            mask |= COMPONENT_MODIFIER
449        else:
450            modifier = ''
451        pos = locale.find('.')
452        if pos >= 0:
453            codeset = locale[pos:]
454            locale = locale[:pos]
455            mask |= COMPONENT_CODESET
456        else:
457            codeset = ''
458        pos = locale.find('_')
459        if pos >= 0:
460            territory = locale[pos:]
461            locale = locale[:pos]
462            mask |= COMPONENT_TERRITORY
463        else:
464            territory = ''
465        language = locale
466        ret = []
467        for i in range(mask+1):
468            if not (i & ~mask):  # if all components for this combo exist ...
469                val = language
470                if i & COMPONENT_TERRITORY: val += territory
471                if i & COMPONENT_CODESET:   val += codeset
472                if i & COMPONENT_MODIFIER:  val += modifier
473                ret.append(val)
474        ret.reverse()
475        return ret
476
477    def bind_translation(self, mofile):
478        """
479        @return a catalog on success or None
480        """
481
482        # We foreach inside locale dir and find a proper dir
483
484        try:
485            import gettext
486            import locale
487            LC_ALL = locale.setlocale(locale.LC_ALL, '')
488        except locale.Error:
489            return None
490
491        LANG, ENC = locale.getdefaultlocale()
492
493        if ENC is None:
494            ENC = "utf8"
495        if LANG is None:
496            LANG = "en_US"
497
498        dir_lst = filter( \
499            lambda x: x.startswith("locale/") and x.endswith("%s.mo" % mofile), \
500            self.file.namelist() \
501        )
502        dir_lst.sort()
503
504        avaiable_langs = []
505
506        for dirname in dir_lst:
507            t = dirname.split("/")
508
509            if len(t) < 3:
510                continue
511
512            avaiable_langs.append(t[-2])
513
514        request = self.expand_lang(".".join([LANG, ENC]))
515
516        for req in request:
517            if req in avaiable_langs:
518                # Ok getted! Lucky day :)
519
520                return gettext.GNUTranslations(StringFile( \
521                    self.file.read("locale/%s/%s.mo" % (req, mofile)) \
522                ))
523
524        return None
525
526class PluginWriter(ManifestObject):
527    def __init__(self, **fields):
528        ManifestObject.__init__(self)
529
530        # Set to None and filter out the unused fields
531
532        FIELDS = ('name', 'version', 'description', 'url', 'start_file',
533                  'update', 'provide', 'need', 'conflict', 'license',
534                  'copyright', 'author', 'contributor', 'translator', 'artist')
535
536        # Filter out fields that are not related to the schema
537
538        for i in fields:
539            if i in FIELDS:
540                setattr(self, i, fields[i])
541
542        if not self.check_validity(use_print=True):
543            print "!! Manifest could not be created."
544            sys.exit(-1)
545
546        dirs = {
547            'bin'  : '*',
548            'data' : '*',
549            'lib'  : '*',
550            'locale' : '*'
551        }
552
553        self.file = ZipFile(fields['output'], "w", ZIP_DEFLATED)
554
555        os.chdir("output")
556
557        for i in dirs:
558            self.dir_foreach(i, dirs[i])
559
560        os.chdir("..")
561
562        writer = ManifestWriter(self)
563        self.file.writestr('Manifest.xml', writer.get_output())
564        self.file.close()
565
566        print ">> Plugin %s created." % fields['output']
567
568    def dir_foreach(self, dir, pattern):
569        "Add files contained in dir and that pass the pattern validation phase."
570
571        for path, dirs, files in os.walk(dir):
572            if not files:
573                continue
574
575            for file in files:
576                if not fnmatch(file, pattern):
577                    continue
578
579                print "Adding file %s %s %s" % (path, file, dir)
580
581                self.file.write(os.path.join(path, file),
582                                os.path.join(path, file))
583
584    def create_manifest(self):
585        """
586        Create a Manifest.xml file
587
588        @return an xml manifest as string
589        """
590        doc = getDOMImplementation().createDocument(None, SIGNATURE, None)
591
592        for field in FIELDS:
593            node = doc.createElement(field)
594            node.appendChild(doc.createTextNode(getattr(self, field)))
595            doc.documentElement.appendChild(node)
596
597        print "Manifest.xml created"
598        return doc.toxml()
599
600
601#
602# distutils related class
603
604class PlugLibInstaller(install_libcmd):
605    def finalize_options(self):
606        self.install_dir = 'output/lib'
607        install_libcmd.finalize_options(self)
608
609class PlugInstaller(installcmd):
610    def finalize_options(self):
611        self.home = 'output'
612        installcmd.finalize_options(self)
613
614    def run(self):
615        installcmd.run(self)
616
617class PlugDistribution(Distribution):
618    def __init__(self, *attrs):
619        Distribution.__init__(self, *attrs)
620        self.cmdclass['install'] = PlugInstaller
621        self.cmdclass['install_lib'] = PlugLibInstaller
622        self.cmdclass['install_egg_info'] = PlugEggInstaller
623
624
625def setup(**attrs):
626    "Called to create a plugin like the dist-tools setup function"
627
628    setup_d = {}
629
630    # We need to filter out some fields
631    # to avoid
632    for attr in attrs:
633        if attr not in setup_keywords and \
634           attr not in extension_keywords:
635            setup_d[attr] = attrs[attr]
636
637    print ">> Running setup()"
638
639    import warnings
640    warnings.filterwarnings('ignore', r".*", UserWarning)
641
642    setup_d['distclass'] = PlugDistribution
643    dist_setup(**setup_d)
644
645    print ">> Creating plugin"
646    PluginWriter(**attrs)
647
648    print ">> Cleaning up"
649    shutil.rmtree('build')
650    shutil.rmtree('output')
651
652if __name__ == "__main__":
653    parser = make_parser()
654    loader = ManifestLoader()
655    parser.setContentHandler(loader)
656    parser.parse(open('test.xml'))
657    loader.dump()
Note: See TracBrowser for help on using the browser.