/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 bzr.dev, resolving conflicts.

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
16
 
19
17
# \subsection{\emph{rio} - simple text metaformat}
20
 
#
 
18
21
19
# \emph{r} stands for `restricted', `reproducible', or `rfc822-like'.
22
 
#
 
20
23
21
# The stored data consists of a series of \emph{stanzas}, each of which contains
24
22
# \emph{fields} identified by an ascii name, with Unicode or string contents.
25
 
# The field tag is constrained to alphanumeric characters.
 
23
# The field tag is constrained to alphanumeric characters.  
26
24
# There may be more than one field in a stanza with the same name.
27
 
#
 
25
28
26
# The format itself does not deal with character encoding issues, though
29
27
# the result will normally be written in Unicode.
30
 
#
 
28
31
29
# The format is intended to be simple enough that there is exactly one character
32
30
# stream representation of an object and vice versa, and that this relation
33
31
# will continue to hold for future versions of bzr.
34
32
 
35
33
import re
36
34
 
37
 
from . import osutils
38
 
from .iterablefile import IterableFile
39
 
from .sixish import (
40
 
    text_type,
41
 
    )
 
35
from bzrlib.iterablefile import IterableFile
42
36
 
43
37
# XXX: some redundancy is allowing to write stanzas in isolation as well as
44
 
# through a writer object.
 
38
# through a writer object.  
45
39
 
46
40
class RioWriter(object):
47
41
    def __init__(self, to_file):
50
44
 
51
45
    def write_stanza(self, stanza):
52
46
        if self._soft_nl:
53
 
            self._to_file.write(b'\n')
 
47
            print >>self._to_file
54
48
        stanza.write(self._to_file)
55
49
        self._soft_nl = True
56
50
 
57
51
 
58
52
class RioReader(object):
59
53
    """Read stanzas from a file as a sequence
60
 
 
61
 
    to_file can be anything that can be enumerated as a sequence of
 
54
    
 
55
    to_file can be anything that can be enumerated as a sequence of 
62
56
    lines (with newlines.)
63
57
    """
64
58
    def __init__(self, from_file):
77
71
    """Produce a rio IterableFile from an iterable of stanzas"""
78
72
    def str_iter():
79
73
        if header is not None:
80
 
            yield header + b'\n'
 
74
            yield header + '\n'
81
75
        first_stanza = True
82
76
        for s in stanzas:
83
77
            if first_stanza is not True:
84
 
                yield b'\n'
 
78
                yield '\n'
85
79
            for line in s.to_lines():
86
80
                yield line
87
81
            first_stanza = False
99
93
class Stanza(object):
100
94
    """One stanza for rio.
101
95
 
102
 
    Each stanza contains a set of named fields.
103
 
 
 
96
    Each stanza contains a set of named fields.  
 
97
    
104
98
    Names must be non-empty ascii alphanumeric plus _.  Names can be repeated
105
99
    within a stanza.  Names are case-sensitive.  The ordering of fields is
106
100
    preserved.
122
116
 
123
117
    def add(self, tag, value):
124
118
        """Append a name and value to the stanza."""
125
 
        if not valid_tag(tag):
126
 
            raise ValueError("invalid tag %r" % (tag,))
127
 
        if isinstance(value, bytes):
128
 
            value = value.decode('ascii')
129
 
        elif isinstance(value, text_type):
 
119
        assert valid_tag(tag), \
 
120
            ("invalid tag %r" % tag)
 
121
        if isinstance(value, str):
 
122
            value = unicode(value)
 
123
        elif isinstance(value, unicode):
130
124
            pass
 
125
        ## elif isinstance(value, (int, long)):
 
126
        ##    value = str(value)           # XXX: python2.4 without L-suffix
131
127
        else:
132
128
            raise TypeError("invalid type for rio value: %r of type %s"
133
129
                            % (value, type(value)))
134
130
        self.items.append((tag, value))
135
 
 
136
 
    @classmethod
137
 
    def from_pairs(cls, pairs):
138
 
        ret = cls()
139
 
        ret.items = pairs
140
 
        return ret
141
 
 
 
131
        
142
132
    def __contains__(self, find_tag):
143
133
        """True if there is any field in this stanza with the given tag."""
144
134
        for tag, value in self.items:
167
157
 
168
158
    def to_lines(self):
169
159
        """Generate sequence of lines for external version of this file.
170
 
 
 
160
        
171
161
        The lines are always utf-8 encoded strings.
