SF.net SVN: docutils:[9817 ] trunk/docutils/docutils/i o.py

aa-turner--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 9817
          http://sourceforge.net/p/docutils/code/9817
Author:   aa-turner
Date:     2024-08-01 17:24:14 +0000 (Thu, 01 Aug 2024)
Log Message:
-----------
Add type hints to ``docutils.io``

Modified Paths:
--------------
    trunk/docutils/docutils/io.py

Modified: trunk/docutils/docutils/io.py
===================================================================
--- trunk/docutils/docutils/io.py	2024-08-01 14:27:54 UTC (rev 9816)
+++ trunk/docutils/docutils/io.py	2024-08-01 17:24:14 UTC (rev 9817)
@@ -7,6 +7,8 @@
 exist for a variety of input/output mechanisms.
 """
 
+from __future__ import annotations
+
 __docformat__ = 'reStructuredText'
 
 import codecs
@@ -15,10 +17,15 @@
 import re
 import sys
 import warnings
+from typing import TYPE_CHECKING
 
 from docutils import TransformSpec
 
+if TYPE_CHECKING:
+    from typing import Any, BinaryIO, ClassVar, Final, Literal, TextIO
 
+    from docutils import nodes
+
 # Guess the locale's preferred encoding.
 # If no valid guess can be made, _locale_encoding is set to `None`:
 #
@@ -29,9 +36,9 @@
     # Return locale encoding also in UTF-8 mode
     with warnings.catch_warnings():
         warnings.simplefilter("ignore")
-        _locale_encoding = (locale.getlocale()[1]
-                            or locale.getdefaultlocale()[1])
-        _locale_encoding = _locale_encoding.lower()
+        _locale_encoding: str | None = (
+            locale.getlocale()[1] or locale.getdefaultlocale()[1]
+        ).lower()
 except:  # noqa  any other problems determining the locale -> use None
     _locale_encoding = None
 try:
@@ -44,7 +51,7 @@
 class OutputError(OSError): pass
 
 
-def check_encoding(stream, encoding):
+def check_encoding(stream: TextIO, encoding: str) -> bool | None:
     """Test, whether the encoding of `stream` matches `encoding`.
 
     Returns
@@ -60,7 +67,7 @@
         return None
 
 
-def error_string(err) -> str:
+def error_string(err: BaseException) -> str:
     """Return string representation of Exception `err`.
     """
     return f'{err.__class__.__name__}: {err}'
@@ -78,12 +85,17 @@
     (Optional for custom input objects since Docutils 0.19.)
     """
 
-    component_type = 'input'
+    component_type: Final = 'input'
 
-    default_source_path = None
+    default_source_path: ClassVar[str | None] = None
 
-    def __init__(self, source=None, source_path=None, encoding='utf-8',
-                 error_handler='strict') -> None:
+    def __init__(
+        self,
+        source: str | TextIO | nodes.document | None = None,
+        source_path: str | os.PathLike[str] | None = None,
+        encoding: str | Literal['unicode'] | None = 'utf-8',
+        error_handler: str | None = 'strict',
+    ) -> None:
         self.encoding = encoding
         """Text encoding for the input source."""
 
@@ -106,11 +118,11 @@
         return '%s: source=%r, source_path=%r' % (self.__class__, self.source,
                                                   self.source_path)
 
-    def read(self):
+    def read(self) -> str:
         """Return input as `str`. Define in subclasses."""
         raise NotImplementedError
 
-    def decode(self, data):
+    def decode(self, data: str | bytes) -> str:
         """
         Decode `data` if required.
 
@@ -164,20 +176,23 @@
             f'{", ".join(repr(enc) for enc in encoding_candidates)}.\n'
             f'({error_string(error)})')
 
-    coding_slug = re.compile(br"coding[:=]\s*([-\w.]+)")
+    coding_slug: ClassVar[re.Pattern[bytes]] = re.compile(
+        br'coding[:=]\s*([-\w.]+)'
+    )
     """Encoding declaration pattern."""
 
