typing: add type hints to most mercurial/pycompat.py functions
authorMatt Harbison <matt_harbison@yahoo.com>
Thu, 15 Dec 2022 01:05:27 -0500
changeset 49902 c5a06cc37401
parent 49901 9eb69fa5a783
child 49903 7a4143428db7
typing: add type hints to most mercurial/pycompat.py functions The `rapply` methods are left out because it's not `rapply(f, xs: _T0) -> _T0` as I first thought- it's used somewhere to walk a collection and convert between bytes and str. Also, the `open()` call is partially untyped because I'm not sure what its purpose is at this point- both the name and mode can be either bytes or str as it is currently constituted. It might make sense to assert that the file is being opened in binary mode (like `namedtempfile()`) and cast the result to `BinaryIO`, but that shouldn't be smuggled in with these other changes. The return is currently typed as `Any` because something suddenly got smarter and a few uses in util.py (like readfile()) suddenly think it returns `IO[str]` instead of `IO[bytes]` (BinaryIO), and it flags the type mismatch there.
mercurial/pycompat.py
--- a/mercurial/pycompat.py	Wed Dec 14 22:27:22 2022 -0500
+++ b/mercurial/pycompat.py	Thu Dec 15 01:05:27 2022 -0500
@@ -29,12 +29,22 @@
 import xmlrpc.client as xmlrpclib
 
 from typing import (
+    Any,
+    AnyStr,
+    BinaryIO,
+    Dict,
     Iterable,
     Iterator,
     List,
+    Mapping,
+    NoReturn,
     Optional,
+    Sequence,
+    Tuple,
     Type,
     TypeVar,
+    cast,
+    overload,
 )
 
 ispy3 = sys.version_info[0] >= 3
@@ -46,6 +56,8 @@
 
     TYPE_CHECKING = typing.TYPE_CHECKING
 
+_GetOptResult = Tuple[List[Tuple[bytes, bytes]], List[bytes]]
+_T0 = TypeVar('_T0')
 _Tbytestr = TypeVar('_Tbytestr', bound='bytestr')
 
 
@@ -56,7 +68,7 @@
 FileNotFoundError = builtins.FileNotFoundError
 
 
-def identity(a):
+def identity(a: _T0) -> _T0:
     return a
 
 
@@ -250,6 +262,17 @@
     return map(bytechr, s)
 
 
+if TYPE_CHECKING:
+
+    @overload
+    def maybebytestr(s: bytes) -> bytestr:
+        ...
+
+    @overload
+    def maybebytestr(s: _T0) -> _T0:
+        ...
+
+
 def maybebytestr(s):
     """Promote bytes to bytestr"""
     if isinstance(s, bytes):
@@ -257,7 +280,7 @@
     return s
 
 
-def sysbytes(s):
+def sysbytes(s: AnyStr) -> bytes:
     """Convert an internal str (e.g. keyword, __doc__) back to bytes
 
     This never raises UnicodeEncodeError, but only ASCII characters
@@ -268,7 +291,7 @@
     return s.encode('utf-8')
 
 
-def sysstr(s):
+def sysstr(s: AnyStr) -> str:
     """Return a keyword str to be passed to Python functions such as
     getattr() and str.encode()
 
@@ -281,26 +304,26 @@
     return s.decode('latin-1')
 
 
-def strurl(url):
+def strurl(url: AnyStr) -> str:
     """Converts a bytes url back to str"""
     if isinstance(url, bytes):
         return url.decode('ascii')
     return url
 
 
-def bytesurl(url):
+def bytesurl(url: AnyStr) -> bytes:
     """Converts a str url to bytes by encoding in ascii"""
     if isinstance(url, str):
         return url.encode('ascii')
     return url
 
 
-def raisewithtb(exc, tb):
+def raisewithtb(exc: BaseException, tb) -> NoReturn:
     """Raise exception with the given traceback"""
     raise exc.with_traceback(tb)
 
 
-def getdoc(obj):
+def getdoc(obj: object) -> Optional[bytes]:
     """Get docstring as bytes; may be None so gettext() won't confuse it
     with _('')"""
     doc = builtins.getattr(obj, '__doc__', None)
