Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/docutils/io.py: 37%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# $Id: io.py 10363 2026-06-18 07:56:49Z milde $
2# Author: David Goodger <goodger@python.org>
3# Copyright: This module has been placed in the public domain.
5"""
6I/O classes provide a uniform API for low-level input and output. Subclasses
7exist for a variety of input/output mechanisms.
8"""
10from __future__ import annotations
12__docformat__ = 'reStructuredText'
14import codecs
15import locale
16import os
17import sys
18import warnings
20from docutils import TransformSpec
22TYPE_CHECKING = False
23if TYPE_CHECKING:
24 from typing import BinaryIO, ClassVar, Final, Literal, TextIO
26 from docutils import nodes
27 from docutils.nodes import StrPath
29# Guess the locale's preferred encoding.
30# If no valid guess can be made, _locale_encoding is set to `None`:
31#
32# TODO: check whether this is set correctly with every OS and Python version
33# or whether front-end tools need to call `locale.setlocale()`
34# before importing this module
35try:
36 # Return locale encoding also in UTF-8 mode
37 with warnings.catch_warnings():
38 warnings.simplefilter("ignore")
39 _locale_encoding: str | None = (locale.getlocale()[1]
40 or locale.getdefaultlocale()[1]
41 ).lower()
42except: # NoQA: E722 (catchall)
43 # Any problem determining the locale: use None
44 _locale_encoding = None
45try:
46 codecs.lookup(_locale_encoding)
47except (LookupError, TypeError):
48 _locale_encoding = None
51class InputError(OSError): pass
52class OutputError(OSError): pass
55def check_encoding(stream: TextIO, encoding: str) -> bool | None:
56 """Test, whether the encoding of `stream` matches `encoding`.
58 Returns
60 :None: if `encoding` or `stream.encoding` are not a valid encoding
61 argument (e.g. ``None``) or `stream.encoding is missing.
62 :True: if the encoding argument resolves to the same value as `encoding`,
63 :False: if the encodings differ.
64 """
65 try:
66 return codecs.lookup(stream.encoding) == codecs.lookup(encoding)
67 except (LookupError, AttributeError, TypeError):
68 return None
71def error_string(err: BaseException) -> str:
72 """Return string representation of Exception `err`.
73 """
74 return f'{err.__class__.__name__}: {err}'
77class Input(TransformSpec):
78 """
79 Abstract base class for input wrappers.
81 Docutils input objects must provide a `read()` method that
82 returns the source, typically as `str` instance.
84 Inheriting `TransformSpec` allows input objects to add "transforms" to
85 the "Transformer". (Since Docutils 0.19, input objects are no longer
86 required to be `TransformSpec` instances.)
87 """
89 component_type: Final = 'input'
91 default_source_path: ClassVar[str | None] = None
93 def __init__(self,
94 source: str | TextIO | nodes.document | None = None,
95 source_path: StrPath | None = None,
96 encoding: str | None = 'utf-8',
97 error_handler: str | None = 'strict',
98 ) -> None:
99 self.encoding = encoding
100 """Text encoding for the input source."""
102 self.error_handler = error_handler
103 """Text decoding error handler."""
105 self.source = source
106 """The source of input data."""
108 self.source_path = source_path
109 """A text reference to the source."""
111 if not source_path:
112 self.source_path = self.default_source_path
114 def __repr__(self) -> str:
115 return '%s: source=%r, source_path=%r' % (self.__class__, self.source,
116 self.source_path)
118 def read(self) -> str:
119 """Return input as `str`. Define in subclasses."""
120 raise NotImplementedError
122 def decode(self, data: str | bytes) -> str:
123 """
124 Decode `data` if required.
126 Return Unicode `str` instances unchanged (nothing to decode).
127 """
128 if isinstance(data, str):
129 return data # nothing to decode
130 return str(data, self.encoding or 'utf-8', self.error_handler)
132 def isatty(self) -> bool:
133 """Return True, if the input source is connected to a TTY device."""
134 try:
135 return self.source.isatty()
136 except AttributeError:
137 return False
140class Output(TransformSpec):
141 """
142 Abstract base class for output wrappers.
144 Docutils output objects must provide a `write()` method that
145 expects and handles one argument (the output).
147 Inheriting `TransformSpec` allows output objects to add "transforms" to
148 the "Transformer". (Since Docutils 0.19, output objects are no longer
149 required to be `TransformSpec` instances.)
150 """
152 component_type: Final = 'output'
154 default_destination_path: ClassVar[str | None] = None
156 def __init__(self,
157 destination: TextIO | str | bytes | None = None,
158 destination_path: StrPath | None = None,
159 encoding: str | None = None,
160 error_handler: str | None = 'strict',
161 ) -> None:
162 self.encoding: str | None = encoding
163 """Text encoding for the output destination."""
165 self.error_handler: str = error_handler or 'strict'
166 """Text encoding error handler."""
168 self.destination: TextIO | str | bytes | None = destination
169 """The destination for output data."""
171 self.destination_path: StrPath | None = destination_path
172 """A text reference to the destination."""
174 if not destination_path:
175 self.destination_path = self.default_destination_path
177 def __repr__(self) -> str:
178 return ('%s: destination=%r, destination_path=%r'
179 % (self.__class__, self.destination, self.destination_path))
181 def write(self, data: str | bytes) -> str | bytes | None:
182 """Write `data`. Define in subclasses."""
183 raise NotImplementedError
185 def encode(self, data: str | bytes) -> str | bytes:
186 """
187 Encode and return `data`.
189 If `data` is a `bytes` instance, it is returned unchanged.
190 Otherwise it is encoded with `self.encoding`.
192 Provisional: If `self.encoding` is set to the pseudo encoding name
193 "unicode", `data` must be a `str` instance and is returned unchanged.
194 """
195 if self.encoding and self.encoding.lower() == 'unicode':
196 assert isinstance(data, str), ('output encoding is "unicode" '
197 'but `data` is no `str` instance')
198 return data
199 if not isinstance(data, str):
200 # Non-unicode (e.g. bytes) output.
201 return data
202 else:
203 return data.encode(self.encoding, self.error_handler)
206class ErrorOutput:
207 """
208 Wrapper class for file-like error streams with
209 failsafe de- and encoding of `str`, `bytes`, and `Exception` instances.
210 """
212 def __init__(self,
213 destination: TextIO|BinaryIO|str|Literal[False]|None = None,
214 encoding: str | None = None,
215 encoding_errors: str = 'backslashreplace',
216 decoding_errors: str = 'replace',
217 ) -> None:
218 """
219 :Parameters:
220 - `destination`: a file-like object,
221 a string (path to a file),
222 `None` (write to `sys.stderr`, default), or
223 evaluating to `False` (write() requests are ignored).
224 - `encoding`: `destination` text encoding. Guessed if None.
225 - `encoding_errors`: how to treat encoding errors.
226 """
227 if destination is None:
228 destination = sys.stderr
229 elif not destination:
230 destination = False
231 # if `destination` is a file name, open it
232 elif isinstance(destination, str):
233 destination = open(destination, 'w')
235 self.destination: TextIO | BinaryIO | Literal[False] = destination
236 """Where warning output is sent."""
238 self.encoding: str = (encoding
239 or getattr(destination, 'encoding', None)
240 or _locale_encoding
241 or 'ascii')
242 """The output character encoding."""
244 self.encoding_errors: str = encoding_errors
245 """Encoding error handler."""
247 self.decoding_errors: str = decoding_errors
248 """Decoding error handler."""
250 def write(self, data: str | bytes | Exception) -> None:
251 """
252 Write `data` to self.destination. Ignore, if self.destination is False.
254 `data` can be a `bytes`, `str`, or `Exception` instance.
255 """
256 if not self.destination:
257 return
258 if isinstance(data, Exception):
259 data = str(data)
260 # The destination is either opened in text or binary mode.
261 # If data has the wrong type, try to convert it.
262 try:
263 self.destination.write(data)
264 except UnicodeEncodeError:
265 # Encoding data from string to bytes failed with the
266 # destination's encoding and error handler.
267 # Try again with our own encoding and error handler.
268 binary = data.encode(self.encoding, self.encoding_errors)
269 self.destination.write(binary)
270 except TypeError:
271 if isinstance(data, str): # destination may expect bytes
272 binary = data.encode(self.encoding, self.encoding_errors)
273 self.destination.write(binary)
274 elif self.destination in (sys.stderr, sys.stdout):
275 # write bytes to raw stream
276 self.destination.buffer.write(data)
277 else:
278 # destination in text mode, write str
279 string = data.decode(self.encoding, self.decoding_errors)
280 self.destination.write(string)
282 def close(self) -> None:
283 """
284 Close the error-output stream.
286 Ignored if the destination is` sys.stderr` or `sys.stdout` or has no
287 close() method.
288 """
289 if self.destination in (sys.stdout, sys.stderr):
290 return
291 try:
292 self.destination.close()
293 except AttributeError:
294 pass
296 def isatty(self) -> bool:
297 """Return True, if the destination is connected to a TTY device."""
298 try:
299 return self.destination.isatty()
300 except AttributeError:
301 return False
304class FileInput(Input):
306 """
307 Input for single, simple file-like objects.
308 """
309 def __init__(self,
310 source: TextIO | None = None,
311 source_path: StrPath | None = None,
312 encoding: str | Literal['unicode'] | None = 'utf-8',
313 error_handler: str | None = 'strict',
314 autoclose: bool = True,
315 mode: Literal['r', 'rb', 'br'] = 'r'
316 ) -> None:
317 """
318 :Parameters:
319 - `source`: either a file-like object (with `read()` and `close()`
320 methods) or None (use source indicated by `source_path`).
321 - `source_path`: a path to a file (which is opened for reading
322 if `source` is None) or `None` (implies `sys.stdin`).
323 - `encoding`: the text encoding of the input file.
324 - `error_handler`: the encoding error handler to use.
325 - `autoclose`: close automatically after read (except when
326 the source is `sys.stdin`).
327 - `mode`: how the file is to be opened. Default is read only ('r').
328 """
329 super().__init__(source, source_path, encoding, error_handler)
330 self.autoclose = autoclose
331 self._stderr = ErrorOutput()
333 if source is None:
334 if source_path:
335 try:
336 self.source = open(source_path, mode,
337 encoding=self.encoding,
338 errors=self.error_handler)
339 except OSError as error:
340 raise InputError(error.errno, error.strerror, source_path)
341 else:
342 self.source = sys.stdin
343 elif check_encoding(self.source, self.encoding) is False:
344 # TODO: re-open, warn or raise error?
345 raise UnicodeError('Encoding clash: encoding given is "%s" '
346 'but source is opened with encoding "%s".' %
347 (self.encoding, self.source.encoding))
348 if not source_path:
349 try:
350 self.source_path = self.source.name
351 except AttributeError:
352 pass
354 def read(self) -> str:
355 """
356 Read and decode a single file, return as `str`.
357 """
358 try:
359 if not self.encoding and hasattr(self.source, 'buffer'):
360 # read as binary data
361 data = self.source.buffer.read()
362 # decode with heuristics
363 data = self.decode(data)
364 # normalize newlines
365 data = '\n'.join(data.splitlines()+[''])
366 else:
367 data = self.decode(self.source.read())
368 finally:
369 if self.autoclose:
370 self.close()
371 return data
373 def readlines(self) -> list[str]:
374 """
375 Return lines of a single file as list of strings.
376 """
377 return self.read().splitlines(True)
379 def close(self) -> None:
380 if self.source is not sys.stdin:
381 self.source.close()
384class FileOutput(Output):
386 """Output for single, simple file-like objects."""
388 default_destination_path: Final = '<file>'
390 mode: Literal['w', 'a', 'x', 'wb', 'ab', 'xb', 'bw', 'ba', 'bx'] = 'w'
391 """The mode argument for `open()`."""
392 # 'wb' for binary (e.g. OpenOffice) files.
393 # (Do not use binary mode ('wb') for text files, as this prevents the
394 # conversion of newlines to the system specific default.)
396 def __init__(self,
397 destination: TextIO | None = None,
398 destination_path: StrPath | None = None,
399 encoding: str | None = None,
400 error_handler: str | None = 'strict',
401 autoclose: bool = True,
402 handle_io_errors: None = None,
403 mode=None,
404 ) -> None:
405 """
406 :Parameters:
407 - `destination`: either a file-like object (which is written
408 directly) or `None` (which implies `sys.stdout` if no
409 `destination_path` given).
410 - `destination_path`: a path to a file, which is opened and then
411 written.
412 - `encoding`: the text encoding of the output file.
413 - `error_handler`: the encoding error handler to use.
414 - `autoclose`: close automatically after write (except when
415 `sys.stdout` or `sys.stderr` is the destination).
416 - `handle_io_errors`: ignored, deprecated, will be removed.
417 - `mode`: how the file is to be opened (see standard function
418 `open`). The default is 'w', providing universal newline
419 support for text files.
420 """
421 super().__init__(
422 destination, destination_path, encoding, error_handler)
423 self.opened = True
424 self.autoclose = autoclose
425 if handle_io_errors is not None:
426 warnings.warn('io.FileOutput: init argument "handle_io_errors" '
427 'is ignored and will be removed in '
428 'Docutils 2.0.', DeprecationWarning, stacklevel=2)
429 if mode is not None:
430 self.mode = mode
431 self._stderr = ErrorOutput()
432 if destination is None:
433 if destination_path:
434 self.opened = False
435 else:
436 self.destination = sys.stdout
437 elif ( # destination is file-type object -> check mode:
438 mode and hasattr(self.destination, 'mode')
439 and mode != self.destination.mode):
440 print('Warning: Destination mode "%s" differs from specified '
441 'mode "%s"' % (self.destination.mode, mode),
442 file=self._stderr)
443 if not destination_path:
444 try:
445 self.destination_path = self.destination.name
446 except AttributeError:
447 pass
449 def open(self) -> None:
450 # Specify encoding
451 if 'b' not in self.mode:
452 kwargs = {'encoding': self.encoding,
453 'errors': self.error_handler}
454 else:
455 kwargs = {}
456 try:
457 self.destination = open(self.destination_path, self.mode, **kwargs)
458 except OSError as error:
459 raise OutputError(error.errno, error.strerror,
460 self.destination_path)
461 self.opened = True
463 def write(self, data: str | bytes) -> str | bytes:
464 """Write `data` to a single file, also return it.
466 `data` can be a `str` or `bytes` instance.
467 If writing `bytes` fails, an attempt is made to write to
468 the low-level interface ``self.destination.buffer``.
470 If `data` is a `str` instance and `self.encoding` and
471 `self.destination.encoding` are set to different values, `data`
472 is encoded to a `bytes` instance using `self.encoding`.
474 Provisional: future versions may raise an error if `self.encoding`
475 and `self.destination.encoding` are set to different values.
476 """
477 if not self.opened:
478 self.open()
479 if (isinstance(data, str)
480 and check_encoding(self.destination, self.encoding) is False):
481 if os.linesep != '\n':
482 data = data.replace('\n', os.linesep) # fix endings
483 data = self.encode(data)
485 try:
486 self.destination.write(data)
487 except TypeError as err:
488 if isinstance(data, bytes):
489 try:
490 self.destination.buffer.write(data)
491 except AttributeError:
492 if check_encoding(self.destination,
493 self.encoding) is False:
494 raise ValueError(
495 f'Encoding of {self.destination_path} '
496 f'({self.destination.encoding}) differs \n'
497 f' from specified encoding ({self.encoding})')
498 else:
499 raise err
500 except (UnicodeError, LookupError) as err:
501 raise UnicodeError(
502 'Unable to encode output data. output-encoding is: '
503 f'{self.encoding}.\n({error_string(err)})')
504 finally:
505 if self.autoclose:
506 self.close()
507 return data
509 def close(self) -> None:
510 if self.destination not in (sys.stdout, sys.stderr):
511 self.destination.close()
512 self.opened = False
515class StringInput(Input):
516 """Input from a `str` or `bytes` instance."""
518 source: str | bytes
520 default_source_path: Final = '<string>'
522 def read(self) -> str:
523 """Return the source as `str` instance.
525 Decode, if required (see `Input.decode`).
526 """
527 return self.decode(self.source)
530class StringOutput(Output):
531 """Output to a `bytes` or `str` instance.
533 Provisional.
534 """
536 destination: str | bytes
538 default_destination_path: Final = '<string>'
540 def write(self, data: str|bytes) -> str | bytes:
541 """Store `data` in `self.destination`, and return it.
543 If `self.encoding` is set to the pseudo encoding name "unicode",
544 `data` must be a `str` instance and is stored/returned unchanged
545 (cf. `Output.encode`).
547 Otherwise, `data` can be a `bytes` or `str` instance and is
548 stored/returned as a `bytes` instance
549 (`str` data is encoded with `self.encode()`).
551 Attention: the `output_encoding`_ setting may affect the content
552 of the output (e.g. an encoding declaration in HTML or XML or the
553 representation of characters as LaTeX macro vs. literal character).
554 """
555 self.destination = self.encode(data)
556 return self.destination
559class NullInput(Input):
561 """Degenerate input: read nothing."""
563 source: None
565 default_source_path: Final = 'null input'
567 def read(self) -> str:
568 """Return an empty string."""
569 return ''
572class NullOutput(Output):
574 """Degenerate output: write nothing."""
576 destination: None
578 default_destination_path: Final = 'null output'
580 def write(self, data: str | bytes) -> None:
581 """Do nothing, return None."""
584class DocTreeInput(Input):
586 """
587 Adapter for document tree input.
589 The document tree must be passed in the ``source`` parameter.
590 """
592 source: nodes.document
594 default_source_path: Final = 'doctree input'
596 def read(self) -> nodes.document:
597 """Return the document tree."""
598 return self.source