comparison hgext/convert/common.py @ 51685:0eb515c7bec8

typing: add trivial type hints to the convert extension's common modules This started as ensuring that the `encoding` and `orig_encoding` attributes has a type other than `Any`, so pytype can catch problems where it needs to be str for stdlib encoding and decoding. It turns out that adding the hint in `mercurial.encoding` is what was needed, but I picked a bunch of low hanging fruit while here. There's definitely more to do, and I see a problem where `shlex.shlex` is being fed bytes instead of str, but there are not enough type hints yet to make pytype notice.
author Matt Harbison <matt_harbison@yahoo.com>
date Thu, 11 Jul 2024 20:54:06 -0400
parents 20e2a20674dc
children 39033e7a6e0a
comparison
equal deleted inserted replaced
51684:20e2a20674dc 51685:0eb515c7bec8
9 import os 9 import os
10 import pickle 10 import pickle
11 import re 11 import re
12 import shlex 12 import shlex
13 import subprocess 13 import subprocess
14 import typing
15
16 from typing import (
17 Any,
18 AnyStr,
19 Optional,
20 )
14 21
15 from mercurial.i18n import _ 22 from mercurial.i18n import _
16 from mercurial.pycompat import open 23 from mercurial.pycompat import open
17 from mercurial import ( 24 from mercurial import (
18 encoding, 25 encoding,
24 from mercurial.utils import ( 31 from mercurial.utils import (
25 dateutil, 32 dateutil,
26 procutil, 33 procutil,
27 ) 34 )
28 35
36 if typing.TYPE_CHECKING:
37 from typing import (
38 overload,
39 )
40 from mercurial import (
41 ui as uimod,
42 )
43
29 propertycache = util.propertycache 44 propertycache = util.propertycache
45
46
47 if typing.TYPE_CHECKING:
48
49 @overload
50 def _encodeornone(d: str) -> bytes:
51 pass
52
53 @overload
54 def _encodeornone(d: None) -> None:
55 pass
30 56
31 57
32 def _encodeornone(d): 58 def _encodeornone(d):
33 if d is None: 59 if d is None:
34 return 60 return
35 return d.encode('latin1') 61 return d.encode('latin1')
36 62
37 63
38 class _shlexpy3proxy: 64 class _shlexpy3proxy:
39 def __init__(self, l): 65 def __init__(self, l: shlex.shlex) -> None:
40 self._l = l 66 self._l = l
41 67
42 def __iter__(self): 68 def __iter__(self):
43 return (_encodeornone(v) for v in self._l) 69 return (_encodeornone(v) for v in self._l)
44 70
48 @property 74 @property
49 def infile(self): 75 def infile(self):
50 return self._l.infile or b'<unknown>' 76 return self._l.infile or b'<unknown>'
51 77
52 @property 78 @property
53 def lineno(self): 79 def lineno(self) -> int:
54 return self._l.lineno 80 return self._l.lineno
55 81
56 82
57 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None): 83 def shlexer(
84 data=None,
85 filepath: Optional[str] = None,
86 wordchars: Optional[bytes] = None,
87 whitespace: Optional[bytes] = None,
88 ):
58 if data is None: 89 if data is None:
59 data = open(filepath, b'r', encoding='latin1') 90 data = open(filepath, b'r', encoding='latin1')
60 else: 91 else:
61 if filepath is not None: 92 if filepath is not None:
62 raise error.ProgrammingError( 93 raise error.ProgrammingError(
70 if wordchars is not None: 101 if wordchars is not None:
71 l.wordchars += wordchars.decode('latin1') 102 l.wordchars += wordchars.decode('latin1')
72 return _shlexpy3proxy(l) 103 return _shlexpy3proxy(l)
73 104
74 105
75 def encodeargs(args): 106 def encodeargs(args: Any) -> bytes:
76 def encodearg(s): 107 def encodearg(s: bytes) -> bytes:
77 lines = base64.encodebytes(s) 108 lines = base64.encodebytes(s)
78 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)] 109 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
79 return b''.join(lines) 110 return b''.join(lines)
80 111
81 s = pickle.dumps(args) 112 s = pickle.dumps(args)
82 return encodearg(s) 113 return encodearg(s)
83 114
84 115
85 def decodeargs(s): 116 def decodeargs(s: bytes) -> Any:
86 s = base64.decodebytes(s) 117 s = base64.decodebytes(s)
87 return pickle.loads(s) 118 return pickle.loads(s)
88 119
89 120
90 class MissingTool(Exception): 121 class MissingTool(Exception):
91 pass 122 pass
92 123
93 124
94 def checktool(exe, name=None, abort=True): 125 def checktool(
126 exe: bytes, name: Optional[bytes] = None, abort: bool = True
127 ) -> None:
95 name = name or exe 128 name = name or exe
96 if not procutil.findexe(exe): 129 if not procutil.findexe(exe):
97 if abort: 130 if abort:
98 exc = error.Abort 131 exc = error.Abort
99 else: 132 else:
103 136
104 class NoRepo(Exception): 137 class NoRepo(Exception):
105 pass 138 pass
106 139
107 140
108 SKIPREV = b'SKIP' 141 SKIPREV: bytes = b'SKIP'
109 142
110 143
111 class commit: 144 class commit:
112 def __init__( 145 def __init__(
113 self, 146 self,
114 author, 147 author: bytes,
115 date, 148 date: bytes,
116 desc, 149 desc: bytes,
117 parents, 150 parents,
118 branch=None, 151 branch: Optional[bytes] = None,
119 rev=None, 152 rev=None,
120 extra=None, 153 extra=None,
121 sortkey=None, 154 sortkey=None,
122 saverev=True, 155 saverev=True,
123 phase=phases.draft, 156 phase: int = phases.draft,
124 optparents=None, 157 optparents=None,
125 ctx=None, 158 ctx=None,
126 ): 159 ) -> None:
127 self.author = author or b'unknown' 160 self.author = author or b'unknown'
128 self.date = date or b'0 0' 161 self.date = date or b'0 0'
129 self.desc = desc 162 self.desc = desc
130 self.parents = parents # will be converted and used as parents 163 self.parents = parents # will be converted and used as parents
131 self.optparents = optparents or [] # will be used if already converted 164 self.optparents = optparents or [] # will be used if already converted
139 172
140 173
141 class converter_source: 174 class converter_source:
142 """Conversion source interface""" 175 """Conversion source interface"""
143 176
144 def __init__(self, ui, repotype, path=None, revs=None): 177 def __init__(
178 self,
179 ui: "uimod.ui",
180 repotype: bytes,
181 path: Optional[bytes] = None,
182 revs=None,
183 ) -> None:
145 """Initialize conversion source (or raise NoRepo("message") 184 """Initialize conversion source (or raise NoRepo("message")
146 exception if path is not a valid repository)""" 185 exception if path is not a valid repository)"""
147 self.ui = ui 186 self.ui = ui
148 self.path = path 187 self.path = path
149 self.revs = revs 188 self.revs = revs
150 self.repotype = repotype 189 self.repotype = repotype
151 190
152 self.encoding = b'utf-8' 191 self.encoding = b'utf-8'
153 192
154 def checkhexformat(self, revstr, mapname=b'splicemap'): 193 def checkhexformat(
194 self, revstr: bytes, mapname: bytes = b'splicemap'
195 ) -> None:
155 """fails if revstr is not a 40 byte hex. mercurial and git both uses 196 """fails if revstr is not a 40 byte hex. mercurial and git both uses
156 such format for their revision numbering 197 such format for their revision numbering
157 """ 198 """
158 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr): 199 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
159 raise error.Abort( 200 raise error.Abort(
160 _(b'%s entry %s is not a valid revision identifier') 201 _(b'%s entry %s is not a valid revision identifier')
161 % (mapname, revstr) 202 % (mapname, revstr)
162 ) 203 )
163 204
164 def before(self): 205 def before(self) -> None:
165 pass 206 pass
166 207
167 def after(self): 208 def after(self) -> None:
168 pass 209 pass
169 210
170 def targetfilebelongstosource(self, targetfilename): 211 def targetfilebelongstosource(self, targetfilename):
171 """Returns true if the given targetfile belongs to the source repo. This 212 """Returns true if the given targetfile belongs to the source repo. This
172 is useful when only a subdirectory of the target belongs to the source 213 is useful when only a subdirectory of the target belongs to the source
221 262
222 Tag names must be UTF-8 strings. 263 Tag names must be UTF-8 strings.
223 """ 264 """
224 raise NotImplementedError 265 raise NotImplementedError
225 266
226 def recode(self, s, encoding=None): 267 def recode(self, s: AnyStr, encoding: Optional[bytes] = None) -> bytes:
227 if not encoding: 268 if not encoding:
228 encoding = self.encoding or b'utf-8' 269 encoding = self.encoding or b'utf-8'
229 270
230 if isinstance(s, str): 271 if isinstance(s, str):
231 return s.encode("utf-8") 272 return s.encode("utf-8")
250 291
251 This function is only needed to support --filemap 292 This function is only needed to support --filemap
252 """ 293 """
253 raise NotImplementedError 294 raise NotImplementedError
254 295
255 def converted(self, rev, sinkrev): 296 def converted(self, rev, sinkrev) -> None:
256 '''Notify the source that a revision has been converted.''' 297 '''Notify the source that a revision has been converted.'''
257 298
258 def hasnativeorder(self): 299 def hasnativeorder(self) -> bool:
259 """Return true if this source has a meaningful, native revision 300 """Return true if this source has a meaningful, native revision
260 order. For instance, Mercurial revisions are store sequentially 301 order. For instance, Mercurial revisions are store sequentially
261 while there is no such global ordering with Darcs. 302 while there is no such global ordering with Darcs.
262 """ 303 """
263 return False 304 return False
264 305
265 def hasnativeclose(self): 306 def hasnativeclose(self) -> bool:
266 """Return true if this source has ability to close branch.""" 307 """Return true if this source has ability to close branch."""
267 return False 308 return False
268 309
269 def lookuprev(self, rev): 310 def lookuprev(self, rev):
270 """If rev is a meaningful revision reference in source, return 311 """If rev is a meaningful revision reference in source, return
278 319
279 Bookmark names are to be UTF-8 strings. 320 Bookmark names are to be UTF-8 strings.
280 """ 321 """
281 return {} 322 return {}
282 323
283 def checkrevformat(self, revstr, mapname=b'splicemap'): 324 def checkrevformat(self, revstr, mapname: bytes = b'splicemap') -> bool:
284 """revstr is a string that describes a revision in the given 325 """revstr is a string that describes a revision in the given
285 source control system. Return true if revstr has correct 326 source control system. Return true if revstr has correct
286 format. 327 format.
287 """ 328 """
288 return True 329 return True
289 330
290 331
291 class converter_sink: 332 class converter_sink:
292 """Conversion sink (target) interface""" 333 """Conversion sink (target) interface"""
293 334
294 def __init__(self, ui, repotype, path): 335 def __init__(self, ui: "uimod.ui", repotype: bytes, path: bytes) -> None:
295 """Initialize conversion sink (or raise NoRepo("message") 336 """Initialize conversion sink (or raise NoRepo("message")
296 exception if path is not a valid repository) 337 exception if path is not a valid repository)
297 338
298 created is a list of paths to remove if a fatal error occurs 339 created is a list of paths to remove if a fatal error occurs
299 later""" 340 later"""
357 was changed in a revision, even if there was no change. This method 398 was changed in a revision, even if there was no change. This method
358 tells the destination that we're using a filemap and that it should 399 tells the destination that we're using a filemap and that it should
359 filter empty revisions. 400 filter empty revisions.
360 """ 401 """
361 402
362 def before(self): 403 def before(self) -> None:
363 pass 404 pass
364 405
365 def after(self): 406 def after(self) -> None:
366 pass 407 pass
367 408
368 def putbookmarks(self, bookmarks): 409 def putbookmarks(self, bookmarks):
369 """Put bookmarks into sink. 410 """Put bookmarks into sink.
370 411
383 special cases.""" 424 special cases."""
384 raise NotImplementedError 425 raise NotImplementedError
385 426
386 427
387 class commandline: 428 class commandline:
388 def __init__(self, ui, command): 429 def __init__(self, ui: "uimod.ui", command: bytes) -> None:
389 self.ui = ui 430 self.ui = ui
390 self.command = command 431 self.command = command
391 432
392 def prerun(self): 433 def prerun(self) -> None:
393 pass 434 pass
394 435
395 def postrun(self): 436 def postrun(self) -> None:
396 pass 437 pass
397 438
398 def _cmdline(self, cmd, *args, **kwargs): 439 def _cmdline(self, cmd: bytes, *args: bytes, **kwargs) -> bytes:
399 kwargs = pycompat.byteskwargs(kwargs) 440 kwargs = pycompat.byteskwargs(kwargs)
400 cmdline = [self.command, cmd] + list(args) 441 cmdline = [self.command, cmd] + list(args)
401 for k, v in kwargs.items(): 442 for k, v in kwargs.items():
402 if len(k) == 1: 443 if len(k) == 1:
403 cmdline.append(b'-' + k) 444 cmdline.append(b'-' + k)
414 if not self.ui.debugflag: 455 if not self.ui.debugflag:
415 cmdline += [b'2>', pycompat.bytestr(os.devnull)] 456 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
416 cmdline = b' '.join(cmdline) 457 cmdline = b' '.join(cmdline)
417 return cmdline 458 return cmdline
418 459
419 def _run(self, cmd, *args, **kwargs): 460 def _run(self, cmd: bytes, *args: bytes, **kwargs):
420 def popen(cmdline): 461 def popen(cmdline):
421 p = subprocess.Popen( 462 p = subprocess.Popen(
422 procutil.tonativestr(cmdline), 463 procutil.tonativestr(cmdline),
423 shell=True, 464 shell=True,
424 bufsize=-1, 465 bufsize=-1,
427 ) 468 )
428 return p 469 return p
429 470
430 return self._dorun(popen, cmd, *args, **kwargs) 471 return self._dorun(popen, cmd, *args, **kwargs)
431 472
432 def _run2(self, cmd, *args, **kwargs): 473 def _run2(self, cmd: bytes, *args: bytes, **kwargs):
433 return self._dorun(procutil.popen2, cmd, *args, **kwargs) 474 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
434 475
435 def _run3(self, cmd, *args, **kwargs): 476 def _run3(self, cmd: bytes, *args: bytes, **kwargs):
436 return self._dorun(procutil.popen3, cmd, *args, **kwargs) 477 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
437 478
438 def _dorun(self, openfunc, cmd, *args, **kwargs): 479 def _dorun(self, openfunc, cmd: bytes, *args: bytes, **kwargs):
439 cmdline = self._cmdline(cmd, *args, **kwargs) 480 cmdline = self._cmdline(cmd, *args, **kwargs)
440 self.ui.debug(b'running: %s\n' % (cmdline,)) 481 self.ui.debug(b'running: %s\n' % (cmdline,))
441 self.prerun() 482 self.prerun()
442 try: 483 try:
443 return openfunc(cmdline) 484 return openfunc(cmdline)
444 finally: 485 finally:
445 self.postrun() 486 self.postrun()
446 487
447 def run(self, cmd, *args, **kwargs): 488 def run(self, cmd: bytes, *args: bytes, **kwargs):
448 p = self._run(cmd, *args, **kwargs) 489 p = self._run(cmd, *args, **kwargs)
449 output = p.communicate()[0] 490 output = p.communicate()[0]
450 self.ui.debug(output) 491 self.ui.debug(output)
451 return output, p.returncode 492 return output, p.returncode
452 493
453 def runlines(self, cmd, *args, **kwargs): 494 def runlines(self, cmd: bytes, *args: bytes, **kwargs):
454 p = self._run(cmd, *args, **kwargs) 495 p = self._run(cmd, *args, **kwargs)
455 output = p.stdout.readlines() 496 output = p.stdout.readlines()
456 p.wait() 497 p.wait()
457 self.ui.debug(b''.join(output)) 498 self.ui.debug(b''.join(output))
458 return output, p.returncode 499 return output, p.returncode
459 500
460 def checkexit(self, status, output=b''): 501 def checkexit(self, status, output: bytes = b'') -> None:
461 if status: 502 if status:
462 if output: 503 if output:
463 self.ui.warn(_(b'%s error:\n') % self.command) 504 self.ui.warn(_(b'%s error:\n') % self.command)
464 self.ui.warn(output) 505 self.ui.warn(output)
465 msg = procutil.explainexit(status) 506 msg = procutil.explainexit(status)
466 raise error.Abort(b'%s %s' % (self.command, msg)) 507 raise error.Abort(b'%s %s' % (self.command, msg))
467 508
468 def run0(self, cmd, *args, **kwargs): 509 def run0(self, cmd: bytes, *args: bytes, **kwargs):
469 output, status = self.run(cmd, *args, **kwargs) 510 output, status = self.run(cmd, *args, **kwargs)
470 self.checkexit(status, output) 511 self.checkexit(status, output)
471 return output 512 return output
472 513
473 def runlines0(self, cmd, *args, **kwargs): 514 def runlines0(self, cmd: bytes, *args: bytes, **kwargs):
474 output, status = self.runlines(cmd, *args, **kwargs) 515 output, status = self.runlines(cmd, *args, **kwargs)
475 self.checkexit(status, b''.join(output)) 516 self.checkexit(status, b''.join(output))
476 return output 517 return output
477 518
478 @propertycache 519 @propertycache
491 532
492 # Since ARG_MAX is for command line _and_ environment, lower our limit 533 # Since ARG_MAX is for command line _and_ environment, lower our limit
493 # (and make happy Windows shells while doing this). 534 # (and make happy Windows shells while doing this).
494 return argmax // 2 - 1 535 return argmax // 2 - 1
495 536
496 def _limit_arglist(self, arglist, cmd, *args, **kwargs): 537 def _limit_arglist(self, arglist, cmd: bytes, *args: bytes, **kwargs):
497 cmdlen = len(self._cmdline(cmd, *args, **kwargs)) 538 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
498 limit = self.argmax - cmdlen 539 limit = self.argmax - cmdlen
499 numbytes = 0 540 numbytes = 0
500 fl = [] 541 fl = []
501 for fn in arglist: 542 for fn in arglist:
508 fl = [fn] 549 fl = [fn]
509 numbytes = b 550 numbytes = b
510 if fl: 551 if fl:
511 yield fl 552 yield fl
512 553
513 def xargs(self, arglist, cmd, *args, **kwargs): 554 def xargs(self, arglist, cmd: bytes, *args: bytes, **kwargs):
514 for l in self._limit_arglist(arglist, cmd, *args, **kwargs): 555 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
515 self.run0(cmd, *(list(args) + l), **kwargs) 556 self.run0(cmd, *(list(args) + l), **kwargs)
516 557
517 558
518 class mapfile(dict): 559 class mapfile(dict):
519 def __init__(self, ui, path): 560 def __init__(self, ui: "uimod.ui", path: bytes) -> None:
520 super(mapfile, self).__init__() 561 super(mapfile, self).__init__()
521 self.ui = ui 562 self.ui = ui
522 self.path = path 563 self.path = path
523 self.fp = None 564 self.fp = None
524 self.order = [] 565 self.order = []
525 self._read() 566 self._read()
526 567
527 def _read(self): 568 def _read(self) -> None:
528 if not self.path: 569 if not self.path:
529 return 570 return
530 try: 571 try:
531 fp = open(self.path, b'rb') 572 fp = open(self.path, b'rb')
532 except FileNotFoundError: 573 except FileNotFoundError:
546 if key not in self: 587 if key not in self:
547 self.order.append(key) 588 self.order.append(key)
548 super(mapfile, self).__setitem__(key, value) 589 super(mapfile, self).__setitem__(key, value)
549 fp.close() 590 fp.close()
550 591
551 def __setitem__(self, key, value): 592 def __setitem__(self, key, value) -> None:
552 if self.fp is None: 593 if self.fp is None:
553 try: 594 try:
554 self.fp = open(self.path, b'ab') 595 self.fp = open(self.path, b'ab')
555 except IOError as err: 596 except IOError as err:
556 raise error.Abort( 597 raise error.Abort(
559 ) 600 )
560 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value))) 601 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
561 self.fp.flush() 602 self.fp.flush()
562 super(mapfile, self).__setitem__(key, value) 603 super(mapfile, self).__setitem__(key, value)
563 604
564 def close(self): 605 def close(self) -> None:
565 if self.fp: 606 if self.fp:
566 self.fp.close() 607 self.fp.close()
567 self.fp = None 608 self.fp = None
568 609
569 610