typing: add type hints to pycompat.bytestr
authorMatt Harbison <matt_harbison@yahoo.com>
Wed, 14 Dec 2022 01:51:33 -0500
changeset 49803 55d45d0de4e7
parent 49802 f3f33980f19b
child 49804 a87338fe8cfa
typing: add type hints to pycompat.bytestr The problem with leaving pytype to its own devices here was that for functions that returned a bytestr, pytype inferred `Union[bytes, int]`. It now accepts that it can be treated as plain bytes. I wasn't able to figure out the arg type for `__getitem__`- `SupportsIndex` (which PyCharm indicated is how the superclass function is typed) got flagged: File "/mnt/c/Users/Matt/hg/mercurial/pycompat.py", line 236, in __getitem__: unsupported operand type(s) for item retrieval: bytestr and SupportsIndex [unsupported-operands] Function __getitem__ on bytestr expects int But some caller got flagged when I marked it as `int`. There's some minor spillover problems elsewhere- pytype doesn't seem to recognize that `bytes.startswith()` can optionally take a 3rd and 4th arg, so those few places have the warning disabled. It also flags where the tar API is being abused, but that would be a tricky refactor (and would require typing extensions until py3.7 is dropped), so disable those too.
mercurial/archival.py
mercurial/pycompat.py
mercurial/templater.py
--- a/mercurial/archival.py	Wed Dec 14 01:38:52 2022 -0500
+++ b/mercurial/archival.py	Wed Dec 14 01:51:33 2022 -0500
@@ -154,9 +154,14 @@
                 )
                 self.fileobj = gzfileobj
                 return (
+                    # taropen() wants Literal['a', 'r', 'w', 'x'] for the mode,
+                    # but Literal[] is only available in 3.8+ without the
+                    # typing_extensions backport.
+                    # pytype: disable=wrong-arg-types
                     tarfile.TarFile.taropen(  # pytype: disable=attribute-error
                         name, pycompat.sysstr(mode), gzfileobj
                     )
+                    # pytype: enable=wrong-arg-types
                 )
             else:
                 try:
--- a/mercurial/pycompat.py	Wed Dec 14 01:38:52 2022 -0500
+++ b/mercurial/pycompat.py	Wed Dec 14 01:51:33 2022 -0500
@@ -29,8 +29,12 @@
 import xmlrpc.client as xmlrpclib
 
 from typing import (
+    Iterable,
+    Iterator,
     List,
     Optional,
+    Type,
+    TypeVar,
 )
 
 ispy3 = sys.version_info[0] >= 3
@@ -42,6 +46,8 @@
 
     TYPE_CHECKING = typing.TYPE_CHECKING
 
+_Tbytestr = TypeVar('_Tbytestr', bound='bytestr')
+
 
 def future_set_exception_info(f, exc_info):
     f.set_exception(exc_info[0])
@@ -212,10 +218,10 @@
     # https://github.com/google/pytype/issues/500
     if TYPE_CHECKING:
 
-        def __init__(self, s=b''):
+        def __init__(self, s: object = b'') -> None:
             pass
 
-    def __new__(cls, s=b''):
+    def __new__(cls: Type[_Tbytestr], s: object = b'') -> _Tbytestr:
         if isinstance(s, bytestr):
             return s
         if not isinstance(
@@ -226,20 +232,20 @@
             s = str(s).encode('ascii')
         return bytes.__new__(cls, s)
 
-    def __getitem__(self, key):
+    def __getitem__(self, key) -> bytes:
         s = bytes.__getitem__(self, key)
         if not isinstance(s, bytes):
             s = bytechr(s)
         return s
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[bytes]:
         return iterbytestr(bytes.__iter__(self))
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return bytes.__repr__(self)[1:]  # drop b''
 
 
-def iterbytestr(s):
+def iterbytestr(s: Iterable[int]) -> Iterator[bytes]:
     """Iterate bytes as if it were a str object of Python 2"""
     return map(bytechr, s)
 
--- a/mercurial/templater.py	Wed Dec 14 01:38:52 2022 -0500
+++ b/mercurial/templater.py	Wed Dec 14 01:51:33 2022 -0500
@@ -177,10 +177,17 @@
             quote = program[pos : pos + 2]
             s = pos = pos + 2
             while pos < end:  # find closing escaped quote
+                # pycompat.bytestr (and bytes) both have .startswith() that
+                # takes an optional start and an optional end, but pytype thinks
+                # it only takes 2 args.
+
+                # pytype: disable=wrong-arg-count
                 if program.startswith(b'\\\\\\', pos, end):
                     pos += 4  # skip over double escaped characters
                     continue
                 if program.startswith(quote, pos, end):
+                    # pytype: enable=wrong-arg-count
+
                     # interpret as if it were a part of an outer string
                     data = parser.unescapestr(program[s:pos])
                     if token == b'template':
@@ -300,7 +307,14 @@
                 return
 
             parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
+
+            # pycompat.bytestr (and bytes) both have .startswith() that
+            # takes an optional start and an optional end, but pytype thinks
+            # it only takes 2 args.
+
+            # pytype: disable=wrong-arg-count
             if not tmpl.startswith(b'}', pos):
+                # pytype: enable=wrong-arg-count
                 raise error.ParseError(_(b"invalid token"), pos)
             yield (b'template', parseres, n)
             pos += 1