-    byte_order_marks = ((codecs.BOM_UTF32_BE, 'utf-32'),
-                        (codecs.BOM_UTF32_LE, 'utf-32'),
-                        (codecs.BOM_UTF8, 'utf-8-sig'),
-                        (codecs.BOM_UTF16_BE, 'utf-16'),
-                        (codecs.BOM_UTF16_LE, 'utf-16'),
-                        )
+    byte_order_marks: ClassVar[tuple[tuple[bytes, str], ...]] = (
+        (codecs.BOM_UTF32_BE, 'utf-32'),
+        (codecs.BOM_UTF32_LE, 'utf-32'),
+        (codecs.BOM_UTF8, 'utf-8-sig'),
+        (codecs.BOM_UTF16_BE, 'utf-16'),
+        (codecs.BOM_UTF16_LE, 'utf-16'),
+    )
     """Sequence of (start_bytes, encoding) tuples for encoding detection.
     The first bytes of input data are checked against the start_bytes strings.
     A match indicates the given encoding."""
 
-    def determine_encoding_from_data(self, data):
+    def determine_encoding_from_data(self, data: bytes) -> str | None:
         """
         Try to determine the encoding of `data` by looking *in* `data`.
         Check for a byte order mark (BOM) or an encoding declaration.
@@ -193,7 +208,7 @@
                 return match.group(1).decode('ascii')
         return None
 
-    def isatty(self):
+    def isatty(self) -> bool:
         """Return True, if the input source is connected to a TTY device."""
         try:
             return self.source.isatty()
@@ -213,22 +228,27 @@
     (Optional for custom output objects since Docutils 0.19.)
     """
 
-    component_type = 'output'
+    component_type: Final = 'output'
 
-    default_destination_path = None
+    default_destination_path: ClassVar[str | None] = None
 
-    def __init__(self, destination=None, destination_path=None,
-                 encoding=None, error_handler='strict') -> None:
-        self.encoding = encoding
+    def __init__(
+        self,
+        destination: TextIO | str | bytes | None = None,
+        destination_path: str | os.PathLike[str] | None = None,
+        encoding: str | None = None,
+        error_handler: str | None = 'strict',
+    ) -> None:
+        self.encoding: str | None = encoding
         """Text encoding for the output destination."""
 
-        self.error_handler = error_handler or 'strict'
+        self.error_handler: str = error_handler or 'strict'
         """Text encoding error handler."""
 
-        self.destination = destination
+        self.destination: TextIO | str | bytes | None = destination
         """The destination for output data."""
 
-        self.destination_path = destination_path
+        self.destination_path: str | os.PathLike[str] | None = destination_path
         """A text reference to the destination."""
 
         if not destination_path:
@@ -238,11 +258,11 @@
         return ('%s: destination=%r, destination_path=%r'
                 % (self.__class__, self.destination, self.destination_path))
 
-    def write(self, data):
+    def write(self, data: str | bytes) -> str | bytes | None:
         """Write `data`. Define in subclasses."""
         raise NotImplementedError
 
-    def encode(self, data):
+    def encode(self, data: str | bytes) -> str | bytes:
         """
         Encode and return `data`.
 
@@ -266,13 +286,16 @@
 class ErrorOutput:
     """
     Wrapper class for file-like error streams with
-    failsafe de- and encoding of `str`, `bytes`, `unicode` and
-    `Exception` instances.
+    failsafe de- and encoding of `str`, `bytes`, and `Exception` instances.
     """
 
-    def __init__(self, destination=None, encoding=None,
-                 encoding_errors='backslashreplace',
-                 decoding_errors='replace') -> None:
+    def __init__(
+        self,
+        destination: TextIO | BinaryIO | str | Literal[False] | None = None,
+        encoding: str | None = None,
+        encoding_errors: str = 'backslashreplace',
+        decoding_errors: str = 'replace',
+    ) -> None:
         """
         :Parameters:
             - `destination`: a file-like object,
@@ -290,20 +313,24 @@
         elif isinstance(destination, str):
             destination = open(destination, 'w')
 