172
162
        """
173
163
        if not self.items:
174
164
            # max() complains if sequence is empty
175
165
            return []
176
166
        result = []
177
 
        for text_tag, text_value in self.items:
178
 
            tag = text_tag.encode('ascii')
179
 
            value = text_value.encode('utf-8')
180
 
            if value == b'':
181
 
                result.append(tag + b': \n')
182
 
            elif b'\n' in value:
 
167
        for tag, value in self.items:
 
168
            assert isinstance(tag, str), type(tag)
 
169
            assert isinstance(value, unicode)
 
170
            if value == '':
 
171
                result.append(tag + ': \n')
 
172
            elif '\n' in value:
183
173
                # don't want splitlines behaviour on empty lines
184
 
                val_lines = value.split(b'\n')
185
 
                result.append(tag + b': ' + val_lines[0] + b'\n')
 
174
                val_lines = value.split('\n')
 
175
                result.append(tag + ': ' + val_lines[0].encode('utf-8') + '\n')
186
176
                for line in val_lines[1:]:
187
 
                    result.append(b'\t' + line + b'\n')
 
177
                    result.append('\t' + line.encode('utf-8') + '\n')
188
178
            else:
189
 
                result.append(tag + b': ' + value + b'\n')
 
179
                result.append(tag + ': ' + value.encode('utf-8') + '\n')
190
180
        return result
191
181
 
192
182
    def to_string(self):
193
183
        """Return stanza as a single string"""
194
 
        return b''.join(self.to_lines())
 
184
        return ''.join(self.to_lines())
195
185
 
196
186
    def to_unicode(self):
197
187
        """Return stanza as a single Unicode string.
203
193
 
204
194
        result = []
205
195
        for tag, value in self.items:
206
 
            if value == u'':
207
 
                result.append(tag + u': \n')
208
 
            elif u'\n' in value:
 
196
            if value == '':
 
197
                result.append(tag + ': \n')
 
198
            elif '\n' in value:
209
199
                # don't want splitlines behaviour on empty lines
210
 
                val_lines = value.split(u'\n')
211
 
                result.append(tag + u': ' + val_lines[0] + u'\n')
 
200
                val_lines = value.split('\n')
 
201
                result.append(tag + ': ' + val_lines[0] + '\n')
212
202
                for line in val_lines[1:]:
213
 
                    result.append(u'\t' + line + u'\n')
 
203
                    result.append('\t' + line + '\n')
214
204
            else:
215
 
                result.append(tag + u': ' + value + u'\n')
 
205
                result.append(tag + ': ' + value + '\n')
216
206
        return u''.join(result)
217
207
 
218
208
    def write(self, to_file):
245
235
        """
246
236
        d = {}
247
237
        for tag, value in self.items:
 
238
            assert tag not in d
248
239
            d[tag] = value
249
240
        return d
250
 
 
251
 
 
 
241
         
 
242
_tag_re = re.compile(r'^[-a-zA-Z0-9_]+$')
252
243
def valid_tag(tag):
253
 
    return _valid_tag(tag)
 
244
    return bool(_tag_re.match(tag))
254
245
 
255
246
 
256
247
def read_stanza(line_iter):
257
248
    """Return new Stanza read from list of lines or a file
258
 
 
 
249
    
259
250
    Returns one Stanza that was read, or returns None at end of file.  If a
260
251
    blank line follows the stanza, it is consumed.  It's not an error for
261
252
    there to be no blank at end of file.  If there is a blank file at the
262
 
    start of the input this is really an empty stanza and that is returned.
 
253
    start of the input this is really an empty stanza and that is returned. 
263
254
 
264
255
    Only the stanza lines and the trailing blank (if any) are consumed
265
256
    from the line_iter.
266
257
 
267
258
    The raw lines must be in utf-8 encoding.
268
259
    """
269
 
    return _read_stanza_utf8(line_iter)
 
260
    unicode_iter = (line.decode('utf-8') for line in line_iter)
 
261
    return read_stanza_unicode(unicode_iter)
270
262
 
271
263
 
272
264
def read_stanza_unicode(unicode_iter):
286
278
    :return: A Stanza object if there are any lines in the file.
287
279
        None otherwise
288
280
    """
289
 
    return _read_stanza_unicode(unicode_iter)
 
281
    stanza = Stanza()
 
282
    tag = None
 
283
    accum_value = None
 
284
    
 
285
    # TODO: jam 20060922 This code should raise real errors rather than
 
286
    #       using 'assert' to process user input, or raising ValueError
 
287
    #       rather than a more specific error.
 
288
 
 
289
    for line in unicode_iter:
 
290
        if line is None or line == '':
 
291
            break       # end of file
 
292
        if line == '\n':
 
293
            break       # end of stanza
 
294
        assert line.endswith('\n')
 
295
        real_l = line
 
296
        if line[0] == '\t': # continues previous value
 
297
            if tag is None:
 
298
                raise ValueError('invalid continuation line %r' % real_l)
 
299
            accum_value += '\n' + line[1:-1]
 
300
        else: # new tag:value line
 
301
            if tag is not None:
 
302
                stanza.add(tag, accum_value)
 
303
            try:
 
304
                colon_index = line.index(': ')
 
305
            except ValueError:
 
306
                raise ValueError('tag/value separator not found in line %r'
 
307
                                 % real_l)
 
308
            tag = str(line[:colon_index])
 
