~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/rio.py

  • Committer: Martin Pool
  • Date: 2005-05-09 01:18:48 UTC
  • Revision ID: mbp@sourcefrog.net-20050509011848-2d0ae1e478a4cf3d
- testbzr finds the right version of bzr to test

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Canonical Ltd
2
 
#
3
 
# Distributed under the GNU General Public Licence v2
4
 
 
5
 
# \subsection{\emph{rio} - simple text metaformat}
6
 
7
 
# \emph{r} stands for `restricted', `reproducible', or `rfc822-like'.
8
 
9
 
# The stored data consists of a series of \emph{stanzas}, each of which contains
10
 
# \emph{fields} identified by an ascii name, with Unicode or string contents.
11
 
# The field tag is constrained to alphanumeric characters.  
12
 
# There may be more than one field in a stanza with the same name.
13
 
14
 
# The format itself does not deal with character encoding issues, though
15
 
# the result will normally be written in Unicode.
16
 
17
 
# The format is intended to be simple enough that there is exactly one character
18
 
# stream representation of an object and vice versa, and that this relation
19
 
# will continue to hold for future versions of bzr.
20
 
 
21
 
import re
22
 
 
23
 
from bzrlib.iterablefile import IterableFile
24
 
 
25
 
# XXX: some redundancy is allowing to write stanzas in isolation as well as
26
 
# through a writer object.  
27
 
 
28
 
class RioWriter(object):
29
 
    def __init__(self, to_file):
30
 
        self._soft_nl = False
31
 
        self._to_file = to_file
32
 
 
33
 
    def write_stanza(self, stanza):
34
 
        if self._soft_nl:
35
 
            print >>self._to_file
36
 
        stanza.write(self._to_file)
37
 
        self._soft_nl = True
38
 
 
39
 
 
40
 
class RioReader(object):
41
 
    """Read stanzas from a file as a sequence
42
 
    
43
 
    to_file can be anything that can be enumerated as a sequence of 
44
 
    lines (with newlines.)
45
 
    """
46
 
    def __init__(self, from_file):
47
 
        self._from_file = from_file
48
 
 
49
 
    def __iter__(self):
50
 
        while True:
51
 
            s = read_stanza(self._from_file)
52
 
            if s is None:
53
 
                break
54
 
            else:
55
 
                yield s
56
 
 
57
 
 
58
 
def rio_file(stanzas, header=None):
59
 
    """Produce a rio IterableFile from an iterable of stanzas"""
60
 
    def str_iter():
61
 
        if header is not None:
62
 
            yield header + '\n'
63
 
        first_stanza = True
64
 
        for s in stanzas:
65
 
            if first_stanza is not True:
66
 
                yield '\n'
67
 
            for line in s.to_lines():
68
 
                yield line
69
 
            first_stanza = False
70
 
    return IterableFile(str_iter())
71
 
 
72
 
 
73
 
def read_stanzas(from_file):
74
 
    while True:
75
 
        s = read_stanza(from_file)
76
 
        if s is None:
77
 
            break
78
 
        else:
79
 
            yield s
80
 
 
81
 
class Stanza(object):
82
 
    """One stanza for rio.
83
 
 
84
 
    Each stanza contains a set of named fields.  
85
 
    
86
 
    Names must be non-empty ascii alphanumeric plus _.  Names can be repeated
87
 
    within a stanza.  Names are case-sensitive.  The ordering of fields is
88
 
    preserved.
89
 
 
90
 
    Each field value must be either an int or a string.
91
 
    """
92
 
 
93
 
    __slots__ = ['items']
94
 
 
95
 
    def __init__(self, **kwargs):
96
 
        """Construct a new Stanza.
97
 
 
98
 
        The keyword arguments, if any, are added in sorted order to the stanza.
99
 
        """
100
 
        self.items = []
101
 
        if kwargs:
102
 
            for tag, value in sorted(kwargs.items()):
103
 
                self.add(tag, value)
104
 
 
105
 
    def add(self, tag, value):