@@ -326,14 +349,22 @@
 unicode = str
 
 
-def open(name, mode=b'r', buffering=-1, encoding=None):
+def open(
+    name,
+    mode: AnyStr = b'r',
+    buffering: int = -1,
+    encoding: Optional[str] = None,
+) -> Any:
+    # TODO: assert binary mode, and cast result to BinaryIO?
     return builtins.open(name, sysstr(mode), buffering, encoding)
 
 
 safehasattr = _wrapattrfunc(builtins.hasattr)
 
 
-def _getoptbwrapper(orig, args, shortlist, namelist):
+def _getoptbwrapper(
+    orig, args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
+) -> _GetOptResult:
     """
     Takes bytes arguments, converts them to unicode, pass them to
     getopt.getopt(), convert the returned values back to bytes and then
@@ -349,7 +380,7 @@
     return opts, args
 
 
-def strkwargs(dic):
+def strkwargs(dic: Mapping[bytes, _T0]) -> Dict[str, _T0]:
     """
     Converts the keys of a python dictonary to str i.e. unicodes so that
     they can be passed as keyword arguments as dictionaries with bytes keys
@@ -359,7 +390,7 @@
     return dic
 
 
-def byteskwargs(dic):
+def byteskwargs(dic: Mapping[str, _T0]) -> Dict[bytes, _T0]:
     """
     Converts keys of python dictionaries to bytes as they were converted to
     str to pass that dictonary as a keyword argument on Python 3.
@@ -369,7 +400,9 @@
 
 
 # TODO: handle shlex.shlex().
-def shlexsplit(s, comments=False, posix=True):
+def shlexsplit(
+    s: bytes, comments: bool = False, posix: bool = True
+) -> List[bytes]:
     """
     Takes bytes argument, convert it to str i.e. unicodes, pass that into
     shlex.split(), convert the returned value to bytes and return that for
@@ -392,38 +425,51 @@
 iswindows: bool = osname == b'nt'
 
 
-def getoptb(args, shortlist, namelist):
+def getoptb(
+    args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
+) -> _GetOptResult:
     return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
 
 
-def gnugetoptb(args, shortlist, namelist):
+def gnugetoptb(
+    args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
+) -> _GetOptResult:
     return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
 
 
-def mkdtemp(suffix=b'', prefix=b'tmp', dir=None):
+def mkdtemp(
+    suffix: bytes = b'', prefix: bytes = b'tmp', dir: Optional[bytes] = None
+) -> bytes:
     return tempfile.mkdtemp(suffix, prefix, dir)
 
 
 # text=True is not supported; use util.from/tonativeeol() instead
-def mkstemp(suffix=b'', prefix=b'tmp', dir=None):
+def mkstemp(
+    suffix: bytes = b'', prefix: bytes = b'tmp', dir: Optional[bytes] = None
+) -> Tuple[int, bytes]:
     return tempfile.mkstemp(suffix, prefix, dir)
 
 
 # TemporaryFile does not support an "encoding=" argument on python2.
 # This wrapper file are always open in byte mode.
-def unnamedtempfile(mode=None, *args, **kwargs):
+def unnamedtempfile(mode: Optional[bytes] = None, *args, **kwargs) -> BinaryIO:
     if mode is None:
         mode = 'w+b'
     else:
         mode = sysstr(mode)
     assert 'b' in mode
-    return tempfile.TemporaryFile(mode, *args, **kwargs)
+    return cast(BinaryIO, tempfile.TemporaryFile(mode, *args, **kwargs))
 
 
 # NamedTemporaryFile does not support an "encoding=" argument on python2.
 # This wrapper file are always open in byte mode.
 def namedtempfile(
-    mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True
+    mode: bytes = b'w+b',
+    bufsize: int = -1,
+    suffix: bytes = b'',
+    prefix: bytes = b'tmp',
+    dir: Optional[bytes] = None,
+    delete: bool = True,
 ):
     mode = sysstr(mode)
     assert 'b' in mode