~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/basicio.py

[merge] basic_io metaformat (mbp)

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