/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 breezy/rio.py

  • Committer: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

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