~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/basicio.py

Merge from integration.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""basic_io - simple text metaformat
18
 
 
19
 
The stored data consists of a series of *stanzas*, each of which contains
20
 
*fields* identified by an ascii name.  The contents of each field can be
21
 
either an integer (scored in decimal) or a Unicode string.
22
 
"""
23
 
 
24
 
import re
25
 
 
26
 
# XXX: basic_io is kind of a dumb name; it seems to imply an io layer not a
27
 
# format
28
 
#
29
 
# XXX: some redundancy is allowing to write stanzas in isolation as well as
30
 
# through a writer object.  
31
 
 
32
 
class BasicWriter(object):
33
 
    def __init__(self, to_file):
34
 
        self._soft_nl = False
35
 
        self._to_file = to_file
36
 
 
37
 
    def write_stanza(self, stanza):
38
 
        if self._soft_nl:
39
 
            print >>self._to_file
40
 
        stanza.write(self._to_file)
41
 
        self._soft_nl = True
42
 
 
43
 
 
44
 
class BasicReader(object):
45
 
    """Read stanzas from a file as a sequence
46
 
    
47
 
    to_file can be anything that can be enumerated as a sequence of 
48
 
    lines (with newlines.)
49
 
    """
50
 
    def __init__(self, from_file):
51
 
        self._from_file = from_file
52
 
 
53
 
    def __iter__(self):
54
 
        while True:
55
 
            s = read_stanza(self._from_file)
56
 
            if s is None:
57
 
                break
58
 
            else:
59
 
                yield s
60
 
 
61
 
def read_stanzas(from_file):
62
 
    while True:
63
 
        s = read_stanza(from_file)
64
 
        if s is None:
65
 
            break
66
 
        else:
67
 
            yield s
68
 
 
69
 
class Stanza(object):
70
 
    """One stanza for basic_io.
71
 
 
72
 
    Each stanza contains a set of named fields.  
73
 
    
74
 
    Names must be non-empty ascii alphanumeric plus _.  Names can be repeated
75
 
    within a stanza.  Names are case-sensitive.  The ordering of fields is
76
 
    preserved.
77
 
 
78
 
    Each field value must be either an int or a string.
79
 
    """
80
 
 
81
 
    __slots__ = ['items']
82
 
 
83
 
    def __init__(self, **kwargs):
84
 
        """Construct a new Stanza.
85
 
 
86
 
        The keyword arguments, if any, are added in sorted order to the stanza.
87
 
        """
88
 
        if kwargs:
89
 
            self.items = sorted(kwargs.items())
90
 
        else:
91
 
            self.items = []
92
 
 
93
 
    def add(self, tag, value):
94
 
        """Append a name and value to the stanza."""
95
 
##         if not valid_tag(tag):
96
 
##             raise ValueError("invalid tag %r" % tag)
97
 
##         if not isinstance(value, (int, long, str, unicode)):
98
 
##             raise ValueError("invalid value %r" % value)
99
 
        self.items.append((tag, value))
100
 
        
101
 
    def __contains__(self, find_tag):
102
 
        """True if there is any field in this stanza with the given tag."""
103
 
        for tag, value in self.items:
104
 
            if tag == find_tag:
105
 
                return True
106
 
        return False
107
 
 
108
 
    def __len__(self):
109
 
        """Return number of pairs in the stanza."""
110
 
        return len(self.items)
111
 
 
112
 
    def __eq__(self, other):
113
 
        if not isinstance(other, Stanza):
114
 
            return False
115
 
        return self.items == other.items
116
 
 
117
 
    def __ne__(self, other):
118
 
        return not self.__eq__(other)
119
 
 
120
 
    def __repr__(self):
121
 
        return "Stanza(%r)" % self.items
122
 
 
123
 
    def iter_pairs(self):
124
 
        """Return iterator of tag, value pairs."""
125
 
        return iter(self.items)
126
 
 
127
 
    def to_lines(self):
128
 
        """Generate sequence of lines for external version of this file."""
129
 
        if not self.items:
130
 
            # max() complains if sequence is empty
131
 
            return 
132
 
        indent = max(len(kv[0]) for kv in self.items)
133
 
        for tag, value in self.items:
134
 
            if isinstance(value, (int, long)):
135
 
                # must use %d so bools are written as ints
136
 
                yield '%*s %d\n' % (indent, tag, value)
137
 
            else:
138
 
                assert isinstance(value, (str, unicode)), ("invalid value %r" % value)
139
 
                qv = value.replace('\\', r'\\') \
140
 
                          .replace('"',  r'\"')
141
 
                yield '%*s "%s"\n' % (indent, tag, qv)
142
 
 
143
 
    def to_string(self):
144
 
        """Return stanza as a single string"""
145
 
        return ''.join(self.to_lines())
146
 
 
147
 
    def write(self, to_file):
148
 
        """Write stanza to a file"""
149
 
        to_file.writelines(self.to_lines())
150
 
 
151
 
    def get(self, tag):
152
 
        """Return the value for a field wih given tag.
153
 
 
154
 
        If there is more than one value, only the first is returned.  If the
155
 
        tag is not present, KeyError is raised.