-        self.destination = destination
+        self.destination: TextIO | BinaryIO | Literal[False] = destination
         """Where warning output is sent."""
 
-        self.encoding = (encoding or getattr(destination, 'encoding', None)
-                         or _locale_encoding or 'ascii')
+        self.encoding: str = (
+            encoding
+            or getattr(destination, 'encoding', None)
+            or _locale_encoding
+            or 'ascii'
+        )
         """The output character encoding."""
 
-        self.encoding_errors = encoding_errors
+        self.encoding_errors: str = encoding_errors
         """Encoding error handler."""
 
-        self.decoding_errors = decoding_errors
+        self.decoding_errors: str = decoding_errors
         """Decoding error handler."""
 
-    def write(self, data) -> None:
+    def write(self, data: str | bytes | Exception) -> None:
         """
         Write `data` to self.destination. Ignore, if self.destination is False.
 
@@ -313,21 +340,27 @@
             return
         if isinstance(data, Exception):
             data = str(data)
+        # The destination is either opened in text or binary mode.
+        # If data has the wrong type, try to convert it.
         try:
             self.destination.write(data)
         except UnicodeEncodeError:
-            self.destination.write(data.encode(self.encoding,
-                                               self.encoding_errors))
+            # Encoding data from string to bytes failed with the
+            # destination's encoding and error handler.
+            # Try again with our own encoding and error handler.
+            binary = data.encode(self.encoding, self.encoding_errors)
+            self.destination.write(binary)
         except TypeError:
             if isinstance(data, str):  # destination may expect bytes
-                self.destination.write(data.encode(self.encoding,
-                                                   self.encoding_errors))
+                binary = data.encode(self.encoding, self.encoding_errors)
+                self.destination.write(binary)
             elif self.destination in (sys.stderr, sys.stdout):
                 # write bytes to raw stream
                 self.destination.buffer.write(data)
             else:
-                self.destination.write(str(data, self.encoding,
-                                           self.decoding_errors))
+                # destination in text mode, write str
+                string = data.decode(self.encoding, self.decoding_errors)
+                self.destination.write(string)
 
     def close(self) -> None:
         """
@@ -343,7 +376,7 @@
         except AttributeError:
             pass
 
-    def isatty(self):
+    def isatty(self) -> bool:
         """Return True, if the destination is connected to a TTY device."""
         try:
             return self.destination.isatty()
@@ -356,9 +389,15 @@
     """
     Input for single, simple file-like objects.
     """
-    def __init__(self, source=None, source_path=None,
-                 encoding='utf-8', error_handler='strict',
-                 autoclose=True, mode='r') -> None:
+    def __init__(
+        self,
+        source: TextIO | None = None,
+        source_path: str | os.PathLike[str] | None = None,
+        encoding: str | Literal['unicode'] | None = 'utf-8',
+        error_handler: str | None = 'strict',
+        autoclose: bool = True,
+        mode: Literal['r', 'rb', 'br'] = 'r'
+    ) -> None:
         """
         :Parameters:
             - `source`: either a file-like object (which is read directly), or
@@ -371,7 +410,7 @@
             - `mode`: how the file is to be opened (see standard function
               `open`). The default is read only ('r').
         """
-        Input.__init__(self, source, source_path, encoding, error_handler)
+        super().__init__(source, source_path, encoding, error_handler)
         self.autoclose = autoclose
         self._stderr = ErrorOutput()
 
@@ -396,7 +435,7 @@
             except AttributeError:
                 pass
 
-    def read(self):
+    def read(self) -> str:
         """
         Read and decode a single file, return as `str`.
         """
@@ -415,7 +454,7 @@
                 self.close()
         return data
 
-    def readlines(self):
+    def readlines(self) -> list[str]:
         """
         Return lines of a single file as list of strings.
         """
@@ -430,17 +469,24 @@
 
     """Output for single, simple file-like objects."""
 
-    default_destination_path = '<file>'
+    default_destination_path: Final = '<file>'
 
