/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

MergeĀ fromĀ upstream.

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