##// END OF EJS Templates
typing: add type hints to pycompat.maplist()...
Matt Harbison -
r51070:0ab92dab default
parent child Browse files
Show More
@@ -1,482 +1,498 b''
1 1 # pycompat.py - portability shim for python 3
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 """Mercurial portability shim for python 3.
7 7
8 8 This contains aliases to hide python version-specific details from the core.
9 9 """
10 10
11 11
12 12 import builtins
13 13 import codecs
14 14 import concurrent.futures as futures
15 15 import functools
16 16 import getopt
17 17 import http.client as httplib
18 18 import http.cookiejar as cookielib
19 19 import inspect
20 20 import io
21 21 import json
22 22 import os
23 23 import queue
24 24 import shlex
25 25 import socketserver
26 26 import struct
27 27 import sys
28 28 import tempfile
29 29 import xmlrpc.client as xmlrpclib
30 30
31 31 from typing import (
32 32 Any,
33 33 AnyStr,
34 34 BinaryIO,
35 Callable,
35 36 Dict,
36 37 Iterable,
37 38 Iterator,
38 39 List,
39 40 Mapping,
40 41 NoReturn,
41 42 Optional,
42 43 Sequence,
43 44 Tuple,
44 45 Type,
45 46 TypeVar,
46 47 cast,
47 48 overload,
48 49 )
49 50
50 51 ispy3 = sys.version_info[0] >= 3
51 52 ispypy = '__pypy__' in sys.builtin_module_names
52 53 TYPE_CHECKING = False
53 54
54 55 if not globals(): # hide this from non-pytype users
55 56 import typing
56 57
57 58 TYPE_CHECKING = typing.TYPE_CHECKING
58 59
59 60 _GetOptResult = Tuple[List[Tuple[bytes, bytes]], List[bytes]]
60 61 _T0 = TypeVar('_T0')
62 _T1 = TypeVar('_T1')
63 _S = TypeVar('_S')
61 64 _Tbytestr = TypeVar('_Tbytestr', bound='bytestr')
62 65
63 66
64 67 def future_set_exception_info(f, exc_info):
65 68 f.set_exception(exc_info[0])
66 69
67 70
68 71 FileNotFoundError = builtins.FileNotFoundError
69 72
70 73
71 74 def identity(a: _T0) -> _T0:
72 75 return a
73 76
74 77
75 78 def _rapply(f, xs):
76 79 if xs is None:
77 80 # assume None means non-value of optional data
78 81 return xs
79 82 if isinstance(xs, (list, set, tuple)):
80 83 return type(xs)(_rapply(f, x) for x in xs)
81 84 if isinstance(xs, dict):
82 85 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
83 86 return f(xs)
84 87
85 88
86 89 def rapply(f, xs):
87 90 """Apply function recursively to every item preserving the data structure
88 91
89 92 >>> def f(x):
90 93 ... return 'f(%s)' % x
91 94 >>> rapply(f, None) is None
92 95 True
93 96 >>> rapply(f, 'a')
94 97 'f(a)'
95 98 >>> rapply(f, {'a'}) == {'f(a)'}
96 99 True
97 100 >>> rapply(f, ['a', 'b', None, {'c': 'd'}, []])
98 101 ['f(a)', 'f(b)', None, {'f(c)': 'f(d)'}, []]
99 102
100 103 >>> xs = [object()]
101 104 >>> rapply(identity, xs) is xs
102 105 True
103 106 """
104 107 if f is identity:
105 108 # fast path mainly for py2
106 109 return xs
107 110 return _rapply(f, xs)
108 111
109 112
110 113 if os.name == r'nt':
111 114 # MBCS (or ANSI) filesystem encoding must be used as before.
112 115 # Otherwise non-ASCII filenames in existing repositories would be
113 116 # corrupted.
114 117 # This must be set once prior to any fsencode/fsdecode calls.
115 118 sys._enablelegacywindowsfsencoding() # pytype: disable=module-attr
116 119
117 120 fsencode = os.fsencode
118 121 fsdecode = os.fsdecode
119 122 oscurdir: bytes = os.curdir.encode('ascii')
120 123 oslinesep: bytes = os.linesep.encode('ascii')
121 124 osname: bytes = os.name.encode('ascii')
122 125 ospathsep: bytes = os.pathsep.encode('ascii')
123 126 ospardir: bytes = os.pardir.encode('ascii')
124 127 ossep: bytes = os.sep.encode('ascii')
125 128 osaltsep: Optional[bytes] = os.altsep.encode('ascii') if os.altsep else None
126 129 osdevnull: bytes = os.devnull.encode('ascii')
127 130
128 131 sysplatform: bytes = sys.platform.encode('ascii')
129 132 sysexecutable: bytes = os.fsencode(sys.executable) if sys.executable else b''
130 133
131 134
132 def maplist(*args):
133 return list(map(*args))
135 if TYPE_CHECKING:
136
137 @overload
138 def maplist(f: Callable[[_T0], _S], arg: Iterable[_T0]) -> List[_S]:
139 ...
140
141 @overload
142 def maplist(
143 f: Callable[[_T0, _T1], _S], arg1: Iterable[_T0], arg2: Iterable[_T1]
144 ) -> List[_S]:
145 ...
146
147
148 def maplist(f, *args):
149 return list(map(f, *args))
134 150
135 151
136 152 def rangelist(*args):
137 153 return list(range(*args))
138 154
139 155
140 156 def ziplist(*args):
141 157 return list(zip(*args))
142 158
143 159
144 160 rawinput = input
145 161 getargspec = inspect.getfullargspec
146 162
147 163 long = int
148 164
149 165 if builtins.getattr(sys, 'argv', None) is not None:
150 166 # On POSIX, the char** argv array is converted to Python str using
151 167 # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
152 168 # isn't directly callable from Python code. In practice, os.fsencode()
153 169 # can be used instead (this is recommended by Python's documentation
154 170 # for sys.argv).
155 171 #
156 172 # On Windows, the wchar_t **argv is passed into the interpreter as-is.
157 173 # Like POSIX, we need to emulate what Py_EncodeLocale() would do. But
158 174 # there's an additional wrinkle. What we really want to access is the
159 175 # ANSI codepage representation of the arguments, as this is what
160 176 # `int main()` would receive if Python 3 didn't define `int wmain()`
161 177 # (this is how Python 2 worked). To get that, we encode with the mbcs
162 178 # encoding, which will pass CP_ACP to the underlying Windows API to
163 179 # produce bytes.
164 180 sysargv: List[bytes] = []
165 181 if os.name == r'nt':
166 182 sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
167 183 else:
168 184 sysargv = [fsencode(a) for a in sys.argv]
169 185
170 186 bytechr = struct.Struct('>B').pack
171 187 byterepr = b'%r'.__mod__
172 188
173 189
174 190 class bytestr(bytes):
175 191 """A bytes which mostly acts as a Python 2 str
176 192
177 193 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
178 194 ('', 'foo', 'ascii', '1')
179 195 >>> s = bytestr(b'foo')
180 196 >>> assert s is bytestr(s)
181 197
182 198 __bytes__() should be called if provided:
183 199
184 200 >>> class bytesable:
185 201 ... def __bytes__(self):
186 202 ... return b'bytes'
187 203 >>> bytestr(bytesable())
188 204 'bytes'
189 205
190 206 There's no implicit conversion from non-ascii str as its encoding is
191 207 unknown:
192 208
193 209 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
194 210 Traceback (most recent call last):
195 211 ...
196 212 UnicodeEncodeError: ...
197 213
198 214 Comparison between bytestr and bytes should work:
199 215
200 216 >>> assert bytestr(b'foo') == b'foo'
201 217 >>> assert b'foo' == bytestr(b'foo')
202 218 >>> assert b'f' in bytestr(b'foo')
203 219 >>> assert bytestr(b'f') in b'foo'
204 220
205 221 Sliced elements should be bytes, not integer:
206 222
207 223 >>> s[1], s[:2]
208 224 (b'o', b'fo')
209 225 >>> list(s), list(reversed(s))
210 226 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
211 227
212 228 As bytestr type isn't propagated across operations, you need to cast
213 229 bytes to bytestr explicitly:
214 230
215 231 >>> s = bytestr(b'foo').upper()
216 232 >>> t = bytestr(s)
217 233 >>> s[0], t[0]
218 234 (70, b'F')
219 235
220 236 Be careful to not pass a bytestr object to a function which expects
221 237 bytearray-like behavior.
222 238
223 239 >>> t = bytes(t) # cast to bytes
224 240 >>> assert type(t) is bytes
225 241 """
226 242
227 243 # Trick pytype into not demanding Iterable[int] be passed to __new__(),
228 244 # since the appropriate bytes format is done internally.
229 245 #
230 246 # https://github.com/google/pytype/issues/500
231 247 if TYPE_CHECKING:
232 248
233 249 def __init__(self, s: object = b'') -> None:
234 250 pass
235 251
236 252 def __new__(cls: Type[_Tbytestr], s: object = b'') -> _Tbytestr:
237 253 if isinstance(s, bytestr):
238 254 return s
239 255 if not isinstance(
240 256 s, (bytes, bytearray)
241 257 ) and not builtins.hasattr( # hasattr-py3-only
242 258 s, u'__bytes__'
243 259 ):
244 260 s = str(s).encode('ascii')
245 261 return bytes.__new__(cls, s)
246 262
247 263 # The base class uses `int` return in py3, but the point of this class is to
248 264 # behave like py2.
249 265 def __getitem__(self, key) -> bytes: # pytype: disable=signature-mismatch
250 266 s = bytes.__getitem__(self, key)
251 267 if not isinstance(s, bytes):
252 268 s = bytechr(s)
253 269 return s
254 270
255 271 # The base class expects `Iterator[int]` return in py3, but the point of
256 272 # this class is to behave like py2.
257 273 def __iter__(self) -> Iterator[bytes]: # pytype: disable=signature-mismatch
258 274 return iterbytestr(bytes.__iter__(self))
259 275
260 276 def __repr__(self) -> str:
261 277 return bytes.__repr__(self)[1:] # drop b''
262 278
263 279
264 280 def iterbytestr(s: Iterable[int]) -> Iterator[bytes]:
265 281 """Iterate bytes as if it were a str object of Python 2"""
266 282 return map(bytechr, s)
267 283
268 284
269 285 if TYPE_CHECKING:
270 286
271 287 @overload
272 288 def maybebytestr(s: bytes) -> bytestr:
273 289 ...
274 290
275 291 @overload
276 292 def maybebytestr(s: _T0) -> _T0:
277 293 ...
278 294
279 295
280 296 def maybebytestr(s):
281 297 """Promote bytes to bytestr"""
282 298 if isinstance(s, bytes):
283 299 return bytestr(s)
284 300 return s
285 301
286 302
287 303 def sysbytes(s: AnyStr) -> bytes:
288 304 """Convert an internal str (e.g. keyword, __doc__) back to bytes
289 305
290 306 This never raises UnicodeEncodeError, but only ASCII characters
291 307 can be round-trip by sysstr(sysbytes(s)).
292 308 """
293 309 if isinstance(s, bytes):
294 310 return s
295 311 return s.encode('utf-8')
296 312
297 313
298 314 def sysstr(s: AnyStr) -> str:
299 315 """Return a keyword str to be passed to Python functions such as
300 316 getattr() and str.encode()
301 317
302 318 This never raises UnicodeDecodeError. Non-ascii characters are
303 319 considered invalid and mapped to arbitrary but unique code points
304 320 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
305 321 """
306 322 if isinstance(s, builtins.str):
307 323 return s
308 324 return s.decode('latin-1')
309 325
310 326
311 327 def strurl(url: AnyStr) -> str:
312 328 """Converts a bytes url back to str"""
313 329 if isinstance(url, bytes):
314 330 return url.decode('ascii')
315 331 return url
316 332
317 333
318 334 def bytesurl(url: AnyStr) -> bytes:
319 335 """Converts a str url to bytes by encoding in ascii"""
320 336 if isinstance(url, str):
321 337 return url.encode('ascii')
322 338 return url
323 339
324 340
325 341 def raisewithtb(exc: BaseException, tb) -> NoReturn:
326 342 """Raise exception with the given traceback"""
327 343 raise exc.with_traceback(tb)
328 344
329 345
330 346 def getdoc(obj: object) -> Optional[bytes]:
331 347 """Get docstring as bytes; may be None so gettext() won't confuse it
332 348 with _('')"""
333 349 doc = builtins.getattr(obj, '__doc__', None)
334 350 if doc is None:
335 351 return doc
336 352 return sysbytes(doc)
337 353
338 354
339 355 def _wrapattrfunc(f):
340 356 @functools.wraps(f)
341 357 def w(object, name, *args):
342 358 return f(object, sysstr(name), *args)
343 359
344 360 return w
345 361
346 362
347 363 # these wrappers are automagically imported by hgloader
348 364 delattr = _wrapattrfunc(builtins.delattr)
349 365 getattr = _wrapattrfunc(builtins.getattr)
350 366 hasattr = _wrapattrfunc(builtins.hasattr)
351 367 setattr = _wrapattrfunc(builtins.setattr)
352 368 xrange = builtins.range
353 369 unicode = str
354 370
355 371
356 372 def open(
357 373 name,
358 374 mode: AnyStr = b'r',
359 375 buffering: int = -1,
360 376 encoding: Optional[str] = None,
361 377 ) -> Any:
362 378 # TODO: assert binary mode, and cast result to BinaryIO?
363 379 return builtins.open(name, sysstr(mode), buffering, encoding)
364 380
365 381
366 382 safehasattr = _wrapattrfunc(builtins.hasattr)
367 383
368 384
369 385 def _getoptbwrapper(
370 386 orig, args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
371 387 ) -> _GetOptResult:
372 388 """
373 389 Takes bytes arguments, converts them to unicode, pass them to
374 390 getopt.getopt(), convert the returned values back to bytes and then
375 391 return them for Python 3 compatibility as getopt.getopt() don't accepts
376 392 bytes on Python 3.
377 393 """
378 394 args = [a.decode('latin-1') for a in args]
379 395 shortlist = shortlist.decode('latin-1')
380 396 namelist = [a.decode('latin-1') for a in namelist]
381 397 opts, args = orig(args, shortlist, namelist)
382 398 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
383 399 args = [a.encode('latin-1') for a in args]
384 400 return opts, args
385 401
386 402
387 403 def strkwargs(dic: Mapping[bytes, _T0]) -> Dict[str, _T0]:
388 404 """
389 405 Converts the keys of a python dictonary to str i.e. unicodes so that
390 406 they can be passed as keyword arguments as dictionaries with bytes keys
391 407 can't be passed as keyword arguments to functions on Python 3.
392 408 """
393 409 dic = {k.decode('latin-1'): v for k, v in dic.items()}
394 410 return dic
395 411
396 412
397 413 def byteskwargs(dic: Mapping[str, _T0]) -> Dict[bytes, _T0]:
398 414 """
399 415 Converts keys of python dictionaries to bytes as they were converted to
400 416 str to pass that dictonary as a keyword argument on Python 3.
401 417 """
402 418 dic = {k.encode('latin-1'): v for k, v in dic.items()}
403 419 return dic
404 420
405 421
406 422 # TODO: handle shlex.shlex().
407 423 def shlexsplit(
408 424 s: bytes, comments: bool = False, posix: bool = True
409 425 ) -> List[bytes]:
410 426 """
411 427 Takes bytes argument, convert it to str i.e. unicodes, pass that into
412 428 shlex.split(), convert the returned value to bytes and return that for
413 429 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
414 430 """
415 431 ret = shlex.split(s.decode('latin-1'), comments, posix)
416 432 return [a.encode('latin-1') for a in ret]
417 433
418 434
419 435 iteritems = lambda x: x.items()
420 436 itervalues = lambda x: x.values()
421 437
422 438 json_loads = json.loads
423 439
424 440 isjython: bool = sysplatform.startswith(b'java')
425 441
426 442 isdarwin: bool = sysplatform.startswith(b'darwin')
427 443 islinux: bool = sysplatform.startswith(b'linux')
428 444 isposix: bool = osname == b'posix'
429 445 iswindows: bool = osname == b'nt'
430 446
431 447
432 448 def getoptb(
433 449 args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
434 450 ) -> _GetOptResult:
435 451 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
436 452
437 453
438 454 def gnugetoptb(
439 455 args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
440 456 ) -> _GetOptResult:
441 457 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
442 458
443 459
444 460 def mkdtemp(
445 461 suffix: bytes = b'', prefix: bytes = b'tmp', dir: Optional[bytes] = None
446 462 ) -> bytes:
447 463 return tempfile.mkdtemp(suffix, prefix, dir)
448 464
449 465
450 466 # text=True is not supported; use util.from/tonativeeol() instead
451 467 def mkstemp(
452 468 suffix: bytes = b'', prefix: bytes = b'tmp', dir: Optional[bytes] = None
453 469 ) -> Tuple[int, bytes]:
454 470 return tempfile.mkstemp(suffix, prefix, dir)
455 471
456 472
457 473 # TemporaryFile does not support an "encoding=" argument on python2.
458 474 # This wrapper file are always open in byte mode.
459 475 def unnamedtempfile(mode: Optional[bytes] = None, *args, **kwargs) -> BinaryIO:
460 476 if mode is None:
461 477 mode = 'w+b'
462 478 else:
463 479 mode = sysstr(mode)
464 480 assert 'b' in mode
465 481 return cast(BinaryIO, tempfile.TemporaryFile(mode, *args, **kwargs))
466 482
467 483
468 484 # NamedTemporaryFile does not support an "encoding=" argument on python2.
469 485 # This wrapper file are always open in byte mode.
470 486 def namedtempfile(
471 487 mode: bytes = b'w+b',
472 488 bufsize: int = -1,
473 489 suffix: bytes = b'',
474 490 prefix: bytes = b'tmp',
475 491 dir: Optional[bytes] = None,
476 492 delete: bool = True,
477 493 ):
478 494 mode = sysstr(mode)
479 495 assert 'b' in mode
480 496 return tempfile.NamedTemporaryFile(
481 497 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete
482 498 )
General Comments 0
You need to be logged in to leave comments. Login now