/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/rio.py

  • Committer: Robert Collins
  • Date: 2005-12-24 02:20:45 UTC
  • mto: (1185.50.57 bzr-jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1550.
  • Revision ID: robertc@robertcollins.net-20051224022045-14efc8dfa0e1a4e9
Start tests for api usage.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
 
1
# Copyright (C) 2005 by Canonical Ltd
 
2
#
 
3
# Distributed under the GNU General Public Licence v2
 
4
#
17
5
# \subsection{\emph{rio} - simple text metaformat}
18
 
#
 
6
19
7
# \emph{r} stands for `restricted', `reproducible', or `rfc822-like'.
20
 
#
 
8
21
9
# The stored data consists of a series of \emph{stanzas}, each of which contains
22
10
# \emph{fields} identified by an ascii name, with Unicode or string contents.
23
 
# The field tag is constrained to alphanumeric characters.
 
11
# The field tag is constrained to alphanumeric characters.  
24
12
# There may be more than one field in a stanza with the same name.
25
 
#
 
13
26
14
# The format itself does not deal with character encoding issues, though
27
15
# the result will normally be written in Unicode.
28
 
#
 
16
29
17
# The format is intended to be simple enough that there is exactly one character
30
18
# stream representation of an object and vice versa, and that this relation
31
19
# will continue to hold for future versions of bzr.
32
20
 
 
21
# In comments, $\min(1,10)$
 
22
 
 
23
min(1,10)
 
24
 
33
25
import re
34
26
 
35
 
from . import osutils
36
 
from .iterablefile import IterableFile
37
 
 
38
27
# XXX: some redundancy is allowing to write stanzas in isolation as well as
39
 
# through a writer object.
40
 
 
 
28
# through a writer object.  
41
29
 
42
30
class RioWriter(object):
43
 
 
44
31
    def __init__(self, to_file):
45
32
        self._soft_nl = False
46
33
        self._to_file = to_file
47
34
 
48
35
    def write_stanza(self, stanza):
49
36
        if self._soft_nl:
50
 
            self._to_file.write(b'\n')
 
37
            print >>self._to_file
51
38
        stanza.write(self._to_file)
52
39
        self._soft_nl = True
53
40
 
54
41
 
55
42
class RioReader(object):
56
43
    """Read stanzas from a file as a sequence
57
 
 
58
 
    to_file can be anything that can be enumerated as a sequence of
 
44
    
 
45
    to_file can be anything that can be enumerated as a sequence of 
59
46
    lines (with newlines.)
60
47
    """
61
 
 
62
48
    def __init__(self, from_file):
63
49
        self._from_file = from_file
64
50
 
70
56
            else:
71
57
                yield s
72
58
 
73
 
 
74
 
def rio_file(stanzas, header=None):
75
 
    """Produce a rio IterableFile from an iterable of stanzas"""
76
 
    def str_iter():
77
 
        if header is not None:
78
 
            yield header + b'\n'
79
 
        first_stanza = True
80
 
        for s in stanzas:
81
 
            if first_stanza is not True:
82
 
                yield b'\n'
83
 
            for line in s.to_lines():
84
 
                yield line
85
 
            first_stanza = False
86
 
    return IterableFile(str_iter())
87
 
 
88
 
 
89
59
def read_stanzas(from_file):
90
 
 
91
60
    while True:
92
61
        s = read_stanza(from_file)
93
62
        if s is None:
94
63
            break
95
 
        yield s
96
 
 
97
 
 
98
 
def read_stanzas_unicode(from_file):
99
 
 
100
 
    while True:
101
 
        s = read_stanza_unicode(from_file)
102
 
        if s is None:
103
 
            break
104
 
        yield s
105
 
 
 
64
        else:
 
65
            yield s
106
66
 
107
67
class Stanza(object):
108
68
    """One stanza for rio.
109
69
 
110
 
    Each stanza contains a set of named fields.
111
 
 
 
70
    Each stanza contains a set of named fields.  
 
71
    
112
72
    Names must be non-empty ascii alphanumeric plus _.  Names can be repeated
113
73
    within a stanza.  Names are case-sensitive.  The ordering of fields is
114
74
    preserved.
130
90
 
131
91
    def add(self, tag, value):
132
92
        """Append a name and value to the stanza."""
133
 
        if not valid_tag(tag):
134
 
            raise ValueError("invalid tag %r" % (tag,))
135
 
        if isinstance(value, bytes):
136
 
            value = value.decode('ascii')
137
 
        elif isinstance(value, str):
 
93
        assert valid_tag(tag), \
 
94
            ("invalid tag %r" % tag)
 
95
        if isinstance(value, (str, unicode)):
138
96
            pass
 
97
        ## elif isinstance(value, (int, long)):
 
98
        ##    value = str(value)           # XXX: python2.4 without L-suffix
139
99
        else:
140
 
            raise TypeError("invalid type for rio value: %r of type %s"
141
 
                            % (value, type(value)))
 
100
            raise ValueError("invalid value %r" % value)
142
101
        self.items.append((tag, value))
143
 
 
144
 
    @classmethod
145
 
    def from_pairs(cls, pairs):
146
 
        ret = cls()
147
 
        ret.items = pairs
148
 
        return ret
149
 
 
 
102
        
150
103
    def __contains__(self, find_tag):
151
104
        """True if there is any field in this stanza with the given tag."""
152
105
        for tag, value in self.items:
174
127
        return iter(self.items)
175
128
 
176
129
    def to_lines(self):
177
 
        """Generate sequence of lines for external version of this file.
178
 
 
179
 
        The lines are always utf-8 encoded strings.
180
 
        """
 
130
        """Generate sequence of lines for external version of this file."""
181
131
        if not self.items:
182
132
            # max() complains if sequence is empty
183
133
            return []
184
134
        result = []
185
 
        for text_tag, text_value in self.items:
186
 
            tag = text_tag.encode('ascii')
187
 
            value = text_value.encode('utf-8', 'surrogateescape')
188
 
            if value == b'':
189
 
                result.append(tag + b': \n')
190
 
            elif b'\n' in value:
 
135
        for tag, value in self.items:
 
136
            assert isinstance(value, (str, unicode))
 
137
            if value == '':
 
138
                result.append(tag + ': \n')
 
139
            elif '\n' in value:
191
140
                # don't want splitlines behaviour on empty lines
192
 
                val_lines = value.split(b'\n')
193
 
                result.append(tag + b': ' + val_lines[0] + b'\n')
 
141
                val_lines = value.split('\n')
 
142
                result.append(tag + ': ' + val_lines[0] + '\n')
194
143
                for line in val_lines[1:]:
195
 
                    result.append(b'\t' + line + b'\n')
 
144
                    result.append('\t' + line + '\n')
196
145
            else:
197
 
                result.append(tag + b': ' + value + b'\n')
 
146
                result.append(tag + ': ' + value + '\n')
198
147
        return result
199
148
 
200
149
    def to_string(self):
201
150
        """Return stanza as a single string"""
202
 
        return b''.join(self.to_lines())
203
 
 
204
 
    def to_unicode(self):
205
 
        """Return stanza as a single Unicode string.
206
 
 
207
 
        This is most useful when adding a Stanza to a parent Stanza
208
 
        """
209
 
        if not self.items:
210
 
            return u''
211
 
 
212
 
        result = []
213
 
        for tag, value in self.items:
214
 
            if value == u'':
215
 
                result.append(tag + u': \n')
216
 
            elif u'\n' in value:
217
 
                # don't want splitlines behaviour on empty lines
218
 
                val_lines = value.split(u'\n')
219
 
                result.append(tag + u': ' + val_lines[0] + u'\n')
220
 
                for line in val_lines[1:]:
221
 
                    result.append(u'\t' + line + u'\n')
222
 
            else:
223
 
                result.append(tag + u': ' + value + u'\n')
224
 
        return u''.join(result)
 
151
        return ''.join(self.to_lines())
225
152
 
226
153
    def write(self, to_file):
227
154
        """Write stanza to a file"""
247
174
            if t == tag:
248
175
                r.append(v)
249
176
        return r
250
 
 
251
 
    def as_dict(self):
252
 
        """Return a dict containing the unique values of the stanza.
253
 
        """
254
 
        d = {}
255
 
        for tag, value in self.items:
256
 
            d[tag] = value
257
 
        return d
258
 
 
259
 
 
 
177
         
 
178
_tag_re = re.compile(r'^[-a-zA-Z0-9_]+$')
260
179
def valid_tag(tag):
261
 
    return _valid_tag(tag)
 
180
    return bool(_tag_re.match(tag))
262
181
 
263
182
 
264
183
def read_stanza(line_iter):
265
184
    """Return new Stanza read from list of lines or a file
266
 
 
 
185
    
267
186
    Returns one Stanza that was read, or returns None at end of file.  If a
268
187
    blank line follows the stanza, it is consumed.  It's not an error for
269
188
    there to be no blank at end of file.  If there is a blank file at the
270
 
    start of the input this is really an empty stanza and that is returned.
 
189
    start of the input this is really an empty stanza and that is returned. 
271
190
 
272
191
    Only the stanza lines and the trailing blank (if any) are consumed
273
192
    from the line_iter.
274
 
 
275
 
    The raw lines must be in utf-8 encoding.
276
 
    """
277
 
    return _read_stanza_utf8(line_iter)
278
 
 
279
 
 
280
 
def read_stanza_unicode(unicode_iter):
281
 
    """Read a Stanza from a list of lines or a file.
282
 
 
283
 
    The lines should already be in unicode form. This returns a single
284
 
    stanza that was read. If there is a blank line at the end of the Stanza,
285
 
    it is consumed. It is not an error for there to be no blank line at
286
 
    the end of the iterable. If there is a blank line at the beginning,
287
 
    this is treated as an empty Stanza and None is returned.
288
 
 
289
 
    Only the stanza lines and the trailing blank (if any) are consumed
290
 
    from the unicode_iter
291
 
 
292
 
    :param unicode_iter: A iterable, yeilding Unicode strings. See read_stanza
293
 
        if you have a utf-8 encoded string.
294
 
    :return: A Stanza object if there are any lines in the file.
295
 
        None otherwise
296
 
    """
297
 
    return _read_stanza_unicode(unicode_iter)
298
 
 
299
 
 
300
 
def to_patch_lines(stanza, max_width=72):
301
 
    """Convert a stanza into RIO-Patch format lines.
302
 
 
303
 
    RIO-Patch is a RIO variant designed to be e-mailed as part of a patch.
304
 
    It resists common forms of damage such as newline conversion or the removal
305
 
    of trailing whitespace, yet is also reasonably easy to read.
306
 
 
307
 
    :param max_width: The maximum number of characters per physical line.
308
 
    :return: a list of lines
309
 
    """
310
 
    if max_width <= 6:
311
 
        raise ValueError(max_width)
312
 
    max_rio_width = max_width - 4
313
 
    lines = []
314
 
    for pline in stanza.to_lines():
315
 
        for line in pline.split(b'\n')[:-1]:
316
 
            line = re.sub(b'\\\\', b'\\\\\\\\', line)
317
 
            while len(line) > 0:
318
 
                partline = line[:max_rio_width]
319
 
                line = line[max_rio_width:]
320
 
                if len(line) > 0 and line[:1] != [b' ']:
321
 
                    break_index = -1
322
 
                    break_index = partline.rfind(b' ', -20)
323
 
                    if break_index < 3:
324
 
                        break_index = partline.rfind(b'-', -20)
325
 
                        break_index += 1
326
 
                    if break_index < 3:
327
 
                        break_index = partline.rfind(b'/', -20)
328
 
                    if break_index >= 3:
329
 
                        line = partline[break_index:] + line
330
 
                        partline = partline[:break_index]
331
 
                if len(line) > 0:
332
 
                    line = b'  ' + line
333
 
                partline = re.sub(b'\r', b'\\\\r', partline)
334
 
                blank_line = False
335
 
                if len(line) > 0:
336
 
                    partline += b'\\'
337
 
                elif re.search(b' $', partline):
338
 
                    partline += b'\\'
339
 
                    blank_line = True
340
 
                lines.append(b'# ' + partline + b'\n')
341
 
                if blank_line:
342
 
                    lines.append(b'#   \n')
343
 
    return lines
344
 
 
345
 
 
346
 
def _patch_stanza_iter(line_iter):
347
 
    map = {b'\\\\': b'\\',
348
 
           b'\\r': b'\r',
349
 
           b'\\\n': b''}
350
 
 
351
 
    def mapget(match):
352
 
        return map[match.group(0)]
353
 
 
354
 
    last_line = None
 
193
    """
 
194
    items = []
 
195
    stanza = Stanza()
 
196
    tag = None
 
197
    accum_value = None
355
198
    for line in line_iter:
356
 
        if line.startswith(b'# '):
357
 
            line = line[2:]
358
 
        elif line.startswith(b'#'):
359
 
            line = line[1:]
360
 
        else:
361
 
            raise ValueError("bad line %r" % (line,))
362
 
        if last_line is not None and len(line) > 2:
363
 
            line = line[2:]
364
 
        line = re.sub(b'\r', b'', line)
365
 
        line = re.sub(b'\\\\(.|\n)', mapget, line)
366
 
        if last_line is None:
367
 
            last_line = line
368
 
        else:
369
 
            last_line += line
370
 
        if last_line[-1:] == b'\n':
371
 
            yield last_line
372
 
            last_line = None
373
 
    if last_line is not None:
374
 
        yield last_line
375
 
 
376
 
 
377
 
def read_patch_stanza(line_iter):
378
 
    """Convert an iterable of RIO-Patch format lines into a Stanza.
379
 
 
380
 
    RIO-Patch is a RIO variant designed to be e-mailed as part of a patch.
381
 
    It resists common forms of damage such as newline conversion or the removal
382
 
    of trailing whitespace, yet is also reasonably easy to read.
383
 
 
384
 
    :return: a Stanza
385
 
    """
386
 
    return read_stanza(_patch_stanza_iter(line_iter))
387
 
 
388
 
 
389
 
try:
390
 
    from ._rio_pyx import (
391
 
        _read_stanza_utf8,
392
 
        _read_stanza_unicode,
393
 
        _valid_tag,
394
 
        )
395
 
except ImportError as e:
396
 
    osutils.failed_to_load_extension(e)
397
 
    from ._rio_py import (
398
 
        _read_stanza_utf8,
399
 
        _read_stanza_unicode,
400
 
        _valid_tag,
401
 
        )
 
199
        if line == None or line == '':
 
200
            break       # end of file
 
201
        if line == '\n':
 
202
            break       # end of stanza
 
203
        assert line[-1] == '\n'
 
204
        real_l = line
 
205
        if line[0] == '\t': # continues previous value
 
206
            if tag is None:
 
207
                raise ValueError('invalid continuation line %r' % real_l)
 
208
            accum_value += '\n' + line[1:-1]
 
209
        else: # new tag:value line
 
210
            if tag is not None:
 
211
                stanza.add(tag, accum_value)
 
212
            try:
 
213
                colon_index = line.index(': ')
 
214
            except ValueError:
 
215
                raise ValueError('tag/value separator not found in line %r' % real_l)
 
216
            tag = line[:colon_index]
 
217
            assert valid_tag(tag), \
 
218
                    "invalid rio tag %r" % tag
 
219
            accum_value = line[colon_index+2:-1]
 
220
    if tag is not None: # add last tag-value
 
221
        stanza.add(tag, accum_value)
 
222
        return stanza
 
223
    else:     # didn't see any content
 
224
        return None