Mercurial > hg
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 |