309
            assert valid_tag(tag), \
 
310
                    "invalid rio tag %r" % tag
 
311
            accum_value = line[colon_index+2:-1]
 
312
 
 
313
    if tag is not None: # add last tag-value
 
314
        stanza.add(tag, accum_value)
 
315
        return stanza
 
316
    else:     # didn't see any content
 
317
        return None    
290
318
 
291
319
 
292
320
def to_patch_lines(stanza, max_width=72):
299
327
    :param max_width: The maximum number of characters per physical line.
300
328
    :return: a list of lines
301
329
    """
302
 
    if max_width <= 6:
303
 
        raise ValueError(max_width)
 
330
    assert max_width > 6
304
331
    max_rio_width = max_width - 4
305
332
    lines = []
306
333
    for pline in stanza.to_lines():
307
 
        for line in pline.split(b'\n')[:-1]:
308
 
            line = re.sub(b'\\\\', b'\\\\\\\\', line)
 
334
        for line in pline.split('\n')[:-1]:
 
335
            line = re.sub('\\\\', '\\\\\\\\', line)
309
336
            while len(line) > 0:
310
337
                partline = line[:max_rio_width]
311
338
                line = line[max_rio_width:]
312
 
                if len(line) > 0 and line[:1] != [b' ']:
 
339
                if len(line) > 0 and line[0] != [' ']:
313
340
                    break_index = -1
314
 
                    break_index = partline.rfind(b' ', -20)
 
341
                    break_index = partline.rfind(' ', -20)
315
342
                    if break_index < 3:
316
 
                        break_index = partline.rfind(b'-', -20)
 
343
                        break_index = partline.rfind('-', -20)
317
344
                        break_index += 1
318
345
                    if break_index < 3:
319
 
                        break_index = partline.rfind(b'/', -20)
 
346
                        break_index = partline.rfind('/', -20)
320
347
                    if break_index >= 3:
321
348
                        line = partline[break_index:] + line
322
349
                        partline = partline[:break_index]
323
350
                if len(line) > 0:
324
 
                    line = b'  ' + line
325
 
                partline = re.sub(b'\r', b'\\\\r', partline)
 
351
                    line = '  ' + line
 
352
                partline = re.sub('\r', '\\\\r', partline)
326
353
                blank_line = False
327
354
                if len(line) > 0:
328
 
                    partline += b'\\'
329
 
                elif re.search(b' $', partline):
330
 
                    partline += b'\\'
 
355
                    partline += '\\'
 
356
                elif re.search(' $', partline):
 
357
                    partline += '\\'
331
358
                    blank_line = True
332
 
                lines.append(b'# ' + partline + b'\n')
 
359
                lines.append('# ' + partline + '\n')
333
360
                if blank_line:
334
 
                    lines.append(b'#   \n')
 
361
                    lines.append('#   \n')
335
362
    return lines
336
363
 
337
364
 
338
365
def _patch_stanza_iter(line_iter):
339
 
    map = {b'\\\\': b'\\',
340
 
           b'\\r' : b'\r',
341
 
           b'\\\n': b''}
 
366
    map = {'\\\\': '\\',
 
367
           '\\r' : '\r',
 
368
           '\\\n': ''}
342
369
    def mapget(match):
343
370
        return map[match.group(0)]
344
371
 
345
372
    last_line = None
346
373
    for line in line_iter:
347
 
        if line.startswith(b'# '):
 
374
        if line.startswith('# '):
348
375
            line = line[2:]
349
 
        elif line.startswith(b'#'):
 
376
        else:
 
377
            assert line.startswith('#')
350
378
            line = line[1:]
351
 
        else:
352
 
            raise ValueError("bad line %r" % (line,))
353
379
        if last_line is not None and len(line) > 2:
354
380
            line = line[2:]
355
 
        line = re.sub(b'\r', b'', line)
356
 
        line = re.sub(b'\\\\(.|\n)', mapget, line)
 
381
        line = re.sub('\r', '', line)
 
382
        line = re.sub('\\\\(.|\n)', mapget, line)
357
383
        if last_line is None:
358
384
            last_line = line
359
385
        else:
360
386
            last_line += line
361
 
        if last_line[-1:] == b'\n':
 
387
        if last_line[-1] == '\n':
362
388
            yield last_line
363
389
            last_line = None
364
390
    if last_line is not None:
375
401
    :return: a Stanza
376
402
    """
377
403
    return read_stanza(_patch_stanza_iter(line_iter))
378
 
 
379
 
 
380
 
try:
381
 
    from ._rio_pyx import (
382
 
        _read_stanza_utf8,
383
 
        _read_stanza_unicode,
384
 
        _valid_tag,
385
 
        )
386
 
except ImportError as e:
387
 
    osutils.failed_to_load_extension(e)
388
 
    from ._rio_py import (
389
 
       _read_stanza_utf8,
390
 
       _read_stanza_unicode,
391
 
       _valid_tag,
392
 
       )