106
 
        """Append a name and value to the stanza."""
107
 
        assert valid_tag(tag), \
108
 
            ("invalid tag %r" % tag)
109
 
        if isinstance(value, str):
110
 
            value = unicode(value)
111
 
        elif isinstance(value, unicode):
112
 
            pass
113
 
        ## elif isinstance(value, (int, long)):
114
 
        ##    value = str(value)           # XXX: python2.4 without L-suffix
115
 
        else:
116
 
            raise TypeError("invalid type for rio value: %r of type %s"
117
 
                            % (value, type(value)))
118
 
        self.items.append((tag, value))
119
 
        
120
 
    def __contains__(self, find_tag):
121
 
        """True if there is any field in this stanza with the given tag."""
122
 
        for tag, value in self.items:
123
 
            if tag == find_tag:
124
 
                return True
125
 
        return False
126
 
 
127
 
    def __len__(self):
128
 
        """Return number of pairs in the stanza."""
129
 
        return len(self.items)
130
 
 
131
 
    def __eq__(self, other):
132
 
        if not isinstance(other, Stanza):
133
 
            return False
134
 
        return self.items == other.items
135
 
 
136
 
    def __ne__(self, other):
137
 
        return not self.__eq__(other)
138
 
 
139
 
    def __repr__(self):
140
 
        return "Stanza(%r)" % self.items
141
 
 
142
 
    def iter_pairs(self):
143
 
        """Return iterator of tag, value pairs."""
144
 
        return iter(self.items)
145
 
 
146
 
    def to_lines(self):
147
 
        """Generate sequence of lines for external version of this file.
148
 
        
149
 
        The lines are always utf-8 encoded strings.
150
 
        """
151
 
        if not self.items:
152
 
            # max() complains if sequence is empty
153
 
            return []
154
 
        result = []
155
 
        for tag, value in self.items:
156
 
            assert isinstance(tag, str), type(tag)
157
 
            assert isinstance(value, unicode)
158
 
            if value == '':
159
 
                result.append(tag + ': \n')
160
 
            elif '\n' in value:
161
 
                # don't want splitlines behaviour on empty lines
162
 
                val_lines = value.split('\n')
163
 
                result.append(tag + ': ' + val_lines[0].encode('utf-8') + '\n')
164
 
                for line in val_lines[1:]:
165
 
                    result.append('\t' + line.encode('utf-8') + '\n')
166
 
            else:
167
 
                result.append(tag + ': ' + value.encode('utf-8') + '\n')
168
 
        return result
169
 
 
170
 
    def to_string(self):
171
 
        """Return stanza as a single string"""
172
 
        return ''.join(self.to_lines())
173
 
 
174
 
    def to_unicode(self):
175
 
        """Return stanza as a single Unicode string.
176
 
 
177
 
        This is most useful when adding a Stanza to a parent Stanza
178
 
        """
179
 
        if not self.items:
180
 
            return u''
181
 
 
182
 
        result = []
183
 
        for tag, value in self.items:
184
 
            if value == '':
185
 
                result.append(tag + ': \n')
186
 
            elif '\n' in value:
187
 
                # don't want splitlines behaviour on empty lines
188
 
                val_lines = value.split('\n')
189
 
                result.append(tag + ': ' + val_lines[0] + '\n')
190
 
                for line in val_lines[1:]:
191
 
                    result.append('\t' + line + '\n')
192
 
            else:
193
 
                result.append(tag + ': ' + value + '\n')
194
 
        return u''.join(result)
195
 
 
196
 
    def write(self, to_file):
197
 
        """Write stanza to a file"""
198
 
        to_file.writelines(self.to_lines())
199
 
 
200
 
    def get(self, tag):
201
 
        """Return the value for a field wih given tag.
202
 
 
203
 
        If there is more than one value, only the first is returned.  If the
204
 
        tag is not present, KeyError is raised.
205
 
        """
206
 
        for t, v in self.items:
207
 
            if t == tag:
