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

250 statements  

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. 

4 

5""" 

6I/O classes provide a uniform API for low-level input and output. Subclasses 

7exist for a variety of input/output mechanisms. 

8""" 

9 

10from __future__ import annotations 

11 

12__docformat__ = 'reStructuredText' 

13 

14import codecs 

15import locale 

16import os 

17import sys 

18import warnings 

19 

20from docutils import TransformSpec 

21 

22TYPE_CHECKING = False 

23if TYPE_CHECKING: 

24 from typing import BinaryIO, ClassVar, Final, Literal, TextIO 

25 

26 from docutils import nodes 

27 from docutils.nodes import StrPath 

28 

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 

49 

50 

51class InputError(OSError): pass 

52class OutputError(OSError): pass 

53 

54 

55def check_encoding(stream: TextIO, encoding: str) -> bool | None: 

56 """Test, whether the encoding of `stream` matches `encoding`. 

57 

58 Returns 

59 

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 

69 

70 

71def error_string(err: BaseException) -> str: 

72 """Return string representation of Exception `err`. 

73 """ 

74 return f'{err.__class__.__name__}: {err}' 

75 

76 

77class Input(TransformSpec): 

78 """ 

79 Abstract base class for input wrappers. 

80 

81 Docutils input objects must provide a `read()` method that 

82 returns the source, typically as `str` instance. 

83 

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 """ 

88 

89 component_type: Final = 'input' 

90 

91 default_source_path: ClassVar[str | None] = None 

92 

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.""" 

101 

102 self.error_handler = error_handler 

103 """Text decoding error handler.""" 

104 

105 self.source = source 

106 """The source of input data.""" 

107 

108 self.source_path = source_path 

109 """A text reference to the source.""" 

110 

111 if not source_path: 

112 self.source_path = self.default_source_path 

113 

114 def __repr__(self) -> str: 

115 return '%s: source=%r, source_path=%r' % (self.__class__, self.source, 

116 self.source_path) 

117 

118 def read(self) -> str: 

119 """Return input as `str`. Define in subclasses.""" 

120 raise NotImplementedError 

121 

122 def decode(self, data: str | bytes) -> str: 

123 """ 

124 Decode `data` if required. 

125 

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) 

131 

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 

138 

139 

140class Output(TransformSpec): 

141 """ 

142 Abstract base class for output wrappers. 

143 

144 Docutils output objects must provide a `write()` method that 

145 expects and handles one argument (the output). 

146 

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 """ 

151 

152 component_type: Final = 'output' 

153 

154 default_destination_path: ClassVar[str | None] = None 

155 

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.""" 

164 

165 self.error_handler: str = error_handler or 'strict' 

166 """Text encoding error handler.""" 

167 

168 self.destination: TextIO | str | bytes | None = destination 

169 """The destination for output data.""" 

170 

171 self.destination_path: StrPath | None = destination_path 

172 """A text reference to the destination.""" 

173 

174 if not destination_path: 

175 self.destination_path = self.default_destination_path 

176 

177 def __repr__(self) -> str: 

178 return ('%s: destination=%r, destination_path=%r' 

179 % (self.__class__, self.destination, self.destination_path)) 

180 

181 def write(self, data: str | bytes) -> str | bytes | None: 

182 """Write `data`. Define in subclasses.""" 

183 raise NotImplementedError 

184 

185 def encode(self, data: str | bytes) -> str | bytes: 

186 """ 

187 Encode and return `data`. 

188 

189 If `data` is a `bytes` instance, it is returned unchanged. 

190 Otherwise it is encoded with `self.encoding`. 

191 

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) 

204 

205 

206class ErrorOutput: 

207 """ 

208 Wrapper class for file-like error streams with 

209 failsafe de- and encoding of `str`, `bytes`, and `Exception` instances. 

210 """ 

211 

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') 

234 

235 self.destination: TextIO | BinaryIO | Literal[False] = destination 

236 """Where warning output is sent.""" 

237 

238 self.encoding: str = (encoding 

239 or getattr(destination, 'encoding', None) 

240 or _locale_encoding 

241 or 'ascii') 

242 """The output character encoding.""" 

243 

244 self.encoding_errors: str = encoding_errors 

245 """Encoding error handler.""" 

246 

247 self.decoding_errors: str = decoding_errors 

248 """Decoding error handler.""" 

249 

250 def write(self, data: str | bytes | Exception) -> None: 

251 """ 

252 Write `data` to self.destination. Ignore, if self.destination is False. 

253 

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) 

281 

282 def close(self) -> None: 

283 """ 

284 Close the error-output stream. 

285 

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 

295 

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 

302 

303 

304class FileInput(Input): 

305 

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() 

332 

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 

353 

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 

372 

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) 

378 

379 def close(self) -> None: 

380 if self.source is not sys.stdin: 

381 self.source.close() 

382 

383 

384class FileOutput(Output): 

385 

386 """Output for single, simple file-like objects.""" 

387 

388 default_destination_path: Final = '<file>' 

389 

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.) 

395 

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 

448 

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 

462 

463 def write(self, data: str | bytes) -> str | bytes: 

464 """Write `data` to a single file, also return it. 

465 

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``. 

469 

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`. 

473 

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) 

484 

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 

508 

509 def close(self) -> None: 

510 if self.destination not in (sys.stdout, sys.stderr): 

511 self.destination.close() 

512 self.opened = False 

513 

514 

515class StringInput(Input): 

516 """Input from a `str` or `bytes` instance.""" 

517 

518 source: str | bytes 

519 

520 default_source_path: Final = '<string>' 

521 

522 def read(self) -> str: 

523 """Return the source as `str` instance. 

524 

525 Decode, if required (see `Input.decode`). 

526 """ 

527 return self.decode(self.source) 

528 

529 

530class StringOutput(Output): 

531 """Output to a `bytes` or `str` instance. 

532 

533 Provisional. 

534 """ 

535 

536 destination: str | bytes 

537 

538 default_destination_path: Final = '<string>' 

539 

540 def write(self, data: str|bytes) -> str | bytes: 

541 """Store `data` in `self.destination`, and return it. 

542 

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`). 

546 

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()`). 

550 

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 

557 

558 

559class NullInput(Input): 

560 

561 """Degenerate input: read nothing.""" 

562 

563 source: None 

564 

565 default_source_path: Final = 'null input' 

566 

567 def read(self) -> str: 

568 """Return an empty string.""" 

569 return '' 

570 

571 

572class NullOutput(Output): 

573 

574 """Degenerate output: write nothing.""" 

575 

576 destination: None 

577 

578 default_destination_path: Final = 'null output' 

579 

580 def write(self, data: str | bytes) -> None: 

581 """Do nothing, return None.""" 

582 

583 

584class DocTreeInput(Input): 

585 

586 """ 

587 Adapter for document tree input. 

588 

589 The document tree must be passed in the ``source`` parameter. 

590 """ 

591 

592 source: nodes.document 

593 

594 default_source_path: Final = 'doctree input' 

595 

596 def read(self) -> nodes.document: 

597 """Return the document tree.""" 

598 return self.source