comparison mercurial/pycompat.py @ 49807:c5a06cc37401

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.
author Matt Harbison <matt_harbison@yahoo.com>
date Thu, 15 Dec 2022 01:05:27 -0500
parents 55d45d0de4e7
children b900f40c343e
comparison
equal deleted inserted replaced
49806:9eb69fa5a783 49807:c5a06cc37401
27 import sys 27 import sys
28 import tempfile 28 import tempfile
29 import xmlrpc.client as xmlrpclib 29 import xmlrpc.client as xmlrpclib
30 30
31 from typing import ( 31 from typing import (
32 Any,
33 AnyStr,
34 BinaryIO,
35 Dict,
32 Iterable, 36 Iterable,
33 Iterator, 37 Iterator,
34 List, 38 List,
39 Mapping,
40 NoReturn,
35 Optional, 41 Optional,
42 Sequence,
43 Tuple,
36 Type, 44 Type,
37 TypeVar, 45 TypeVar,
46 cast,
47 overload,
38 ) 48 )
39 49
40 ispy3 = sys.version_info[0] >= 3 50 ispy3 = sys.version_info[0] >= 3
41 ispypy = '__pypy__' in sys.builtin_module_names 51 ispypy = '__pypy__' in sys.builtin_module_names
42 TYPE_CHECKING = False 52 TYPE_CHECKING = False
44 if not globals(): # hide this from non-pytype users 54 if not globals(): # hide this from non-pytype users
45 import typing 55 import typing
46 56
47 TYPE_CHECKING = typing.TYPE_CHECKING 57 TYPE_CHECKING = typing.TYPE_CHECKING
48 58
59 _GetOptResult = Tuple[List[Tuple[bytes, bytes]], List[bytes]]
60 _T0 = TypeVar('_T0')
49 _Tbytestr = TypeVar('_Tbytestr', bound='bytestr') 61 _Tbytestr = TypeVar('_Tbytestr', bound='bytestr')
50 62
51 63
52 def future_set_exception_info(f, exc_info): 64 def future_set_exception_info(f, exc_info):
53 f.set_exception(exc_info[0]) 65 f.set_exception(exc_info[0])
54 66
55 67
56 FileNotFoundError = builtins.FileNotFoundError 68 FileNotFoundError = builtins.FileNotFoundError
57 69
58 70
59 def identity(a): 71 def identity(a: _T0) -> _T0:
60 return a 72 return a
61 73
62 74
63 def _rapply(f, xs): 75 def _rapply(f, xs):
64 if xs is None: 76 if xs is None:
248 def iterbytestr(s: Iterable[int]) -> Iterator[bytes]: 260 def iterbytestr(s: Iterable[int]) -> Iterator[bytes]:
249 """Iterate bytes as if it were a str object of Python 2""" 261 """Iterate bytes as if it were a str object of Python 2"""
250 return map(bytechr, s) 262 return map(bytechr, s)
251 263
252 264
265 if TYPE_CHECKING:
266
267 @overload
268 def maybebytestr(s: bytes) -> bytestr:
269 ...
270
271 @overload
272 def maybebytestr(s: _T0) -> _T0:
273 ...
274
275
253 def maybebytestr(s): 276 def maybebytestr(s):
254 """Promote bytes to bytestr""" 277 """Promote bytes to bytestr"""
255 if isinstance(s, bytes): 278 if isinstance(s, bytes):
256 return bytestr(s) 279 return bytestr(s)
257 return s 280 return s
258 281
259 282
260 def sysbytes(s): 283 def sysbytes(s: AnyStr) -> bytes:
261 """Convert an internal str (e.g. keyword, __doc__) back to bytes 284 """Convert an internal str (e.g. keyword, __doc__) back to bytes
262 285
263 This never raises UnicodeEncodeError, but only ASCII characters 286 This never raises UnicodeEncodeError, but only ASCII characters
264 can be round-trip by sysstr(sysbytes(s)). 287 can be round-trip by sysstr(sysbytes(s)).
265 """ 288 """
266 if isinstance(s, bytes): 289 if isinstance(s, bytes):
267 return s 290 return s
268 return s.encode('utf-8') 291 return s.encode('utf-8')
269 292
270 293
271 def sysstr(s): 294 def sysstr(s: AnyStr) -> str:
272 """Return a keyword str to be passed to Python functions such as 295 """Return a keyword str to be passed to Python functions such as
273 getattr() and str.encode() 296 getattr() and str.encode()
274 297
275 This never raises UnicodeDecodeError. Non-ascii characters are 298 This never raises UnicodeDecodeError. Non-ascii characters are
276 considered invalid and mapped to arbitrary but unique code points 299 considered invalid and mapped to arbitrary but unique code points
279 if isinstance(s, builtins.str): 302 if isinstance(s, builtins.str):
280 return s 303 return s
281 return s.decode('latin-1') 304 return s.decode('latin-1')
282 305
283 306
284 def strurl(url): 307 def strurl(url: AnyStr) -> str:
285 """Converts a bytes url back to str""" 308 """Converts a bytes url back to str"""
286 if isinstance(url, bytes): 309 if isinstance(url, bytes):
287 return url.decode('ascii') 310 return url.decode('ascii')
288 return url 311 return url
289 312
290 313
291 def bytesurl(url): 314 def bytesurl(url: AnyStr) -> bytes:
292 """Converts a str url to bytes by encoding in ascii""" 315 """Converts a str url to bytes by encoding in ascii"""
293 if isinstance(url, str): 316 if isinstance(url, str):
294 return url.encode('ascii') 317 return url.encode('ascii')
295 return url 318 return url
296 319
297 320
298 def raisewithtb(exc, tb): 321 def raisewithtb(exc: BaseException, tb) -> NoReturn:
299 """Raise exception with the given traceback""" 322 """Raise exception with the given traceback"""
300 raise exc.with_traceback(tb) 323 raise exc.with_traceback(tb)
301 324
302 325
303 def getdoc(obj): 326 def getdoc(obj: object) -> Optional[bytes]:
304 """Get docstring as bytes; may be None so gettext() won't confuse it 327 """Get docstring as bytes; may be None so gettext() won't confuse it
305 with _('')""" 328 with _('')"""
306 doc = builtins.getattr(obj, '__doc__', None) 329 doc = builtins.getattr(obj, '__doc__', None)
307 if doc is None: 330 if doc is None:
308 return doc 331 return doc
324 setattr = _wrapattrfunc(builtins.setattr) 347 setattr = _wrapattrfunc(builtins.setattr)
325 xrange = builtins.range 348 xrange = builtins.range
326 unicode = str 349 unicode = str
327 350
328 351
329 def open(name, mode=b'r', buffering=-1, encoding=None): 352 def open(
353 name,
354 mode: AnyStr = b'r',
355 buffering: int = -1,
356 encoding: Optional[str] = None,
357 ) -> Any:
358 # TODO: assert binary mode, and cast result to BinaryIO?
330 return builtins.open(name, sysstr(mode), buffering, encoding) 359 return builtins.open(name, sysstr(mode), buffering, encoding)
331 360
332 361
333 safehasattr = _wrapattrfunc(builtins.hasattr) 362 safehasattr = _wrapattrfunc(builtins.hasattr)
334 363
335 364
336 def _getoptbwrapper(orig, args, shortlist, namelist): 365 def _getoptbwrapper(
366 orig, args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
367 ) -> _GetOptResult:
337 """ 368 """
338 Takes bytes arguments, converts them to unicode, pass them to 369 Takes bytes arguments, converts them to unicode, pass them to
339 getopt.getopt(), convert the returned values back to bytes and then 370 getopt.getopt(), convert the returned values back to bytes and then
340 return them for Python 3 compatibility as getopt.getopt() don't accepts 371 return them for Python 3 compatibility as getopt.getopt() don't accepts
341 bytes on Python 3. 372 bytes on Python 3.
347 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts] 378 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
348 args = [a.encode('latin-1') for a in args] 379 args = [a.encode('latin-1') for a in args]
349 return opts, args 380 return opts, args
350 381
351 382
352 def strkwargs(dic): 383 def strkwargs(dic: Mapping[bytes, _T0]) -> Dict[str, _T0]:
353 """ 384 """
354 Converts the keys of a python dictonary to str i.e. unicodes so that 385 Converts the keys of a python dictonary to str i.e. unicodes so that
355 they can be passed as keyword arguments as dictionaries with bytes keys 386 they can be passed as keyword arguments as dictionaries with bytes keys
356 can't be passed as keyword arguments to functions on Python 3. 387 can't be passed as keyword arguments to functions on Python 3.
357 """ 388 """
358 dic = {k.decode('latin-1'): v for k, v in dic.items()} 389 dic = {k.decode('latin-1'): v for k, v in dic.items()}
359 return dic 390 return dic
360 391
361 392
362 def byteskwargs(dic): 393 def byteskwargs(dic: Mapping[str, _T0]) -> Dict[bytes, _T0]:
363 """ 394 """
364 Converts keys of python dictionaries to bytes as they were converted to 395 Converts keys of python dictionaries to bytes as they were converted to
365 str to pass that dictonary as a keyword argument on Python 3. 396 str to pass that dictonary as a keyword argument on Python 3.
366 """ 397 """
367 dic = {k.encode('latin-1'): v for k, v in dic.items()} 398 dic = {k.encode('latin-1'): v for k, v in dic.items()}
368 return dic 399 return dic
369 400
370 401
371 # TODO: handle shlex.shlex(). 402 # TODO: handle shlex.shlex().
372 def shlexsplit(s, comments=False, posix=True): 403 def shlexsplit(
404 s: bytes, comments: bool = False, posix: bool = True
405 ) -> List[bytes]:
373 """ 406 """
374 Takes bytes argument, convert it to str i.e. unicodes, pass that into 407 Takes bytes argument, convert it to str i.e. unicodes, pass that into
375 shlex.split(), convert the returned value to bytes and return that for 408 shlex.split(), convert the returned value to bytes and return that for
376 Python 3 compatibility as shelx.split() don't accept bytes on Python 3. 409 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
377 """ 410 """
390 islinux: bool = sysplatform.startswith(b'linux') 423 islinux: bool = sysplatform.startswith(b'linux')
391 isposix: bool = osname == b'posix' 424 isposix: bool = osname == b'posix'
392 iswindows: bool = osname == b'nt' 425 iswindows: bool = osname == b'nt'
393 426
394 427
395 def getoptb(args, shortlist, namelist): 428 def getoptb(
429 args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
430 ) -> _GetOptResult:
396 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist) 431 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
397 432
398 433
399 def gnugetoptb(args, shortlist, namelist): 434 def gnugetoptb(
435 args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
436 ) -> _GetOptResult:
400 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist) 437 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
401 438
402 439
403 def mkdtemp(suffix=b'', prefix=b'tmp', dir=None): 440 def mkdtemp(
441 suffix: bytes = b'', prefix: bytes = b'tmp', dir: Optional[bytes] = None
442 ) -> bytes:
404 return tempfile.mkdtemp(suffix, prefix, dir) 443 return tempfile.mkdtemp(suffix, prefix, dir)
405 444
406 445
407 # text=True is not supported; use util.from/tonativeeol() instead 446 # text=True is not supported; use util.from/tonativeeol() instead
408 def mkstemp(suffix=b'', prefix=b'tmp', dir=None): 447 def mkstemp(
448 suffix: bytes = b'', prefix: bytes = b'tmp', dir: Optional[bytes] = None
449 ) -> Tuple[int, bytes]:
409 return tempfile.mkstemp(suffix, prefix, dir) 450 return tempfile.mkstemp(suffix, prefix, dir)
410 451
411 452
412 # TemporaryFile does not support an "encoding=" argument on python2. 453 # TemporaryFile does not support an "encoding=" argument on python2.
413 # This wrapper file are always open in byte mode. 454 # This wrapper file are always open in byte mode.
414 def unnamedtempfile(mode=None, *args, **kwargs): 455 def unnamedtempfile(mode: Optional[bytes] = None, *args, **kwargs) -> BinaryIO:
415 if mode is None: 456 if mode is None:
416 mode = 'w+b' 457 mode = 'w+b'
417 else: 458 else:
418 mode = sysstr(mode) 459 mode = sysstr(mode)
419 assert 'b' in mode 460 assert 'b' in mode
420 return tempfile.TemporaryFile(mode, *args, **kwargs) 461 return cast(BinaryIO, tempfile.TemporaryFile(mode, *args, **kwargs))
421 462
422 463
423 # NamedTemporaryFile does not support an "encoding=" argument on python2. 464 # NamedTemporaryFile does not support an "encoding=" argument on python2.
424 # This wrapper file are always open in byte mode. 465 # This wrapper file are always open in byte mode.
425 def namedtempfile( 466 def namedtempfile(
426 mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True 467 mode: bytes = b'w+b',
468 bufsize: int = -1,
469 suffix: bytes = b'',
470 prefix: bytes = b'tmp',
471 dir: Optional[bytes] = None,
472 delete: bool = True,
427 ): 473 ):
428 mode = sysstr(mode) 474 mode = sysstr(mode)
429 assert 'b' in mode 475 assert 'b' in mode
430 return tempfile.NamedTemporaryFile( 476 return tempfile.NamedTemporaryFile(
431 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete 477 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete