~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/xml5.py

  • Committer: John Arbash Meinel
  • Date: 2006-11-10 15:38:16 UTC
  • mto: This revision was merged to the branch mainline in revision 2129.
  • Revision ID: john@arbash-meinel.com-20061110153816-46acf76fc86a512b
use try/finally to clean up a nested progress bar during weave fetching

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
19
19
 
20
20
from bzrlib import (
21
21
    cache_utf8,
22
 
    errors,
23
22
    inventory,
24
23
    )
25
24
from bzrlib.xml_serializer import SubElement, Element, Serializer
29
28
 
30
29
 
31
30
_utf8_re = None
32
 
_unicode_re = None
33
 
_xml_escape_map = {
 
31
_utf8_escape_map = {
34
32
    "&":'&',
35
33
    "'":"'", # FIXME: overkill
36
34
    "\"":""",
40
38
 
41
39
 
42
40
def _ensure_utf8_re():
43
 
    """Make sure the _utf8_re and _unicode_re regexes have been compiled."""
44
 
    global _utf8_re, _unicode_re
45
 
    if _utf8_re is None:
46
 
        _utf8_re = re.compile('[&<>\'\"]|[\x80-\xff]+')
47
 
    if _unicode_re is None:
48
 
        _unicode_re = re.compile(u'[&<>\'\"\u0080-\uffff]')
49
 
 
50
 
 
51
 
def _unicode_escape_replace(match, _map=_xml_escape_map):
 
41
    """Make sure the _utf8_re regex has been compiled"""
 
42
    global _utf8_re
 
43
    if _utf8_re is not None:
 
44
        return
 
45
    _utf8_re = re.compile(u'[&<>\'\"\u0080-\uffff]')
 
46
 
 
47
 
 
48
def _utf8_escape_replace(match, _map=_utf8_escape_map):
52
49
    """Replace a string of non-ascii, non XML safe characters with their escape
53
50
 
54
51
    This will escape both Standard XML escapes, like <>"', etc.
67
64
        return "&#%d;" % ord(match.group())
68
65
 
69
66
 
70
 
def _utf8_escape_replace(match, _map=_xml_escape_map):
71
 
    """Escape utf8 characters into XML safe ones.
72
 
 
73
 
    This uses 2 tricks. It is either escaping "standard" characters, like "&<>,
74
 
    or it is handling characters with the high-bit set. For ascii characters,
75
 
    we just lookup the replacement in the dictionary. For everything else, we
76
 
    decode back into Unicode, and then use the XML escape code.
77
 
    """
78
 
    try:
79
 
        return _map[match.group()]
80
 
    except KeyError:
81
 
        return ''.join('&#%d;' % ord(uni_chr)
82
 
                       for uni_chr in match.group().decode('utf8'))
83
 
 
84
 
 
85
 
_to_escaped_map = {}
86
 
 
87
 
def _encode_and_escape(unicode_or_utf8_str, _map=_to_escaped_map):
 
67
_unicode_to_escaped_map = {}
 
68
 
 
69
def _encode_and_escape(unicode_str, _map=_unicode_to_escaped_map):
88
70
    """Encode the string into utf8, and escape invalid XML characters"""
89
71
    # We frequently get entities we have not seen before, so it is better
90
72
    # to check if None, rather than try/KeyError
91
 
    text = _map.get(unicode_or_utf8_str)
 
73
    text = _map.get(unicode_str)
92
74
    if text is None:
93
 
        if unicode_or_utf8_str.__class__ == unicode:
94
 
            # The alternative policy is to do a regular UTF8 encoding
95
 
            # and then escape only XML meta characters.
96
 
            # Performance is equivalent once you use cache_utf8. *However*
97
 
            # this makes the serialized texts incompatible with old versions
98
 
            # of bzr. So no net gain. (Perhaps the read code would handle utf8
99
 
            # better than entity escapes, but cElementTree seems to do just fine
100
 
            # either way)
101
 
            text = str(_unicode_re.sub(_unicode_escape_replace,
102
 
                                       unicode_or_utf8_str)) + '"'
103
 
        else:
104
 
            # Plain strings are considered to already be in utf-8 so we do a
105
 
            # slightly different method for escaping.
106
 
            text = _utf8_re.sub(_utf8_escape_replace,
107
 
                                unicode_or_utf8_str) + '"'
108
 
        _map[unicode_or_utf8_str] = text
 
75
        # The alternative policy is to do a regular UTF8 encoding
 
76
        # and then escape only XML meta characters.
 
77
        # Performance is equivalent once you use cache_utf8. *However*
 
78
        # this makes the serialized texts incompatible with old versions
 
79
        # of bzr. So no net gain. (Perhaps the read code would handle utf8
 
80
        # better than entity escapes, but cElementTree seems to do just fine
 
81
        # either way)
 
82
        text = str(_utf8_re.sub(_utf8_escape_replace, unicode_str)) + '"'
 
83
        _map[unicode_str] = text
109
84
    return text
110
85
 
111
86
 
112
 
def _get_utf8_or_ascii(a_str,
113
 
                       _encode_utf8=cache_utf8.encode,
114
 
                       _get_cached_ascii=cache_utf8.get_cached_ascii):
115
 
    """Return a cached version of the string.
116
 
 
117
 
    cElementTree will return a plain string if the XML is plain ascii. It only
118
 
    returns Unicode when it needs to. We want to work in utf-8 strings. So if
119
 
    cElementTree returns a plain string, we can just return the cached version.
120
 
    If it is Unicode, then we need to encode it.
121
 
 
122
 
    :param a_str: An 8-bit string or Unicode as returned by
123
 
                  cElementTree.Element.get()
124
 
    :return: A utf-8 encoded 8-bit string.
125
 
    """
126
 
    # This is fairly optimized because we know what cElementTree does, this is
127
 
    # not meant as a generic function for all cases. Because it is possible for
128
 
    # an 8-bit string to not be ascii or valid utf8.
129
 
    if a_str.__class__ == unicode:
130
 
        return _encode_utf8(a_str)
131
 
    else:
132
 
        return _get_cached_ascii(a_str)
133
 
 
134
 
 
135
87
def _clear_cache():
136
88
    """Clean out the unicode => escaped map"""
137
 
    _to_escaped_map.clear()
 
89
    _unicode_to_escaped_map.clear()
138
90
 
139
91
 
140
92
class Serializer_v5(Serializer):
149
101
    # This format supports the altered-by hack that reads file ids directly out
150
102
    # of the versionedfile, without doing XML parsing.
151
103
 
152
 
    supported_kinds = set(['file', 'directory', 'symlink'])
153
 
 
154
104
    def write_inventory_to_string(self, inv):
155
105
        """Just call write_inventory with a StringIO and return the value"""
156
106
        sio = cStringIO.StringIO()
193
143
    def _append_entry(self, append, ie):
194
144
        """Convert InventoryEntry to XML element and append to output."""
195
145
        # TODO: should just be a plain assertion
196
 
        if ie.kind not in self.supported_kinds:
197
 
            raise errors.UnsupportedInventoryKind(ie.kind)
 
146
        assert InventoryEntry.versionable_kind(ie.kind), \
 
147
            'unsupported entry kind %s' % ie.kind
198
148
 
199
149
        append("<")
200
150
        append(ie.kind)
220
170
            append('"')
221
171
        if ie.text_size is not None:
222
172
            append(' text_size="%d"' % ie.text_size)
223
 
        if getattr(ie, 'reference_revision', None) is not None:
224
 
            append(' reference_revision="')
225
 
            append(_encode_and_escape(ie.reference_revision))
226
173
        append(" />\n")
227
174
        return
228
175
 
231
178
 
232
179
    def _pack_revision(self, rev):
233
180
        """Revision object -> xml tree"""
234
 
        # For the XML format, we need to write them as Unicode rather than as
235
 
        # utf-8 strings. So that cElementTree can handle properly escaping
236
 
        # them.
237
 
        decode_utf8 = cache_utf8.decode
238
 
        revision_id = rev.revision_id
239
 
        if isinstance(revision_id, str):
240
 
            revision_id = decode_utf8(revision_id)
241
181
        root = Element('revision',
242
182
                       committer = rev.committer,
243
 
                       timestamp = '%.3f' % rev.timestamp,
244
 
                       revision_id = revision_id,
 
183
                       timestamp = '%.9f' % rev.timestamp,
 
184
                       revision_id = rev.revision_id,
245
185
                       inventory_sha1 = rev.inventory_sha1,
246
186
                       format='5',
247
187
                       )
258
198
                assert isinstance(parent_id, basestring)
259
199
                p = SubElement(pelts, 'revision_ref')
260
200
                p.tail = '\n'
261
 
                if isinstance(parent_id, str):
262
 
                    parent_id = decode_utf8(parent_id)
263
201
                p.set('revision_id', parent_id)
264
202
        if rev.properties:
265
203
            self._pack_revision_properties(rev, root)
281
219
        """
282
220
        assert elt.tag == 'inventory'
283
221
        root_id = elt.get('file_id') or ROOT_ID
284
 
        root_id = _get_utf8_or_ascii(root_id)
285
 
 
286
222
        format = elt.get('format')
287
223
        if format is not None:
288
224
            if format != '5':
290
226
                                % format)
291
227
        revision_id = elt.get('revision_id')
292
228
        if revision_id is not None:
293
 
            revision_id = cache_utf8.encode(revision_id)
 
229
            revision_id = cache_utf8.get_cached_unicode(revision_id)
294
230
        inv = Inventory(root_id, revision_id=revision_id)
295
231
        for e in elt:
296
232
            ie = self._unpack_entry(e)
297
 
            if ie.parent_id is None:
 
233
            if ie.parent_id == ROOT_ID:
298
234
                ie.parent_id = root_id
299
235
            inv.add(ie)
300
236
        return inv
301
237
 
302
 
    def _unpack_entry(self, elt):
 
238
    def _unpack_entry(self, elt, none_parents=False):
303
239
        kind = elt.tag
304
240
        if not InventoryEntry.versionable_kind(kind):
305
241
            raise AssertionError('unsupported entry kind %s' % kind)
306
242
 
307
 
        get_cached = _get_utf8_or_ascii
 
243
        get_cached = cache_utf8.get_cached_unicode
308
244
 
309
245
        parent_id = elt.get('parent_id')
310
 
        if parent_id is not None:
311
 
            parent_id = get_cached(parent_id)
312
 
        file_id = get_cached(elt.get('file_id'))
 
246
        if parent_id is None and not none_parents:
 
247
            parent_id = ROOT_ID
 
248
        # TODO: jam 20060817 At present, caching file ids costs us too 
 
249
        #       much time. It slows down overall read performances from
 
250
        #       approx 500ms to 700ms. And doesn't improve future reads.
 
251
        #       it might be because revision ids and file ids are mixing.
 
252
        #       Consider caching *just* the file ids, for a limited period
 
253
        #       of time.
 
254
        #parent_id = get_cached(parent_id)
 
255
        #file_id = get_cached(elt.get('file_id'))
 
256
        file_id = elt.get('file_id')
313
257
 
314
258
        if kind == 'directory':
315
259
            ie = inventory.InventoryDirectory(file_id,
330
274
                                         parent_id)
331
275
            ie.symlink_target = elt.get('symlink_target')
332
276
        else:
333
 
            raise errors.UnsupportedInventoryKind(kind)
 
277
            raise BzrError("unknown kind %r" % kind)
334
278
        revision = elt.get('revision')
335
279
        if revision is not None:
336
280
            revision = get_cached(revision)
346
290
            if format != '5':
347
291
                raise BzrError("invalid format version %r on inventory"
348
292
                                % format)
349
 
        get_cached = _get_utf8_or_ascii
 
293
        get_cached = cache_utf8.get_cached_unicode
350
294
        rev = Revision(committer = elt.get('committer'),
351
295
                       timestamp = float(elt.get('timestamp')),
352
296
                       revision_id = get_cached(elt.get('revision_id')),