/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: 2007-04-19 02:27:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2426.
  • Revision ID: robertc@robertcollins.net-20070419022744-pfdqz42kp1wizh43
``make docs`` now creates a man page at ``man1/bzr.1`` fixing bug 107388.
(Robert Collins)

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