208
 
                return v
209
 
        else:
210
 
            raise KeyError(tag)
211
 
 
212
 
    __getitem__ = get
213
 
 
214
 
    def get_all(self, tag):
215
 
        r = []
216
 
        for t, v in self.items:
217
 
            if t == tag:
218
 
                r.append(v)
219
 
        return r
220
 
 
221
 
    def as_dict(self):
222
 
        """Return a dict containing the unique values of the stanza.
223
 
        """
224
 
        d = {}
225
 
        for tag, value in self.items:
226
 
            assert tag not in d
227
 
            d[tag] = value
228
 
        return d
229
 
         
230
 
_tag_re = re.compile(r'^[-a-zA-Z0-9_]+$')
231
 
def valid_tag(tag):
232
 
    return bool(_tag_re.match(tag))
233
 
 
234
 
 
235
 
def read_stanza(line_iter):
236
 
    """Return new Stanza read from list of lines or a file
237
 
    
238
 
    Returns one Stanza that was read, or returns None at end of file.  If a
239
 
    blank line follows the stanza, it is consumed.  It's not an error for
240
 
    there to be no blank at end of file.  If there is a blank file at the
241
 
    start of the input this is really an empty stanza and that is returned. 
242
 
 
243
 
    Only the stanza lines and the trailing blank (if any) are consumed
244
 
    from the line_iter.
245
 
 
246
 
    The raw lines must be in utf-8 encoding.
247
 
    """
248
 
    unicode_iter = (line.decode('utf-8') for line in line_iter)
249
 
    return read_stanza_unicode(unicode_iter)
250
 
 
251
 
 
252
 
def read_stanza_unicode(unicode_iter):
253
 
    """Read a Stanza from a list of lines or a file.
254
 
 
255
 
    The lines should already be in unicode form. This returns a single
256
 
    stanza that was read. If there is a blank line at the end of the Stanza,
257
 
    it is consumed. It is not an error for there to be no blank line at
258
 
    the end of the iterable. If there is a blank line at the beginning,
259
 
    this is treated as an empty Stanza and None is returned.
260
 
 
261
 
    Only the stanza lines and the trailing blank (if any) are consumed
262
 
    from the unicode_iter
263
 
 
264
 
    :param unicode_iter: A iterable, yeilding Unicode strings. See read_stanza
265
 
        if you have a utf-8 encoded string.
266
 
    :return: A Stanza object if there are any lines in the file.
267
 
        None otherwise
268
 
    """
269
 
    stanza = Stanza()
270
 
    tag = None
271
 
    accum_value = None
272
 
    
273
 
    # TODO: jam 20060922 This code should raise real errors rather than
274
 
    #       using 'assert' to process user input, or raising ValueError
275
 
    #       rather than a more specific error.
276
 
 
277
 
    for line in unicode_iter:
278
 
        if line is None or line == '':
279
 
            break       # end of file
280
 
        if line == '\n':
281
 
            break       # end of stanza
282
 
        assert line.endswith('\n')
283
 
        real_l = line
284
 
        if line[0] == '\t': # continues previous value
285
 
            if tag is None:
286
 
                raise ValueError('invalid continuation line %r' % real_l)
287
 
            accum_value += '\n' + line[1:-1]
288
 
        else: # new tag:value line
289
 
            if tag is not None:
290
 
                stanza.add(tag, accum_value)
291
 
            try:
292
 
                colon_index = line.index(': ')
293
 
            except ValueError:
294
 
                raise ValueError('tag/value separator not found in line %r'
295
 
                                 % real_l)
296
 
            tag = str(line[:colon_index])
297
 
            assert valid_tag(tag), \
298
 
                    "invalid rio tag %r" % tag
299
 
            accum_value = line[colon_index+2:-1]
300
 
 
301
 
    if tag is not None: # add last tag-value
302
 
        stanza.add(tag, accum_value)
303
 
        return stanza
304
 
    else:     # didn't see any content
305
 
        return None