156
 
        """
157
 
        for t, v in self.items:
158
 
            if t == tag:
159
 
                return v
160
 
        else:
161
 
            raise KeyError(tag)
162
 
 
163
 
    __getitem__ = get
164
 
 
165
 
    def get_all(self, tag):
166
 
        r = []
167
 
        for t, v in self.items:
168
 
            if t == tag:
169
 
                r.append(v)
170
 
        return r
171
 
         
172
 
TAG_RE = re.compile(r'^[-a-zA-Z0-9_]+$')
173
 
def valid_tag(tag):
174
 
    return bool(TAG_RE.match(tag))
175
 
 
176
 
 
177
 
def read_stanza(line_iter):
178
 
    """Return new Stanza read from list of lines or a file"""
179
 
    items = []
180
 
    got_lines = False
181
 
    for l in line_iter:
182
 
        if l == None or l == '':
183
 
            break # eof
184
 
        got_lines = True
185
 
        if l == '\n':
186
 
            break
187
 
        assert l[-1] == '\n'
188
 
        real_l = l
189
 
        l = l.lstrip()
190
 
        try:
191
 
            space = l.index(' ')
192
 
        except ValueError:
193
 
            raise ValueError('tag/value separator not found in line %r' % real_l)
194
 
        tag = l[:space]
195
 
        assert valid_tag(tag), \
196
 
                "invalid basic_io tag %r" % tag
197
 
        rest = l[space+1:]
198
 
        if l[space+1] == '"':
199
 
            value = ''
200
 
            valpart = l[space+2:]
201
 
            while True:
202
 
                assert valpart[-1] == '\n'
203
 
                len_valpart = len(valpart)
204
 
                if len_valpart >= 2 and valpart[-2] == '"':
205
 
                    # is this a real terminating doublequote, or is it escaped
206
 
                    # by a preceding backslash that is not itself escaped?
207
 
                    i = 3
208
 
                    while i <= len_valpart and valpart[-i] == '\\':
209
 
                        i += 1
210
 
                    num_slashes = i - 3
211
 
                    if num_slashes & 1:
212
 
                        # it's escaped, so the escaped backslash and newline 
213
 
                        # are passed through
214
 
                        value += valpart
215
 
                    else:
216
 
                        value += valpart[:-2]
217
 
                        break
218
 
                else:
219
 
                    value += valpart
220
 
                try:
221
 
                    valpart = line_iter.next()
222
 
                except StopIteration:
223
 
                    raise ValueError('end of file in quoted string after %r' % value)
224
 
            value = value.replace('\\"', '"').replace('\\\\', '\\')
225
 
        else:
226
 
            value_str = l[space+1:]
227
 
            try:
228
 
                value = int(value_str)
229
 
            except ValueError:
230
 
                raise ValueError('invalid integer %r for tag %r in line %r' 
231
 
                        % (value_str, tag, real_l))
232
 
        items.append((tag, value))
233
 
    if not got_lines:
234
 
        return None         # didn't see any content
235
 
    s = Stanza()
236
 
    s.items = items
237
 
    return s
238
 
 
239
 
 
240
 
############################################################
241
 
 
242
 
# XXX: Move these to object serialization code. 
243
 
 
244
 
def write_revision(writer, revision):
245
 
    s = Stanza(revision=revision.revision_id,
246
 
               committer=revision.committer, 
247
 
               timezone=long(revision.timezone),
248
 
               timestamp=long(revision.timestamp),
249
 
               inventory_sha1=revision.inventory_sha1,
250
 
               message=revision.message)
251
 
    for parent_id in revision.parent_ids:
252
 
        s.add('parent', parent_id)
253
 
    for prop_name, prop_value in revision.properties.items():
254
 
        s.add(prop_name, prop_value)
255
 
    writer.write_stanza(s)
256
 
 
257
 
def write_inventory(writer, inventory):
258
 
    s = Stanza(inventory_version=7)
259
 
    writer.write_stanza(s)
260
 
 
261
 
    for path, ie in inventory.iter_entries():
262
 
        s = Stanza()
263
 
        s.add(ie.kind, ie.file_id)
264
 
        for attr in ['name', 'parent_id', 'revision',
265
 
                     'text_sha1', 'text_size', 'executable', 'symlink_target',
266
 
                     ]:
267
 
            attr_val = getattr(ie, attr, None)
268
 
            if attr == 'executable' and attr_val == 0:
269
 
                continue
270
 
            if attr == 'parent_id' and attr_val == 'TREE_ROOT':
271
 
                continue
272
 
            if attr_val is not None:
273
 
                s.add(attr, attr_val)
274
 
        writer.write_stanza(s)
275
 
 
276
 
 
277
 
def read_inventory(inv_file):
278
 
    """Read inventory object from basic_io formatted inventory file"""
279
 
    from bzrlib.inventory import Inventory, InventoryFile
280
 
    s = read_stanza(inv_file)
281
 
    assert s['inventory_version'] == 7
282
 
    inv = Inventory()
283
 
    for s in read_stanzas(inv_file):
284
 
        kind, file_id = s.items[0]
285
 
        parent_id = None
286
 
        if 'parent_id' in s:
287
 
            parent_id = s['parent_id']
288
 
        if kind == 'file':
289
 
            ie = InventoryFile(file_id, s['name'], parent_id)
290
 
            ie.text_sha1 = s['text_sha1']
291
 
            ie.text_size = s['text_size']
292
 
        else:
293
 
            raise NotImplementedError()
294
 
        inv.add(ie)
295
 
    return inv