comparison env/lib/python3.7/site-packages/docutils/io.py @ 5:9b1c78e6ba9c draft default tip

"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
author shellac
date Mon, 01 Jun 2020 08:59:25 -0400
parents 79f47841a781
children
comparison
equal deleted inserted replaced
4:79f47841a781 5:9b1c78e6ba9c
1 # $Id: io.py 8394 2019-09-18 10:13:17Z milde $
2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
4
5 """
6 I/O classes provide a uniform API for low-level input and output. Subclasses
7 exist for a variety of input/output mechanisms.
8 """
9 from __future__ import print_function
10
11 __docformat__ = 'reStructuredText'
12
13 import sys
14 import os
15 import re
16 import codecs
17 from docutils import TransformSpec
18 from docutils.utils.error_reporting import locale_encoding, ErrorString, ErrorOutput
19
20 if sys.version_info >= (3, 0):
21 unicode = str # noqa
22
23
24 class InputError(IOError): pass
25 class OutputError(IOError): pass
26
27 def check_encoding(stream, encoding):
28 """Test, whether the encoding of `stream` matches `encoding`.
29
30 Returns
31
32 :None: if `encoding` or `stream.encoding` are not a valid encoding
33 argument (e.g. ``None``) or `stream.encoding is missing.
34 :True: if the encoding argument resolves to the same value as `encoding`,
35 :False: if the encodings differ.
36 """
37 try:
38 return codecs.lookup(stream.encoding) == codecs.lookup(encoding)
39 except (LookupError, AttributeError, TypeError):
40 return None
41
42
43 class Input(TransformSpec):
44
45 """
46 Abstract base class for input wrappers.
47 """
48
49 component_type = 'input'
50
51 default_source_path = None
52
53 def __init__(self, source=None, source_path=None, encoding=None,
54 error_handler='strict'):
55 self.encoding = encoding
56 """Text encoding for the input source."""
57
58 self.error_handler = error_handler
59 """Text decoding error handler."""
60
61 self.source = source
62 """The source of input data."""
63
64 self.source_path = source_path
65 """A text reference to the source."""
66
67 if not source_path:
68 self.source_path = self.default_source_path
69
70 self.successful_encoding = None
71 """The encoding that successfully decoded the source data."""
72
73 def __repr__(self):
74 return '%s: source=%r, source_path=%r' % (self.__class__, self.source,
75 self.source_path)
76
77 def read(self):
78 raise NotImplementedError
79
80 def decode(self, data):
81 """
82 Decode a string, `data`, heuristically.
83 Raise UnicodeError if unsuccessful.
84
85 The client application should call ``locale.setlocale`` at the
86 beginning of processing::
87
88 locale.setlocale(locale.LC_ALL, '')
89 """
90 if self.encoding and self.encoding.lower() == 'unicode':
91 assert isinstance(data, unicode), (
92 'input encoding is "unicode" '
93 'but input is not a unicode object')
94 if isinstance(data, unicode):
95 # Accept unicode even if self.encoding != 'unicode'.
96 return data
97 if self.encoding:
98 # We believe the user/application when the encoding is
99 # explicitly given.
100 encodings = [self.encoding]
101 else:
102 data_encoding = self.determine_encoding_from_data(data)
103 if data_encoding:
104 # If the data declares its encoding (explicitly or via a BOM),
105 # we believe it.
106 encodings = [data_encoding]
107 else:
108 # Apply heuristics only if no encoding is explicitly given and
109 # no BOM found. Start with UTF-8, because that only matches
110 # data that *IS* UTF-8:
111 encodings = ['utf-8', 'latin-1']
112 if locale_encoding:
113 encodings.insert(1, locale_encoding)
114 for enc in encodings:
115 try:
116 decoded = unicode(data, enc, self.error_handler)
117 self.successful_encoding = enc
118 # Return decoded, removing BOMs.
119 return decoded.replace(u'\ufeff', u'')
120 except (UnicodeError, LookupError) as err:
121 error = err # in Python 3, the <exception instance> is
122 # local to the except clause
123 raise UnicodeError(
124 'Unable to decode input data. Tried the following encodings: '
125 '%s.\n(%s)' % (', '.join([repr(enc) for enc in encodings]),
126 ErrorString(error)))
127
128 coding_slug = re.compile(br"coding[:=]\s*([-\w.]+)")
129 """Encoding declaration pattern."""
130
131 byte_order_marks = ((codecs.BOM_UTF8, 'utf-8'),
132 (codecs.BOM_UTF16_BE, 'utf-16-be'),
133 (codecs.BOM_UTF16_LE, 'utf-16-le'),)
134 """Sequence of (start_bytes, encoding) tuples for encoding detection.
135 The first bytes of input data are checked against the start_bytes strings.
136 A match indicates the given encoding."""
137
138 def determine_encoding_from_data(self, data):
139 """
140 Try to determine the encoding of `data` by looking *in* `data`.
141 Check for a byte order mark (BOM) or an encoding declaration.
142 """
143 # check for a byte order mark:
144 for start_bytes, encoding in self.byte_order_marks:
145 if data.startswith(start_bytes):
146 return encoding
147 # check for an encoding declaration pattern in first 2 lines of file:
148 for line in data.splitlines()[:2]:
149 match = self.coding_slug.search(line)
150 if match:
151 return match.group(1).decode('ascii')
152 return None
153
154
155 class Output(TransformSpec):
156
157 """
158 Abstract base class for output wrappers.
159 """
160
161 component_type = 'output'
162
163 default_destination_path = None
164
165 def __init__(self, destination=None, destination_path=None,
166 encoding=None, error_handler='strict'):
167 self.encoding = encoding
168 """Text encoding for the output destination."""
169
170 self.error_handler = error_handler or 'strict'
171 """Text encoding error handler."""
172
173 self.destination = destination
174 """The destination for output data."""
175
176 self.destination_path = destination_path
177 """A text reference to the destination."""
178
179 if not destination_path:
180 self.destination_path = self.default_destination_path
181
182 def __repr__(self):
183 return ('%s: destination=%r, destination_path=%r'
184 % (self.__class__, self.destination, self.destination_path))
185
186 def write(self, data):
187 """`data` is a Unicode string, to be encoded by `self.encode`."""
188 raise NotImplementedError
189
190 def encode(self, data):
191 if self.encoding and self.encoding.lower() == 'unicode':
192 assert isinstance(data, unicode), (
193 'the encoding given is "unicode" but the output is not '
194 'a Unicode string')
195 return data
196 if not isinstance(data, unicode):
197 # Non-unicode (e.g. bytes) output.
198 return data
199 else:
200 return data.encode(self.encoding, self.error_handler)
201
202
203 class FileInput(Input):
204
205 """
206 Input for single, simple file-like objects.
207 """
208 def __init__(self, source=None, source_path=None,
209 encoding=None, error_handler='strict',
210 autoclose=True,
211 mode='r' if sys.version_info >= (3, 0) else 'rU'):
212 """
213 :Parameters:
214 - `source`: either a file-like object (which is read directly), or
215 `None` (which implies `sys.stdin` if no `source_path` given).
216 - `source_path`: a path to a file, which is opened and then read.
217 - `encoding`: the expected text encoding of the input file.
218 - `error_handler`: the encoding error handler to use.
219 - `autoclose`: close automatically after read (except when
220 `sys.stdin` is the source).
221 - `mode`: how the file is to be opened (see standard function
222 `open`). The default 'rU' provides universal newline support
223 for text files with Python 2.x.
224 """
225 Input.__init__(self, source, source_path, encoding, error_handler)
226 self.autoclose = autoclose
227 self._stderr = ErrorOutput()
228
229 if source is None:
230 if source_path:
231 # Specify encoding in Python 3
232 if sys.version_info >= (3, 0):
233 kwargs = {'encoding': self.encoding,
234 'errors': self.error_handler}
235 else:
236 kwargs = {}
237 try:
238 self.source = open(source_path, mode, **kwargs)
239 except IOError as error:
240 raise InputError(error.errno, error.strerror, source_path)
241 else:
242 self.source = sys.stdin
243 elif (sys.version_info >= (3, 0) and
244 check_encoding(self.source, self.encoding) is False):
245 # TODO: re-open, warn or raise error?
246 raise UnicodeError('Encoding clash: encoding given is "%s" '
247 'but source is opened with encoding "%s".' %
248 (self.encoding, self.source.encoding))
249 if not source_path:
250 try:
251 self.source_path = self.source.name
252 except AttributeError:
253 pass
254
255 def read(self):
256 """
257 Read and decode a single file and return the data (Unicode string).
258 """
259 try:
260 if self.source is sys.stdin and sys.version_info >= (3, 0):
261 # read as binary data to circumvent auto-decoding
262 data = self.source.buffer.read()
263 # normalize newlines
264 data = b'\n'.join(data.splitlines()) + b'\n'
265 else:
266 data = self.source.read()
267 except (UnicodeError, LookupError) as err: # (in Py3k read() decodes)
268 if not self.encoding and self.source_path:
269 # re-read in binary mode and decode with heuristics
270 b_source = open(self.source_path, 'rb')
271 data = b_source.read()
272 b_source.close()
273 # normalize newlines
274 data = b'\n'.join(data.splitlines()) + b'\n'
275 else:
276 raise
277 finally:
278 if self.autoclose:
279 self.close()
280 return self.decode(data)
281
282 def readlines(self):
283 """
284 Return lines of a single file as list of Unicode strings.
285 """
286 return self.read().splitlines(True)
287
288 def close(self):
289 if self.source is not sys.stdin:
290 self.source.close()
291
292
293 class FileOutput(Output):
294
295 """
296 Output for single, simple file-like objects.
297 """
298
299 mode = 'w'
300 """The mode argument for `open()`."""
301 # 'wb' for binary (e.g. OpenOffice) files (see also `BinaryFileOutput`).
302 # (Do not use binary mode ('wb') for text files, as this prevents the
303 # conversion of newlines to the system specific default.)
304
305 def __init__(self, destination=None, destination_path=None,
306 encoding=None, error_handler='strict', autoclose=True,
307 handle_io_errors=None, mode=None):
308 """
309 :Parameters:
310 - `destination`: either a file-like object (which is written
311 directly) or `None` (which implies `sys.stdout` if no
312 `destination_path` given).
313 - `destination_path`: a path to a file, which is opened and then
314 written.
315 - `encoding`: the text encoding of the output file.
316 - `error_handler`: the encoding error handler to use.
317 - `autoclose`: close automatically after write (except when
318 `sys.stdout` or `sys.stderr` is the destination).
319 - `handle_io_errors`: ignored, deprecated, will be removed.
320 - `mode`: how the file is to be opened (see standard function
321 `open`). The default is 'w', providing universal newline
322 support for text files.
323 """
324 Output.__init__(self, destination, destination_path,
325 encoding, error_handler)
326 self.opened = True
327 self.autoclose = autoclose
328 if mode is not None:
329 self.mode = mode
330 self._stderr = ErrorOutput()
331 if destination is None:
332 if destination_path:
333 self.opened = False
334 else:
335 self.destination = sys.stdout
336 elif (# destination is file-type object -> check mode:
337 mode and hasattr(self.destination, 'mode')
338 and mode != self.destination.mode):
339 print('Warning: Destination mode "%s" differs from specified '
340 'mode "%s"' % (self.destination.mode, mode),
341 file=self._stderr)
342 if not destination_path:
343 try:
344 self.destination_path = self.destination.name
345 except AttributeError:
346 pass
347
348 def open(self):
349 # Specify encoding in Python 3.
350 if sys.version_info >= (3, 0) and 'b' not in self.mode:
351 kwargs = {'encoding': self.encoding,
352 'errors': self.error_handler}
353 else:
354 kwargs = {}
355 try:
356 self.destination = open(self.destination_path, self.mode, **kwargs)
357 except IOError as error:
358 raise OutputError(error.errno, error.strerror,
359 self.destination_path)
360 self.opened = True
361
362 def write(self, data):
363 """Encode `data`, write it to a single file, and return it.
364
365 With Python 3 or binary output mode, `data` is returned unchanged,
366 except when specified encoding and output encoding differ.
367 """
368 if not self.opened:
369 self.open()
370 if ('b' not in self.mode and sys.version_info < (3, 0)
371 or check_encoding(self.destination, self.encoding) is False
372 ):
373 data = self.encode(data)
374 if sys.version_info >= (3, 0) and os.linesep != '\n':
375 data = data.replace(b'\n', bytes(os.linesep, 'ascii')) # fix endings
376
377 try:
378 self.destination.write(data)
379 except TypeError as e:
380 if sys.version_info >= (3, 0) and isinstance(data, bytes):
381 try:
382 self.destination.buffer.write(data)
383 except AttributeError:
384 if check_encoding(self.destination,
385 self.encoding) is False:
386 raise ValueError('Encoding of %s (%s) differs \n'
387 ' from specified encoding (%s)' %
388 (self.destination_path or 'destination',
389 self.destination.encoding, self.encoding))
390 else:
391 raise e
392 except (UnicodeError, LookupError) as err:
393 raise UnicodeError(
394 'Unable to encode output data. output-encoding is: '
395 '%s.\n(%s)' % (self.encoding, ErrorString(err)))
396 finally:
397 if self.autoclose:
398 self.close()
399 return data
400
401 def close(self):
402 if self.destination not in (sys.stdout, sys.stderr):
403 self.destination.close()
404 self.opened = False
405
406
407 class BinaryFileOutput(FileOutput):
408 """
409 A version of docutils.io.FileOutput which writes to a binary file.
410 """
411 # Used by core.publish_cmdline_to_binary() which in turn is used by
412 # rst2odt (OpenOffice writer)
413 mode = 'wb'
414
415
416 class StringInput(Input):
417
418 """
419 Direct string input.
420 """
421
422 default_source_path = '<string>'
423
424 def read(self):
425 """Decode and return the source string."""
426 return self.decode(self.source)
427
428
429 class StringOutput(Output):
430
431 """
432 Direct string output.
433 """
434
435 default_destination_path = '<string>'
436
437 def write(self, data):
438 """Encode `data`, store it in `self.destination`, and return it."""
439 self.destination = self.encode(data)
440 return self.destination
441
442
443 class NullInput(Input):
444
445 """
446 Degenerate input: read nothing.
447 """
448
449 default_source_path = 'null input'
450
451 def read(self):
452 """Return a null string."""
453 return u''
454
455
456 class NullOutput(Output):
457
458 """
459 Degenerate output: write nothing.
460 """
461
462 default_destination_path = 'null output'
463
464 def write(self, data):
465 """Do nothing ([don't even] send data to the bit bucket)."""
466 pass
467
468
469 class DocTreeInput(Input):
470
471 """
472 Adapter for document tree input.
473
474 The document tree must be passed in the ``source`` parameter.
475 """
476
477 default_source_path = 'doctree input'
478
479 def read(self):
480 """Return the document tree."""
481 return self.source