-    mode = 'w'
+    mode: Literal['w', 'a', 'x', 'wb', 'ab', 'xb', 'bw', 'ba', 'bx'] = 'w'
     """The mode argument for `open()`."""
     # 'wb' for binary (e.g. OpenOffice) files (see also `BinaryFileOutput`).
     # (Do not use binary mode ('wb') for text files, as this prevents the
     # conversion of newlines to the system specific default.)
 
-    def __init__(self, destination=None, destination_path=None,
-                 encoding=None, error_handler='strict', autoclose=True,
-                 handle_io_errors=None, mode=None) -> None:
+    def __init__(
+        self,
+        destination: TextIO | None = None,
+        destination_path: str | os.PathLike[str] | None = None,
+        encoding: str | None = None,
+        error_handler: str | None = 'strict',
+        autoclose: bool = True,
+        handle_io_errors: None = None,
+        mode=None,
+    ) -> None:
         """
         :Parameters:
             - `destination`: either a file-like object (which is written
@@ -457,8 +503,9 @@
               `open`). The default is 'w', providing universal newline
               support for text files.
         """
-        Output.__init__(self, destination, destination_path,
-                        encoding, error_handler)
+        super().__init__(
+            destination, destination_path, encoding, error_handler,
+        )
         self.opened = True
         self.autoclose = autoclose
         if handle_io_errors is not None:
@@ -485,7 +532,7 @@
             except AttributeError:
                 pass
 
-    def open(self):
+    def open(self) -> None:
         # Specify encoding
         if 'b' not in self.mode:
             kwargs = {'encoding': self.encoding,
@@ -499,7 +546,7 @@
                               self.destination_path)
         self.opened = True
 
-    def write(self, data):
+    def write(self, data: str | bytes) -> str | bytes:
         """Write `data` to a single file, also return it.
 
         `data` can be a `str` or `bytes` instance.
@@ -561,7 +608,7 @@
     # Used by core.publish_cmdline_to_binary() which is also deprecated.
     mode = 'wb'
 
-    def __init__(self, *args, **kwargs) -> None:
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
         warnings.warn('"BinaryFileOutput" is obsoleted by "FileOutput"'
                       ' and will be removed in Docutils 0.24.',
                       DeprecationWarning, stacklevel=2)
@@ -571,9 +618,11 @@
 class StringInput(Input):
     """Input from a `str` or `bytes` instance."""
 
-    default_source_path = '<string>'
+    source: str | bytes
 
-    def read(self):
+    default_source_path: Final = '<string>'
+
+    def read(self) -> str:
         """Return the source as `str` instance.
 
         Decode, if required (see `Input.decode`).
@@ -587,9 +636,11 @@
     Provisional.
     """
 
-    default_destination_path = '<string>'
+    destination: str | bytes
 
-    def write(self, data):
+    default_destination_path: Final = '<string>'
+
+    def write(self, data: str | bytes) -> str | bytes:
         """Store `data` in `self.destination`, and return it.
 
         If `self.encoding` is set to the pseudo encoding name "unicode",
@@ -612,8 +663,10 @@
 
     """Degenerate input: read nothing."""
 
-    default_source_path = 'null input'
+    source: None
 
+    default_source_path: Final = 'null input'
+
     def read(self) -> str:
         """Return an empty string."""
         return ''
@@ -623,9 +676,11 @@
 
     """Degenerate output: write nothing."""
 
-    default_destination_path = 'null output'
+    destination: None
 
-    def write(self, data) -> None:
+    default_destination_path: Final = 'null output'
+
+    def write(self, data: str | bytes) -> None:
         """Do nothing, return None."""
         pass
 
@@ -638,8 +693,10 @@
     The document tree must be passed in the ``source`` parameter.
     """
 
-    default_source_path = 'doctree input'
+    source: nodes.document
 
-    def read(self):
+    default_source_path: Final = 'doctree input'
+
+    def read(self) -> nodes.document:
         """Return the document tree."""
         return self.source

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.