Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/distlib/util.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
| author | shellac |
|---|---|
| date | Sat, 02 May 2020 07:14:21 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:26e78fe6e8c4 |
|---|---|
| 1 # | |
| 2 # Copyright (C) 2012-2017 The Python Software Foundation. | |
| 3 # See LICENSE.txt and CONTRIBUTORS.txt. | |
| 4 # | |
| 5 import codecs | |
| 6 from collections import deque | |
| 7 import contextlib | |
| 8 import csv | |
| 9 from glob import iglob as std_iglob | |
| 10 import io | |
| 11 import json | |
| 12 import logging | |
| 13 import os | |
| 14 import py_compile | |
| 15 import re | |
| 16 import socket | |
| 17 try: | |
| 18 import ssl | |
| 19 except ImportError: # pragma: no cover | |
| 20 ssl = None | |
| 21 import subprocess | |
| 22 import sys | |
| 23 import tarfile | |
| 24 import tempfile | |
| 25 import textwrap | |
| 26 | |
| 27 try: | |
| 28 import threading | |
| 29 except ImportError: # pragma: no cover | |
| 30 import dummy_threading as threading | |
| 31 import time | |
| 32 | |
| 33 from . import DistlibException | |
| 34 from .compat import (string_types, text_type, shutil, raw_input, StringIO, | |
| 35 cache_from_source, urlopen, urljoin, httplib, xmlrpclib, | |
| 36 splittype, HTTPHandler, BaseConfigurator, valid_ident, | |
| 37 Container, configparser, URLError, ZipFile, fsdecode, | |
| 38 unquote, urlparse) | |
| 39 | |
| 40 logger = logging.getLogger(__name__) | |
| 41 | |
| 42 # | |
| 43 # Requirement parsing code as per PEP 508 | |
| 44 # | |
| 45 | |
| 46 IDENTIFIER = re.compile(r'^([\w\.-]+)\s*') | |
| 47 VERSION_IDENTIFIER = re.compile(r'^([\w\.*+-]+)\s*') | |
| 48 COMPARE_OP = re.compile(r'^(<=?|>=?|={2,3}|[~!]=)\s*') | |
| 49 MARKER_OP = re.compile(r'^((<=?)|(>=?)|={2,3}|[~!]=|in|not\s+in)\s*') | |
| 50 OR = re.compile(r'^or\b\s*') | |
| 51 AND = re.compile(r'^and\b\s*') | |
| 52 NON_SPACE = re.compile(r'(\S+)\s*') | |
| 53 STRING_CHUNK = re.compile(r'([\s\w\.{}()*+#:;,/?!~`@$%^&=|<>\[\]-]+)') | |
| 54 | |
| 55 | |
| 56 def parse_marker(marker_string): | |
| 57 """ | |
| 58 Parse a marker string and return a dictionary containing a marker expression. | |
| 59 | |
| 60 The dictionary will contain keys "op", "lhs" and "rhs" for non-terminals in | |
| 61 the expression grammar, or strings. A string contained in quotes is to be | |
| 62 interpreted as a literal string, and a string not contained in quotes is a | |
| 63 variable (such as os_name). | |
| 64 """ | |
| 65 def marker_var(remaining): | |
| 66 # either identifier, or literal string | |
| 67 m = IDENTIFIER.match(remaining) | |
| 68 if m: | |
| 69 result = m.groups()[0] | |
| 70 remaining = remaining[m.end():] | |
| 71 elif not remaining: | |
| 72 raise SyntaxError('unexpected end of input') | |
| 73 else: | |
| 74 q = remaining[0] | |
| 75 if q not in '\'"': | |
| 76 raise SyntaxError('invalid expression: %s' % remaining) | |
| 77 oq = '\'"'.replace(q, '') | |
| 78 remaining = remaining[1:] | |
| 79 parts = [q] | |
| 80 while remaining: | |
| 81 # either a string chunk, or oq, or q to terminate | |
| 82 if remaining[0] == q: | |
| 83 break | |
| 84 elif remaining[0] == oq: | |
| 85 parts.append(oq) | |
| 86 remaining = remaining[1:] | |
| 87 else: | |
| 88 m = STRING_CHUNK.match(remaining) | |
| 89 if not m: | |
| 90 raise SyntaxError('error in string literal: %s' % remaining) | |
| 91 parts.append(m.groups()[0]) | |
| 92 remaining = remaining[m.end():] | |
| 93 else: | |
| 94 s = ''.join(parts) | |
| 95 raise SyntaxError('unterminated string: %s' % s) | |
| 96 parts.append(q) | |
| 97 result = ''.join(parts) | |
| 98 remaining = remaining[1:].lstrip() # skip past closing quote | |
| 99 return result, remaining | |
| 100 | |
| 101 def marker_expr(remaining): | |
| 102 if remaining and remaining[0] == '(': | |
| 103 result, remaining = marker(remaining[1:].lstrip()) | |
| 104 if remaining[0] != ')': | |
| 105 raise SyntaxError('unterminated parenthesis: %s' % remaining) | |
| 106 remaining = remaining[1:].lstrip() | |
| 107 else: | |
| 108 lhs, remaining = marker_var(remaining) | |
| 109 while remaining: | |
| 110 m = MARKER_OP.match(remaining) | |
| 111 if not m: | |
| 112 break | |
| 113 op = m.groups()[0] | |
| 114 remaining = remaining[m.end():] | |
| 115 rhs, remaining = marker_var(remaining) | |
| 116 lhs = {'op': op, 'lhs': lhs, 'rhs': rhs} | |
| 117 result = lhs | |
| 118 return result, remaining | |
| 119 | |
| 120 def marker_and(remaining): | |
| 121 lhs, remaining = marker_expr(remaining) | |
| 122 while remaining: | |
| 123 m = AND.match(remaining) | |
| 124 if not m: | |
| 125 break | |
| 126 remaining = remaining[m.end():] | |
| 127 rhs, remaining = marker_expr(remaining) | |
| 128 lhs = {'op': 'and', 'lhs': lhs, 'rhs': rhs} | |
| 129 return lhs, remaining | |
| 130 | |
| 131 def marker(remaining): | |
| 132 lhs, remaining = marker_and(remaining) | |
| 133 while remaining: | |
| 134 m = OR.match(remaining) | |
| 135 if not m: | |
| 136 break | |
| 137 remaining = remaining[m.end():] | |
| 138 rhs, remaining = marker_and(remaining) | |
| 139 lhs = {'op': 'or', 'lhs': lhs, 'rhs': rhs} | |
| 140 return lhs, remaining | |
| 141 | |
| 142 return marker(marker_string) | |
| 143 | |
| 144 | |
| 145 def parse_requirement(req): | |
| 146 """ | |
| 147 Parse a requirement passed in as a string. Return a Container | |
| 148 whose attributes contain the various parts of the requirement. | |
| 149 """ | |
| 150 remaining = req.strip() | |
| 151 if not remaining or remaining.startswith('#'): | |
| 152 return None | |
| 153 m = IDENTIFIER.match(remaining) | |
| 154 if not m: | |
| 155 raise SyntaxError('name expected: %s' % remaining) | |
| 156 distname = m.groups()[0] | |
| 157 remaining = remaining[m.end():] | |
| 158 extras = mark_expr = versions = uri = None | |
| 159 if remaining and remaining[0] == '[': | |
| 160 i = remaining.find(']', 1) | |
| 161 if i < 0: | |
| 162 raise SyntaxError('unterminated extra: %s' % remaining) | |
| 163 s = remaining[1:i] | |
| 164 remaining = remaining[i + 1:].lstrip() | |
| 165 extras = [] | |
| 166 while s: | |
| 167 m = IDENTIFIER.match(s) | |
| 168 if not m: | |
| 169 raise SyntaxError('malformed extra: %s' % s) | |
| 170 extras.append(m.groups()[0]) | |
| 171 s = s[m.end():] | |
| 172 if not s: | |
| 173 break | |
| 174 if s[0] != ',': | |
| 175 raise SyntaxError('comma expected in extras: %s' % s) | |
| 176 s = s[1:].lstrip() | |
| 177 if not extras: | |
| 178 extras = None | |
| 179 if remaining: | |
| 180 if remaining[0] == '@': | |
| 181 # it's a URI | |
| 182 remaining = remaining[1:].lstrip() | |
| 183 m = NON_SPACE.match(remaining) | |
| 184 if not m: | |
| 185 raise SyntaxError('invalid URI: %s' % remaining) | |
| 186 uri = m.groups()[0] | |
| 187 t = urlparse(uri) | |
| 188 # there are issues with Python and URL parsing, so this test | |
| 189 # is a bit crude. See bpo-20271, bpo-23505. Python doesn't | |
| 190 # always parse invalid URLs correctly - it should raise | |
| 191 # exceptions for malformed URLs | |
| 192 if not (t.scheme and t.netloc): | |
| 193 raise SyntaxError('Invalid URL: %s' % uri) | |
| 194 remaining = remaining[m.end():].lstrip() | |
| 195 else: | |
| 196 | |
| 197 def get_versions(ver_remaining): | |
| 198 """ | |
| 199 Return a list of operator, version tuples if any are | |
| 200 specified, else None. | |
| 201 """ | |
| 202 m = COMPARE_OP.match(ver_remaining) | |
| 203 versions = None | |
| 204 if m: | |
| 205 versions = [] | |
| 206 while True: | |
| 207 op = m.groups()[0] | |
| 208 ver_remaining = ver_remaining[m.end():] | |
| 209 m = VERSION_IDENTIFIER.match(ver_remaining) | |
| 210 if not m: | |
| 211 raise SyntaxError('invalid version: %s' % ver_remaining) | |
| 212 v = m.groups()[0] | |
| 213 versions.append((op, v)) | |
| 214 ver_remaining = ver_remaining[m.end():] | |
| 215 if not ver_remaining or ver_remaining[0] != ',': | |
| 216 break | |
| 217 ver_remaining = ver_remaining[1:].lstrip() | |
| 218 m = COMPARE_OP.match(ver_remaining) | |
| 219 if not m: | |
| 220 raise SyntaxError('invalid constraint: %s' % ver_remaining) | |
| 221 if not versions: | |
| 222 versions = None | |
| 223 return versions, ver_remaining | |
| 224 | |
| 225 if remaining[0] != '(': | |
| 226 versions, remaining = get_versions(remaining) | |
| 227 else: | |
| 228 i = remaining.find(')', 1) | |
| 229 if i < 0: | |
| 230 raise SyntaxError('unterminated parenthesis: %s' % remaining) | |
| 231 s = remaining[1:i] | |
| 232 remaining = remaining[i + 1:].lstrip() | |
| 233 # As a special diversion from PEP 508, allow a version number | |
| 234 # a.b.c in parentheses as a synonym for ~= a.b.c (because this | |
| 235 # is allowed in earlier PEPs) | |
| 236 if COMPARE_OP.match(s): | |
| 237 versions, _ = get_versions(s) | |
| 238 else: | |
| 239 m = VERSION_IDENTIFIER.match(s) | |
| 240 if not m: | |
| 241 raise SyntaxError('invalid constraint: %s' % s) | |
| 242 v = m.groups()[0] | |
| 243 s = s[m.end():].lstrip() | |
| 244 if s: | |
| 245 raise SyntaxError('invalid constraint: %s' % s) | |
| 246 versions = [('~=', v)] | |
| 247 | |
| 248 if remaining: | |
| 249 if remaining[0] != ';': | |
| 250 raise SyntaxError('invalid requirement: %s' % remaining) | |
| 251 remaining = remaining[1:].lstrip() | |
| 252 | |
| 253 mark_expr, remaining = parse_marker(remaining) | |
| 254 | |
| 255 if remaining and remaining[0] != '#': | |
| 256 raise SyntaxError('unexpected trailing data: %s' % remaining) | |
| 257 | |
| 258 if not versions: | |
| 259 rs = distname | |
| 260 else: | |
| 261 rs = '%s %s' % (distname, ', '.join(['%s %s' % con for con in versions])) | |
| 262 return Container(name=distname, extras=extras, constraints=versions, | |
| 263 marker=mark_expr, url=uri, requirement=rs) | |
| 264 | |
| 265 | |
| 266 def get_resources_dests(resources_root, rules): | |
| 267 """Find destinations for resources files""" | |
| 268 | |
| 269 def get_rel_path(root, path): | |
| 270 # normalizes and returns a lstripped-/-separated path | |
| 271 root = root.replace(os.path.sep, '/') | |
| 272 path = path.replace(os.path.sep, '/') | |
| 273 assert path.startswith(root) | |
| 274 return path[len(root):].lstrip('/') | |
| 275 | |
| 276 destinations = {} | |
| 277 for base, suffix, dest in rules: | |
| 278 prefix = os.path.join(resources_root, base) | |
| 279 for abs_base in iglob(prefix): | |
| 280 abs_glob = os.path.join(abs_base, suffix) | |
| 281 for abs_path in iglob(abs_glob): | |
| 282 resource_file = get_rel_path(resources_root, abs_path) | |
| 283 if dest is None: # remove the entry if it was here | |
| 284 destinations.pop(resource_file, None) | |
| 285 else: | |
| 286 rel_path = get_rel_path(abs_base, abs_path) | |
| 287 rel_dest = dest.replace(os.path.sep, '/').rstrip('/') | |
| 288 destinations[resource_file] = rel_dest + '/' + rel_path | |
| 289 return destinations | |
| 290 | |
| 291 | |
| 292 def in_venv(): | |
| 293 if hasattr(sys, 'real_prefix'): | |
| 294 # virtualenv venvs | |
| 295 result = True | |
| 296 else: | |
| 297 # PEP 405 venvs | |
| 298 result = sys.prefix != getattr(sys, 'base_prefix', sys.prefix) | |
| 299 return result | |
| 300 | |
| 301 | |
| 302 def get_executable(): | |
| 303 # The __PYVENV_LAUNCHER__ dance is apparently no longer needed, as | |
| 304 # changes to the stub launcher mean that sys.executable always points | |
| 305 # to the stub on OS X | |
| 306 # if sys.platform == 'darwin' and ('__PYVENV_LAUNCHER__' | |
| 307 # in os.environ): | |
| 308 # result = os.environ['__PYVENV_LAUNCHER__'] | |
| 309 # else: | |
| 310 # result = sys.executable | |
| 311 # return result | |
| 312 result = os.path.normcase(sys.executable) | |
| 313 if not isinstance(result, text_type): | |
| 314 result = fsdecode(result) | |
| 315 return result | |
| 316 | |
| 317 | |
| 318 def proceed(prompt, allowed_chars, error_prompt=None, default=None): | |
| 319 p = prompt | |
| 320 while True: | |
| 321 s = raw_input(p) | |
| 322 p = prompt | |
| 323 if not s and default: | |
| 324 s = default | |
| 325 if s: | |
| 326 c = s[0].lower() | |
| 327 if c in allowed_chars: | |
| 328 break | |
| 329 if error_prompt: | |
| 330 p = '%c: %s\n%s' % (c, error_prompt, prompt) | |
| 331 return c | |
| 332 | |
| 333 | |
| 334 def extract_by_key(d, keys): | |
| 335 if isinstance(keys, string_types): | |
| 336 keys = keys.split() | |
| 337 result = {} | |
| 338 for key in keys: | |
| 339 if key in d: | |
| 340 result[key] = d[key] | |
| 341 return result | |
| 342 | |
| 343 def read_exports(stream): | |
| 344 if sys.version_info[0] >= 3: | |
| 345 # needs to be a text stream | |
| 346 stream = codecs.getreader('utf-8')(stream) | |
| 347 # Try to load as JSON, falling back on legacy format | |
| 348 data = stream.read() | |
| 349 stream = StringIO(data) | |
| 350 try: | |
| 351 jdata = json.load(stream) | |
| 352 result = jdata['extensions']['python.exports']['exports'] | |
| 353 for group, entries in result.items(): | |
| 354 for k, v in entries.items(): | |
| 355 s = '%s = %s' % (k, v) | |
| 356 entry = get_export_entry(s) | |
| 357 assert entry is not None | |
| 358 entries[k] = entry | |
| 359 return result | |
| 360 except Exception: | |
| 361 stream.seek(0, 0) | |
| 362 | |
| 363 def read_stream(cp, stream): | |
| 364 if hasattr(cp, 'read_file'): | |
| 365 cp.read_file(stream) | |
| 366 else: | |
| 367 cp.readfp(stream) | |
| 368 | |
| 369 cp = configparser.ConfigParser() | |
| 370 try: | |
| 371 read_stream(cp, stream) | |
| 372 except configparser.MissingSectionHeaderError: | |
| 373 stream.close() | |
| 374 data = textwrap.dedent(data) | |
| 375 stream = StringIO(data) | |
| 376 read_stream(cp, stream) | |
| 377 | |
| 378 result = {} | |
| 379 for key in cp.sections(): | |
| 380 result[key] = entries = {} | |
| 381 for name, value in cp.items(key): | |
| 382 s = '%s = %s' % (name, value) | |
| 383 entry = get_export_entry(s) | |
| 384 assert entry is not None | |
| 385 #entry.dist = self | |
| 386 entries[name] = entry | |
| 387 return result | |
| 388 | |
| 389 | |
| 390 def write_exports(exports, stream): | |
| 391 if sys.version_info[0] >= 3: | |
| 392 # needs to be a text stream | |
| 393 stream = codecs.getwriter('utf-8')(stream) | |
| 394 cp = configparser.ConfigParser() | |
| 395 for k, v in exports.items(): | |
| 396 # TODO check k, v for valid values | |
| 397 cp.add_section(k) | |
| 398 for entry in v.values(): | |
| 399 if entry.suffix is None: | |
| 400 s = entry.prefix | |
| 401 else: | |
| 402 s = '%s:%s' % (entry.prefix, entry.suffix) | |
| 403 if entry.flags: | |
| 404 s = '%s [%s]' % (s, ', '.join(entry.flags)) | |
| 405 cp.set(k, entry.name, s) | |
| 406 cp.write(stream) | |
| 407 | |
| 408 | |
| 409 @contextlib.contextmanager | |
| 410 def tempdir(): | |
| 411 td = tempfile.mkdtemp() | |
| 412 try: | |
| 413 yield td | |
| 414 finally: | |
| 415 shutil.rmtree(td) | |
| 416 | |
| 417 @contextlib.contextmanager | |
| 418 def chdir(d): | |
| 419 cwd = os.getcwd() | |
| 420 try: | |
| 421 os.chdir(d) | |
| 422 yield | |
| 423 finally: | |
| 424 os.chdir(cwd) | |
| 425 | |
| 426 | |
| 427 @contextlib.contextmanager | |
| 428 def socket_timeout(seconds=15): | |
| 429 cto = socket.getdefaulttimeout() | |
| 430 try: | |
| 431 socket.setdefaulttimeout(seconds) | |
| 432 yield | |
| 433 finally: | |
| 434 socket.setdefaulttimeout(cto) | |
| 435 | |
| 436 | |
| 437 class cached_property(object): | |
| 438 def __init__(self, func): | |
| 439 self.func = func | |
| 440 #for attr in ('__name__', '__module__', '__doc__'): | |
| 441 # setattr(self, attr, getattr(func, attr, None)) | |
| 442 | |
| 443 def __get__(self, obj, cls=None): | |
| 444 if obj is None: | |
| 445 return self | |
| 446 value = self.func(obj) | |
| 447 object.__setattr__(obj, self.func.__name__, value) | |
| 448 #obj.__dict__[self.func.__name__] = value = self.func(obj) | |
| 449 return value | |
| 450 | |
| 451 def convert_path(pathname): | |
| 452 """Return 'pathname' as a name that will work on the native filesystem. | |
| 453 | |
| 454 The path is split on '/' and put back together again using the current | |
| 455 directory separator. Needed because filenames in the setup script are | |
| 456 always supplied in Unix style, and have to be converted to the local | |
| 457 convention before we can actually use them in the filesystem. Raises | |
| 458 ValueError on non-Unix-ish systems if 'pathname' either starts or | |
| 459 ends with a slash. | |
| 460 """ | |
| 461 if os.sep == '/': | |
| 462 return pathname | |
| 463 if not pathname: | |
| 464 return pathname | |
| 465 if pathname[0] == '/': | |
| 466 raise ValueError("path '%s' cannot be absolute" % pathname) | |
| 467 if pathname[-1] == '/': | |
| 468 raise ValueError("path '%s' cannot end with '/'" % pathname) | |
| 469 | |
| 470 paths = pathname.split('/') | |
| 471 while os.curdir in paths: | |
| 472 paths.remove(os.curdir) | |
| 473 if not paths: | |
| 474 return os.curdir | |
| 475 return os.path.join(*paths) | |
| 476 | |
| 477 | |
| 478 class FileOperator(object): | |
| 479 def __init__(self, dry_run=False): | |
| 480 self.dry_run = dry_run | |
| 481 self.ensured = set() | |
| 482 self._init_record() | |
| 483 | |
| 484 def _init_record(self): | |
| 485 self.record = False | |
| 486 self.files_written = set() | |
| 487 self.dirs_created = set() | |
| 488 | |
| 489 def record_as_written(self, path): | |
| 490 if self.record: | |
| 491 self.files_written.add(path) | |
| 492 | |
| 493 def newer(self, source, target): | |
| 494 """Tell if the target is newer than the source. | |
| 495 | |
| 496 Returns true if 'source' exists and is more recently modified than | |
| 497 'target', or if 'source' exists and 'target' doesn't. | |
| 498 | |
| 499 Returns false if both exist and 'target' is the same age or younger | |
| 500 than 'source'. Raise PackagingFileError if 'source' does not exist. | |
| 501 | |
| 502 Note that this test is not very accurate: files created in the same | |
| 503 second will have the same "age". | |
| 504 """ | |
| 505 if not os.path.exists(source): | |
| 506 raise DistlibException("file '%r' does not exist" % | |
| 507 os.path.abspath(source)) | |
| 508 if not os.path.exists(target): | |
| 509 return True | |
| 510 | |
| 511 return os.stat(source).st_mtime > os.stat(target).st_mtime | |
| 512 | |
| 513 def copy_file(self, infile, outfile, check=True): | |
| 514 """Copy a file respecting dry-run and force flags. | |
| 515 """ | |
| 516 self.ensure_dir(os.path.dirname(outfile)) | |
| 517 logger.info('Copying %s to %s', infile, outfile) | |
| 518 if not self.dry_run: | |
| 519 msg = None | |
| 520 if check: | |
| 521 if os.path.islink(outfile): | |
| 522 msg = '%s is a symlink' % outfile | |
| 523 elif os.path.exists(outfile) and not os.path.isfile(outfile): | |
| 524 msg = '%s is a non-regular file' % outfile | |
| 525 if msg: | |
| 526 raise ValueError(msg + ' which would be overwritten') | |
| 527 shutil.copyfile(infile, outfile) | |
| 528 self.record_as_written(outfile) | |
| 529 | |
| 530 def copy_stream(self, instream, outfile, encoding=None): | |
| 531 assert not os.path.isdir(outfile) | |
| 532 self.ensure_dir(os.path.dirname(outfile)) | |
| 533 logger.info('Copying stream %s to %s', instream, outfile) | |
| 534 if not self.dry_run: | |
| 535 if encoding is None: | |
| 536 outstream = open(outfile, 'wb') | |
| 537 else: | |
| 538 outstream = codecs.open(outfile, 'w', encoding=encoding) | |
| 539 try: | |
| 540 shutil.copyfileobj(instream, outstream) | |
| 541 finally: | |
| 542 outstream.close() | |
| 543 self.record_as_written(outfile) | |
| 544 | |
| 545 def write_binary_file(self, path, data): | |
| 546 self.ensure_dir(os.path.dirname(path)) | |
| 547 if not self.dry_run: | |
| 548 if os.path.exists(path): | |
| 549 os.remove(path) | |
| 550 with open(path, 'wb') as f: | |
| 551 f.write(data) | |
| 552 self.record_as_written(path) | |
| 553 | |
| 554 def write_text_file(self, path, data, encoding): | |
| 555 self.write_binary_file(path, data.encode(encoding)) | |
| 556 | |
| 557 def set_mode(self, bits, mask, files): | |
| 558 if os.name == 'posix' or (os.name == 'java' and os._name == 'posix'): | |
| 559 # Set the executable bits (owner, group, and world) on | |
| 560 # all the files specified. | |
| 561 for f in files: | |
| 562 if self.dry_run: | |
| 563 logger.info("changing mode of %s", f) | |
| 564 else: | |
| 565 mode = (os.stat(f).st_mode | bits) & mask | |
| 566 logger.info("changing mode of %s to %o", f, mode) | |
| 567 os.chmod(f, mode) | |
| 568 | |
| 569 set_executable_mode = lambda s, f: s.set_mode(0o555, 0o7777, f) | |
| 570 | |
| 571 def ensure_dir(self, path): | |
| 572 path = os.path.abspath(path) | |
| 573 if path not in self.ensured and not os.path.exists(path): | |
| 574 self.ensured.add(path) | |
| 575 d, f = os.path.split(path) | |
| 576 self.ensure_dir(d) | |
| 577 logger.info('Creating %s' % path) | |
| 578 if not self.dry_run: | |
| 579 os.mkdir(path) | |
| 580 if self.record: | |
| 581 self.dirs_created.add(path) | |
| 582 | |
| 583 def byte_compile(self, path, optimize=False, force=False, prefix=None, hashed_invalidation=False): | |
| 584 dpath = cache_from_source(path, not optimize) | |
| 585 logger.info('Byte-compiling %s to %s', path, dpath) | |
| 586 if not self.dry_run: | |
| 587 if force or self.newer(path, dpath): | |
| 588 if not prefix: | |
| 589 diagpath = None | |
| 590 else: | |
| 591 assert path.startswith(prefix) | |
| 592 diagpath = path[len(prefix):] | |
| 593 compile_kwargs = {} | |
| 594 if hashed_invalidation and hasattr(py_compile, 'PycInvalidationMode'): | |
| 595 compile_kwargs['invalidation_mode'] = py_compile.PycInvalidationMode.CHECKED_HASH | |
| 596 py_compile.compile(path, dpath, diagpath, True, **compile_kwargs) # raise error | |
| 597 self.record_as_written(dpath) | |
| 598 return dpath | |
| 599 | |
| 600 def ensure_removed(self, path): | |
| 601 if os.path.exists(path): | |
| 602 if os.path.isdir(path) and not os.path.islink(path): | |
| 603 logger.debug('Removing directory tree at %s', path) | |
| 604 if not self.dry_run: | |
| 605 shutil.rmtree(path) | |
| 606 if self.record: | |
| 607 if path in self.dirs_created: | |
| 608 self.dirs_created.remove(path) | |
| 609 else: | |
| 610 if os.path.islink(path): | |
| 611 s = 'link' | |
| 612 else: | |
| 613 s = 'file' | |
| 614 logger.debug('Removing %s %s', s, path) | |
| 615 if not self.dry_run: | |
| 616 os.remove(path) | |
| 617 if self.record: | |
| 618 if path in self.files_written: | |
| 619 self.files_written.remove(path) | |
| 620 | |
| 621 def is_writable(self, path): | |
| 622 result = False | |
| 623 while not result: | |
| 624 if os.path.exists(path): | |
| 625 result = os.access(path, os.W_OK) | |
| 626 break | |
| 627 parent = os.path.dirname(path) | |
| 628 if parent == path: | |
| 629 break | |
| 630 path = parent | |
| 631 return result | |
| 632 | |
| 633 def commit(self): | |
| 634 """ | |
| 635 Commit recorded changes, turn off recording, return | |
| 636 changes. | |
| 637 """ | |
| 638 assert self.record | |
| 639 result = self.files_written, self.dirs_created | |
| 640 self._init_record() | |
| 641 return result | |
| 642 | |
| 643 def rollback(self): | |
| 644 if not self.dry_run: | |
| 645 for f in list(self.files_written): | |
| 646 if os.path.exists(f): | |
| 647 os.remove(f) | |
| 648 # dirs should all be empty now, except perhaps for | |
| 649 # __pycache__ subdirs | |
| 650 # reverse so that subdirs appear before their parents | |
| 651 dirs = sorted(self.dirs_created, reverse=True) | |
| 652 for d in dirs: | |
| 653 flist = os.listdir(d) | |
| 654 if flist: | |
| 655 assert flist == ['__pycache__'] | |
| 656 sd = os.path.join(d, flist[0]) | |
| 657 os.rmdir(sd) | |
| 658 os.rmdir(d) # should fail if non-empty | |
| 659 self._init_record() | |
| 660 | |
| 661 def resolve(module_name, dotted_path): | |
| 662 if module_name in sys.modules: | |
| 663 mod = sys.modules[module_name] | |
| 664 else: | |
| 665 mod = __import__(module_name) | |
| 666 if dotted_path is None: | |
| 667 result = mod | |
| 668 else: | |
| 669 parts = dotted_path.split('.') | |
| 670 result = getattr(mod, parts.pop(0)) | |
| 671 for p in parts: | |
| 672 result = getattr(result, p) | |
| 673 return result | |
| 674 | |
| 675 | |
| 676 class ExportEntry(object): | |
| 677 def __init__(self, name, prefix, suffix, flags): | |
| 678 self.name = name | |
| 679 self.prefix = prefix | |
| 680 self.suffix = suffix | |
| 681 self.flags = flags | |
| 682 | |
| 683 @cached_property | |
| 684 def value(self): | |
| 685 return resolve(self.prefix, self.suffix) | |
| 686 | |
| 687 def __repr__(self): # pragma: no cover | |
| 688 return '<ExportEntry %s = %s:%s %s>' % (self.name, self.prefix, | |
| 689 self.suffix, self.flags) | |
| 690 | |
| 691 def __eq__(self, other): | |
| 692 if not isinstance(other, ExportEntry): | |
| 693 result = False | |
| 694 else: | |
| 695 result = (self.name == other.name and | |
| 696 self.prefix == other.prefix and | |
| 697 self.suffix == other.suffix and | |
| 698 self.flags == other.flags) | |
| 699 return result | |
| 700 | |
| 701 __hash__ = object.__hash__ | |
| 702 | |
| 703 | |
| 704 ENTRY_RE = re.compile(r'''(?P<name>(\w|[-.+])+) | |
| 705 \s*=\s*(?P<callable>(\w+)([:\.]\w+)*) | |
| 706 \s*(\[\s*(?P<flags>[\w-]+(=\w+)?(,\s*\w+(=\w+)?)*)\s*\])? | |
| 707 ''', re.VERBOSE) | |
| 708 | |
| 709 def get_export_entry(specification): | |
| 710 m = ENTRY_RE.search(specification) | |
| 711 if not m: | |
| 712 result = None | |
| 713 if '[' in specification or ']' in specification: | |
| 714 raise DistlibException("Invalid specification " | |
| 715 "'%s'" % specification) | |
| 716 else: | |
| 717 d = m.groupdict() | |
| 718 name = d['name'] | |
| 719 path = d['callable'] | |
| 720 colons = path.count(':') | |
| 721 if colons == 0: | |
| 722 prefix, suffix = path, None | |
| 723 else: | |
| 724 if colons != 1: | |
| 725 raise DistlibException("Invalid specification " | |
| 726 "'%s'" % specification) | |
| 727 prefix, suffix = path.split(':') | |
| 728 flags = d['flags'] | |
| 729 if flags is None: | |
| 730 if '[' in specification or ']' in specification: | |
| 731 raise DistlibException("Invalid specification " | |
| 732 "'%s'" % specification) | |
| 733 flags = [] | |
| 734 else: | |
| 735 flags = [f.strip() for f in flags.split(',')] | |
| 736 result = ExportEntry(name, prefix, suffix, flags) | |
| 737 return result | |
| 738 | |
| 739 | |
| 740 def get_cache_base(suffix=None): | |
| 741 """ | |
| 742 Return the default base location for distlib caches. If the directory does | |
| 743 not exist, it is created. Use the suffix provided for the base directory, | |
| 744 and default to '.distlib' if it isn't provided. | |
| 745 | |
| 746 On Windows, if LOCALAPPDATA is defined in the environment, then it is | |
| 747 assumed to be a directory, and will be the parent directory of the result. | |
| 748 On POSIX, and on Windows if LOCALAPPDATA is not defined, the user's home | |
| 749 directory - using os.expanduser('~') - will be the parent directory of | |
| 750 the result. | |
| 751 | |
| 752 The result is just the directory '.distlib' in the parent directory as | |
| 753 determined above, or with the name specified with ``suffix``. | |
| 754 """ | |
| 755 if suffix is None: | |
| 756 suffix = '.distlib' | |
| 757 if os.name == 'nt' and 'LOCALAPPDATA' in os.environ: | |
| 758 result = os.path.expandvars('$localappdata') | |
| 759 else: | |
| 760 # Assume posix, or old Windows | |
| 761 result = os.path.expanduser('~') | |
| 762 # we use 'isdir' instead of 'exists', because we want to | |
| 763 # fail if there's a file with that name | |
| 764 if os.path.isdir(result): | |
| 765 usable = os.access(result, os.W_OK) | |
| 766 if not usable: | |
| 767 logger.warning('Directory exists but is not writable: %s', result) | |
| 768 else: | |
| 769 try: | |
| 770 os.makedirs(result) | |
| 771 usable = True | |
| 772 except OSError: | |
| 773 logger.warning('Unable to create %s', result, exc_info=True) | |
| 774 usable = False | |
| 775 if not usable: | |
| 776 result = tempfile.mkdtemp() | |
| 777 logger.warning('Default location unusable, using %s', result) | |
| 778 return os.path.join(result, suffix) | |
| 779 | |
| 780 | |
| 781 def path_to_cache_dir(path): | |
| 782 """ | |
| 783 Convert an absolute path to a directory name for use in a cache. | |
| 784 | |
| 785 The algorithm used is: | |
| 786 | |
| 787 #. On Windows, any ``':'`` in the drive is replaced with ``'---'``. | |
| 788 #. Any occurrence of ``os.sep`` is replaced with ``'--'``. | |
| 789 #. ``'.cache'`` is appended. | |
| 790 """ | |
| 791 d, p = os.path.splitdrive(os.path.abspath(path)) | |
| 792 if d: | |
| 793 d = d.replace(':', '---') | |
| 794 p = p.replace(os.sep, '--') | |
| 795 return d + p + '.cache' | |
| 796 | |
| 797 | |
| 798 def ensure_slash(s): | |
| 799 if not s.endswith('/'): | |
| 800 return s + '/' | |
| 801 return s | |
| 802 | |
| 803 | |
| 804 def parse_credentials(netloc): | |
| 805 username = password = None | |
| 806 if '@' in netloc: | |
| 807 prefix, netloc = netloc.rsplit('@', 1) | |
| 808 if ':' not in prefix: | |
| 809 username = prefix | |
| 810 else: | |
| 811 username, password = prefix.split(':', 1) | |
| 812 if username: | |
| 813 username = unquote(username) | |
| 814 if password: | |
| 815 password = unquote(password) | |
| 816 return username, password, netloc | |
| 817 | |
| 818 | |
| 819 def get_process_umask(): | |
| 820 result = os.umask(0o22) | |
| 821 os.umask(result) | |
| 822 return result | |
| 823 | |
| 824 def is_string_sequence(seq): | |
| 825 result = True | |
| 826 i = None | |
| 827 for i, s in enumerate(seq): | |
| 828 if not isinstance(s, string_types): | |
| 829 result = False | |
| 830 break | |
| 831 assert i is not None | |
| 832 return result | |
| 833 | |
| 834 PROJECT_NAME_AND_VERSION = re.compile('([a-z0-9_]+([.-][a-z_][a-z0-9_]*)*)-' | |
| 835 '([a-z0-9_.+-]+)', re.I) | |
| 836 PYTHON_VERSION = re.compile(r'-py(\d\.?\d?)') | |
| 837 | |
| 838 | |
| 839 def split_filename(filename, project_name=None): | |
| 840 """ | |
| 841 Extract name, version, python version from a filename (no extension) | |
| 842 | |
| 843 Return name, version, pyver or None | |
| 844 """ | |
| 845 result = None | |
| 846 pyver = None | |
| 847 filename = unquote(filename).replace(' ', '-') | |
| 848 m = PYTHON_VERSION.search(filename) | |
| 849 if m: | |
| 850 pyver = m.group(1) | |
| 851 filename = filename[:m.start()] | |
| 852 if project_name and len(filename) > len(project_name) + 1: | |
| 853 m = re.match(re.escape(project_name) + r'\b', filename) | |
| 854 if m: | |
| 855 n = m.end() | |
| 856 result = filename[:n], filename[n + 1:], pyver | |
| 857 if result is None: | |
| 858 m = PROJECT_NAME_AND_VERSION.match(filename) | |
| 859 if m: | |
| 860 result = m.group(1), m.group(3), pyver | |
| 861 return result | |
| 862 | |
| 863 # Allow spaces in name because of legacy dists like "Twisted Core" | |
| 864 NAME_VERSION_RE = re.compile(r'(?P<name>[\w .-]+)\s*' | |
| 865 r'\(\s*(?P<ver>[^\s)]+)\)$') | |
| 866 | |
| 867 def parse_name_and_version(p): | |
| 868 """ | |
| 869 A utility method used to get name and version from a string. | |
| 870 | |
| 871 From e.g. a Provides-Dist value. | |
| 872 | |
| 873 :param p: A value in a form 'foo (1.0)' | |
| 874 :return: The name and version as a tuple. | |
| 875 """ | |
| 876 m = NAME_VERSION_RE.match(p) | |
| 877 if not m: | |
| 878 raise DistlibException('Ill-formed name/version string: \'%s\'' % p) | |
| 879 d = m.groupdict() | |
| 880 return d['name'].strip().lower(), d['ver'] | |
| 881 | |
| 882 def get_extras(requested, available): | |
| 883 result = set() | |
| 884 requested = set(requested or []) | |
| 885 available = set(available or []) | |
| 886 if '*' in requested: | |
| 887 requested.remove('*') | |
| 888 result |= available | |
| 889 for r in requested: | |
| 890 if r == '-': | |
| 891 result.add(r) | |
| 892 elif r.startswith('-'): | |
| 893 unwanted = r[1:] | |
| 894 if unwanted not in available: | |
| 895 logger.warning('undeclared extra: %s' % unwanted) | |
| 896 if unwanted in result: | |
| 897 result.remove(unwanted) | |
| 898 else: | |
| 899 if r not in available: | |
| 900 logger.warning('undeclared extra: %s' % r) | |
| 901 result.add(r) | |
| 902 return result | |
| 903 # | |
| 904 # Extended metadata functionality | |
| 905 # | |
| 906 | |
| 907 def _get_external_data(url): | |
| 908 result = {} | |
| 909 try: | |
| 910 # urlopen might fail if it runs into redirections, | |
| 911 # because of Python issue #13696. Fixed in locators | |
| 912 # using a custom redirect handler. | |
| 913 resp = urlopen(url) | |
| 914 headers = resp.info() | |
| 915 ct = headers.get('Content-Type') | |
| 916 if not ct.startswith('application/json'): | |
| 917 logger.debug('Unexpected response for JSON request: %s', ct) | |
| 918 else: | |
| 919 reader = codecs.getreader('utf-8')(resp) | |
| 920 #data = reader.read().decode('utf-8') | |
| 921 #result = json.loads(data) | |
| 922 result = json.load(reader) | |
| 923 except Exception as e: | |
| 924 logger.exception('Failed to get external data for %s: %s', url, e) | |
| 925 return result | |
| 926 | |
| 927 _external_data_base_url = 'https://www.red-dove.com/pypi/projects/' | |
| 928 | |
| 929 def get_project_data(name): | |
| 930 url = '%s/%s/project.json' % (name[0].upper(), name) | |
| 931 url = urljoin(_external_data_base_url, url) | |
| 932 result = _get_external_data(url) | |
| 933 return result | |
| 934 | |
| 935 def get_package_data(name, version): | |
| 936 url = '%s/%s/package-%s.json' % (name[0].upper(), name, version) | |
| 937 url = urljoin(_external_data_base_url, url) | |
| 938 return _get_external_data(url) | |
| 939 | |
| 940 | |
| 941 class Cache(object): | |
| 942 """ | |
| 943 A class implementing a cache for resources that need to live in the file system | |
| 944 e.g. shared libraries. This class was moved from resources to here because it | |
| 945 could be used by other modules, e.g. the wheel module. | |
| 946 """ | |
| 947 | |
| 948 def __init__(self, base): | |
| 949 """ | |
| 950 Initialise an instance. | |
| 951 | |
| 952 :param base: The base directory where the cache should be located. | |
| 953 """ | |
| 954 # we use 'isdir' instead of 'exists', because we want to | |
| 955 # fail if there's a file with that name | |
| 956 if not os.path.isdir(base): # pragma: no cover | |
| 957 os.makedirs(base) | |
| 958 if (os.stat(base).st_mode & 0o77) != 0: | |
| 959 logger.warning('Directory \'%s\' is not private', base) | |
| 960 self.base = os.path.abspath(os.path.normpath(base)) | |
| 961 | |
| 962 def prefix_to_dir(self, prefix): | |
| 963 """ | |
| 964 Converts a resource prefix to a directory name in the cache. | |
| 965 """ | |
| 966 return path_to_cache_dir(prefix) | |
| 967 | |
| 968 def clear(self): | |
| 969 """ | |
| 970 Clear the cache. | |
| 971 """ | |
| 972 not_removed = [] | |
| 973 for fn in os.listdir(self.base): | |
| 974 fn = os.path.join(self.base, fn) | |
| 975 try: | |
| 976 if os.path.islink(fn) or os.path.isfile(fn): | |
| 977 os.remove(fn) | |
| 978 elif os.path.isdir(fn): | |
| 979 shutil.rmtree(fn) | |
| 980 except Exception: | |
| 981 not_removed.append(fn) | |
| 982 return not_removed | |
| 983 | |
| 984 | |
| 985 class EventMixin(object): | |
| 986 """ | |
| 987 A very simple publish/subscribe system. | |
| 988 """ | |
| 989 def __init__(self): | |
| 990 self._subscribers = {} | |
| 991 | |
| 992 def add(self, event, subscriber, append=True): | |
| 993 """ | |
| 994 Add a subscriber for an event. | |
| 995 | |
| 996 :param event: The name of an event. | |
| 997 :param subscriber: The subscriber to be added (and called when the | |
| 998 event is published). | |
| 999 :param append: Whether to append or prepend the subscriber to an | |
| 1000 existing subscriber list for the event. | |
| 1001 """ | |
| 1002 subs = self._subscribers | |
| 1003 if event not in subs: | |
| 1004 subs[event] = deque([subscriber]) | |
| 1005 else: | |
| 1006 sq = subs[event] | |
| 1007 if append: | |
| 1008 sq.append(subscriber) | |
| 1009 else: | |
| 1010 sq.appendleft(subscriber) | |
| 1011 | |
| 1012 def remove(self, event, subscriber): | |
| 1013 """ | |
| 1014 Remove a subscriber for an event. | |
| 1015 | |
| 1016 :param event: The name of an event. | |
| 1017 :param subscriber: The subscriber to be removed. | |
| 1018 """ | |
| 1019 subs = self._subscribers | |
| 1020 if event not in subs: | |
| 1021 raise ValueError('No subscribers: %r' % event) | |
| 1022 subs[event].remove(subscriber) | |
| 1023 | |
| 1024 def get_subscribers(self, event): | |
| 1025 """ | |
| 1026 Return an iterator for the subscribers for an event. | |
| 1027 :param event: The event to return subscribers for. | |
| 1028 """ | |
| 1029 return iter(self._subscribers.get(event, ())) | |
| 1030 | |
| 1031 def publish(self, event, *args, **kwargs): | |
| 1032 """ | |
| 1033 Publish a event and return a list of values returned by its | |
| 1034 subscribers. | |
| 1035 | |
| 1036 :param event: The event to publish. | |
| 1037 :param args: The positional arguments to pass to the event's | |
| 1038 subscribers. | |
| 1039 :param kwargs: The keyword arguments to pass to the event's | |
| 1040 subscribers. | |
| 1041 """ | |
| 1042 result = [] | |
| 1043 for subscriber in self.get_subscribers(event): | |
| 1044 try: | |
| 1045 value = subscriber(event, *args, **kwargs) | |
| 1046 except Exception: | |
| 1047 logger.exception('Exception during event publication') | |
| 1048 value = None | |
| 1049 result.append(value) | |
| 1050 logger.debug('publish %s: args = %s, kwargs = %s, result = %s', | |
| 1051 event, args, kwargs, result) | |
| 1052 return result | |
| 1053 | |
| 1054 # | |
| 1055 # Simple sequencing | |
| 1056 # | |
| 1057 class Sequencer(object): | |
| 1058 def __init__(self): | |
| 1059 self._preds = {} | |
| 1060 self._succs = {} | |
| 1061 self._nodes = set() # nodes with no preds/succs | |
| 1062 | |
| 1063 def add_node(self, node): | |
| 1064 self._nodes.add(node) | |
| 1065 | |
| 1066 def remove_node(self, node, edges=False): | |
| 1067 if node in self._nodes: | |
| 1068 self._nodes.remove(node) | |
| 1069 if edges: | |
| 1070 for p in set(self._preds.get(node, ())): | |
| 1071 self.remove(p, node) | |
| 1072 for s in set(self._succs.get(node, ())): | |
| 1073 self.remove(node, s) | |
| 1074 # Remove empties | |
| 1075 for k, v in list(self._preds.items()): | |
| 1076 if not v: | |
| 1077 del self._preds[k] | |
| 1078 for k, v in list(self._succs.items()): | |
| 1079 if not v: | |
| 1080 del self._succs[k] | |
| 1081 | |
| 1082 def add(self, pred, succ): | |
| 1083 assert pred != succ | |
| 1084 self._preds.setdefault(succ, set()).add(pred) | |
| 1085 self._succs.setdefault(pred, set()).add(succ) | |
| 1086 | |
| 1087 def remove(self, pred, succ): | |
| 1088 assert pred != succ | |
| 1089 try: | |
| 1090 preds = self._preds[succ] | |
| 1091 succs = self._succs[pred] | |
| 1092 except KeyError: # pragma: no cover | |
| 1093 raise ValueError('%r not a successor of anything' % succ) | |
| 1094 try: | |
| 1095 preds.remove(pred) | |
| 1096 succs.remove(succ) | |
| 1097 except KeyError: # pragma: no cover | |
| 1098 raise ValueError('%r not a successor of %r' % (succ, pred)) | |
| 1099 | |
| 1100 def is_step(self, step): | |
| 1101 return (step in self._preds or step in self._succs or | |
| 1102 step in self._nodes) | |
| 1103 | |
| 1104 def get_steps(self, final): | |
| 1105 if not self.is_step(final): | |
| 1106 raise ValueError('Unknown: %r' % final) | |
| 1107 result = [] | |
| 1108 todo = [] | |
| 1109 seen = set() | |
| 1110 todo.append(final) | |
| 1111 while todo: | |
| 1112 step = todo.pop(0) | |
| 1113 if step in seen: | |
| 1114 # if a step was already seen, | |
| 1115 # move it to the end (so it will appear earlier | |
| 1116 # when reversed on return) ... but not for the | |
| 1117 # final step, as that would be confusing for | |
| 1118 # users | |
| 1119 if step != final: | |
| 1120 result.remove(step) | |
| 1121 result.append(step) | |
| 1122 else: | |
| 1123 seen.add(step) | |
| 1124 result.append(step) | |
| 1125 preds = self._preds.get(step, ()) | |
| 1126 todo.extend(preds) | |
| 1127 return reversed(result) | |
| 1128 | |
| 1129 @property | |
| 1130 def strong_connections(self): | |
| 1131 #http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm | |
| 1132 index_counter = [0] | |
| 1133 stack = [] | |
| 1134 lowlinks = {} | |
| 1135 index = {} | |
| 1136 result = [] | |
| 1137 | |
| 1138 graph = self._succs | |
| 1139 | |
| 1140 def strongconnect(node): | |
| 1141 # set the depth index for this node to the smallest unused index | |
| 1142 index[node] = index_counter[0] | |
| 1143 lowlinks[node] = index_counter[0] | |
| 1144 index_counter[0] += 1 | |
| 1145 stack.append(node) | |
| 1146 | |
| 1147 # Consider successors | |
| 1148 try: | |
| 1149 successors = graph[node] | |
| 1150 except Exception: | |
| 1151 successors = [] | |
| 1152 for successor in successors: | |
| 1153 if successor not in lowlinks: | |
| 1154 # Successor has not yet been visited | |
| 1155 strongconnect(successor) | |
| 1156 lowlinks[node] = min(lowlinks[node],lowlinks[successor]) | |
| 1157 elif successor in stack: | |
| 1158 # the successor is in the stack and hence in the current | |
| 1159 # strongly connected component (SCC) | |
| 1160 lowlinks[node] = min(lowlinks[node],index[successor]) | |
| 1161 | |
| 1162 # If `node` is a root node, pop the stack and generate an SCC | |
| 1163 if lowlinks[node] == index[node]: | |
| 1164 connected_component = [] | |
| 1165 | |
| 1166 while True: | |
| 1167 successor = stack.pop() | |
| 1168 connected_component.append(successor) | |
| 1169 if successor == node: break | |
| 1170 component = tuple(connected_component) | |
| 1171 # storing the result | |
| 1172 result.append(component) | |
| 1173 | |
| 1174 for node in graph: | |
| 1175 if node not in lowlinks: | |
| 1176 strongconnect(node) | |
| 1177 | |
| 1178 return result | |
| 1179 | |
| 1180 @property | |
| 1181 def dot(self): | |
| 1182 result = ['digraph G {'] | |
| 1183 for succ in self._preds: | |
| 1184 preds = self._preds[succ] | |
| 1185 for pred in preds: | |
| 1186 result.append(' %s -> %s;' % (pred, succ)) | |
| 1187 for node in self._nodes: | |
| 1188 result.append(' %s;' % node) | |
| 1189 result.append('}') | |
| 1190 return '\n'.join(result) | |
| 1191 | |
| 1192 # | |
| 1193 # Unarchiving functionality for zip, tar, tgz, tbz, whl | |
| 1194 # | |
| 1195 | |
| 1196 ARCHIVE_EXTENSIONS = ('.tar.gz', '.tar.bz2', '.tar', '.zip', | |
| 1197 '.tgz', '.tbz', '.whl') | |
| 1198 | |
| 1199 def unarchive(archive_filename, dest_dir, format=None, check=True): | |
| 1200 | |
| 1201 def check_path(path): | |
| 1202 if not isinstance(path, text_type): | |
| 1203 path = path.decode('utf-8') | |
| 1204 p = os.path.abspath(os.path.join(dest_dir, path)) | |
| 1205 if not p.startswith(dest_dir) or p[plen] != os.sep: | |
| 1206 raise ValueError('path outside destination: %r' % p) | |
| 1207 | |
| 1208 dest_dir = os.path.abspath(dest_dir) | |
| 1209 plen = len(dest_dir) | |
| 1210 archive = None | |
| 1211 if format is None: | |
| 1212 if archive_filename.endswith(('.zip', '.whl')): | |
| 1213 format = 'zip' | |
| 1214 elif archive_filename.endswith(('.tar.gz', '.tgz')): | |
| 1215 format = 'tgz' | |
| 1216 mode = 'r:gz' | |
| 1217 elif archive_filename.endswith(('.tar.bz2', '.tbz')): | |
| 1218 format = 'tbz' | |
| 1219 mode = 'r:bz2' | |
| 1220 elif archive_filename.endswith('.tar'): | |
| 1221 format = 'tar' | |
| 1222 mode = 'r' | |
| 1223 else: # pragma: no cover | |
| 1224 raise ValueError('Unknown format for %r' % archive_filename) | |
| 1225 try: | |
| 1226 if format == 'zip': | |
| 1227 archive = ZipFile(archive_filename, 'r') | |
| 1228 if check: | |
| 1229 names = archive.namelist() | |
| 1230 for name in names: | |
| 1231 check_path(name) | |
| 1232 else: | |
| 1233 archive = tarfile.open(archive_filename, mode) | |
| 1234 if check: | |
| 1235 names = archive.getnames() | |
| 1236 for name in names: | |
| 1237 check_path(name) | |
| 1238 if format != 'zip' and sys.version_info[0] < 3: | |
| 1239 # See Python issue 17153. If the dest path contains Unicode, | |
| 1240 # tarfile extraction fails on Python 2.x if a member path name | |
| 1241 # contains non-ASCII characters - it leads to an implicit | |
| 1242 # bytes -> unicode conversion using ASCII to decode. | |
| 1243 for tarinfo in archive.getmembers(): | |
| 1244 if not isinstance(tarinfo.name, text_type): | |
| 1245 tarinfo.name = tarinfo.name.decode('utf-8') | |
| 1246 archive.extractall(dest_dir) | |
| 1247 | |
| 1248 finally: | |
| 1249 if archive: | |
| 1250 archive.close() | |
| 1251 | |
| 1252 | |
| 1253 def zip_dir(directory): | |
| 1254 """zip a directory tree into a BytesIO object""" | |
| 1255 result = io.BytesIO() | |
| 1256 dlen = len(directory) | |
| 1257 with ZipFile(result, "w") as zf: | |
| 1258 for root, dirs, files in os.walk(directory): | |
| 1259 for name in files: | |
| 1260 full = os.path.join(root, name) | |
| 1261 rel = root[dlen:] | |
| 1262 dest = os.path.join(rel, name) | |
| 1263 zf.write(full, dest) | |
| 1264 return result | |
| 1265 | |
| 1266 # | |
| 1267 # Simple progress bar | |
| 1268 # | |
| 1269 | |
| 1270 UNITS = ('', 'K', 'M', 'G','T','P') | |
| 1271 | |
| 1272 | |
| 1273 class Progress(object): | |
| 1274 unknown = 'UNKNOWN' | |
| 1275 | |
| 1276 def __init__(self, minval=0, maxval=100): | |
| 1277 assert maxval is None or maxval >= minval | |
| 1278 self.min = self.cur = minval | |
| 1279 self.max = maxval | |
| 1280 self.started = None | |
| 1281 self.elapsed = 0 | |
| 1282 self.done = False | |
| 1283 | |
| 1284 def update(self, curval): | |
| 1285 assert self.min <= curval | |
| 1286 assert self.max is None or curval <= self.max | |
| 1287 self.cur = curval | |
| 1288 now = time.time() | |
| 1289 if self.started is None: | |
| 1290 self.started = now | |
| 1291 else: | |
| 1292 self.elapsed = now - self.started | |
| 1293 | |
| 1294 def increment(self, incr): | |
| 1295 assert incr >= 0 | |
| 1296 self.update(self.cur + incr) | |
| 1297 | |
| 1298 def start(self): | |
| 1299 self.update(self.min) | |
| 1300 return self | |
| 1301 | |
| 1302 def stop(self): | |
| 1303 if self.max is not None: | |
| 1304 self.update(self.max) | |
| 1305 self.done = True | |
| 1306 | |
| 1307 @property | |
| 1308 def maximum(self): | |
| 1309 return self.unknown if self.max is None else self.max | |
| 1310 | |
| 1311 @property | |
| 1312 def percentage(self): | |
| 1313 if self.done: | |
| 1314 result = '100 %' | |
| 1315 elif self.max is None: | |
| 1316 result = ' ?? %' | |
| 1317 else: | |
| 1318 v = 100.0 * (self.cur - self.min) / (self.max - self.min) | |
| 1319 result = '%3d %%' % v | |
| 1320 return result | |
| 1321 | |
| 1322 def format_duration(self, duration): | |
| 1323 if (duration <= 0) and self.max is None or self.cur == self.min: | |
| 1324 result = '??:??:??' | |
| 1325 #elif duration < 1: | |
| 1326 # result = '--:--:--' | |
| 1327 else: | |
| 1328 result = time.strftime('%H:%M:%S', time.gmtime(duration)) | |
| 1329 return result | |
| 1330 | |
| 1331 @property | |
| 1332 def ETA(self): | |
| 1333 if self.done: | |
| 1334 prefix = 'Done' | |
| 1335 t = self.elapsed | |
| 1336 #import pdb; pdb.set_trace() | |
| 1337 else: | |
| 1338 prefix = 'ETA ' | |
| 1339 if self.max is None: | |
| 1340 t = -1 | |
| 1341 elif self.elapsed == 0 or (self.cur == self.min): | |
| 1342 t = 0 | |
| 1343 else: | |
| 1344 #import pdb; pdb.set_trace() | |
| 1345 t = float(self.max - self.min) | |
| 1346 t /= self.cur - self.min | |
| 1347 t = (t - 1) * self.elapsed | |
| 1348 return '%s: %s' % (prefix, self.format_duration(t)) | |
| 1349 | |
| 1350 @property | |
| 1351 def speed(self): | |
| 1352 if self.elapsed == 0: | |
| 1353 result = 0.0 | |
| 1354 else: | |
| 1355 result = (self.cur - self.min) / self.elapsed | |
| 1356 for unit in UNITS: | |
| 1357 if result < 1000: | |
| 1358 break | |
| 1359 result /= 1000.0 | |
| 1360 return '%d %sB/s' % (result, unit) | |
| 1361 | |
| 1362 # | |
| 1363 # Glob functionality | |
| 1364 # | |
| 1365 | |
| 1366 RICH_GLOB = re.compile(r'\{([^}]*)\}') | |
| 1367 _CHECK_RECURSIVE_GLOB = re.compile(r'[^/\\,{]\*\*|\*\*[^/\\,}]') | |
| 1368 _CHECK_MISMATCH_SET = re.compile(r'^[^{]*\}|\{[^}]*$') | |
| 1369 | |
| 1370 | |
| 1371 def iglob(path_glob): | |
| 1372 """Extended globbing function that supports ** and {opt1,opt2,opt3}.""" | |
| 1373 if _CHECK_RECURSIVE_GLOB.search(path_glob): | |
| 1374 msg = """invalid glob %r: recursive glob "**" must be used alone""" | |
| 1375 raise ValueError(msg % path_glob) | |
| 1376 if _CHECK_MISMATCH_SET.search(path_glob): | |
| 1377 msg = """invalid glob %r: mismatching set marker '{' or '}'""" | |
| 1378 raise ValueError(msg % path_glob) | |
| 1379 return _iglob(path_glob) | |
| 1380 | |
| 1381 | |
| 1382 def _iglob(path_glob): | |
| 1383 rich_path_glob = RICH_GLOB.split(path_glob, 1) | |
| 1384 if len(rich_path_glob) > 1: | |
| 1385 assert len(rich_path_glob) == 3, rich_path_glob | |
| 1386 prefix, set, suffix = rich_path_glob | |
| 1387 for item in set.split(','): | |
| 1388 for path in _iglob(''.join((prefix, item, suffix))): | |
| 1389 yield path | |
| 1390 else: | |
| 1391 if '**' not in path_glob: | |
| 1392 for item in std_iglob(path_glob): | |
| 1393 yield item | |
| 1394 else: | |
| 1395 prefix, radical = path_glob.split('**', 1) | |
| 1396 if prefix == '': | |
| 1397 prefix = '.' | |
| 1398 if radical == '': | |
| 1399 radical = '*' | |
| 1400 else: | |
| 1401 # we support both | |
| 1402 radical = radical.lstrip('/') | |
| 1403 radical = radical.lstrip('\\') | |
| 1404 for path, dir, files in os.walk(prefix): | |
| 1405 path = os.path.normpath(path) | |
| 1406 for fn in _iglob(os.path.join(path, radical)): | |
| 1407 yield fn | |
| 1408 | |
| 1409 if ssl: | |
| 1410 from .compat import (HTTPSHandler as BaseHTTPSHandler, match_hostname, | |
| 1411 CertificateError) | |
| 1412 | |
| 1413 | |
| 1414 # | |
| 1415 # HTTPSConnection which verifies certificates/matches domains | |
| 1416 # | |
| 1417 | |
| 1418 class HTTPSConnection(httplib.HTTPSConnection): | |
| 1419 ca_certs = None # set this to the path to the certs file (.pem) | |
| 1420 check_domain = True # only used if ca_certs is not None | |
| 1421 | |
| 1422 # noinspection PyPropertyAccess | |
| 1423 def connect(self): | |
| 1424 sock = socket.create_connection((self.host, self.port), self.timeout) | |
| 1425 if getattr(self, '_tunnel_host', False): | |
| 1426 self.sock = sock | |
| 1427 self._tunnel() | |
| 1428 | |
| 1429 if not hasattr(ssl, 'SSLContext'): | |
| 1430 # For 2.x | |
| 1431 if self.ca_certs: | |
| 1432 cert_reqs = ssl.CERT_REQUIRED | |
| 1433 else: | |
| 1434 cert_reqs = ssl.CERT_NONE | |
| 1435 self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, | |
| 1436 cert_reqs=cert_reqs, | |
| 1437 ssl_version=ssl.PROTOCOL_SSLv23, | |
| 1438 ca_certs=self.ca_certs) | |
| 1439 else: # pragma: no cover | |
| 1440 context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) | |
| 1441 if hasattr(ssl, 'OP_NO_SSLv2'): | |
| 1442 context.options |= ssl.OP_NO_SSLv2 | |
| 1443 if self.cert_file: | |
| 1444 context.load_cert_chain(self.cert_file, self.key_file) | |
| 1445 kwargs = {} | |
| 1446 if self.ca_certs: | |
| 1447 context.verify_mode = ssl.CERT_REQUIRED | |
| 1448 context.load_verify_locations(cafile=self.ca_certs) | |
| 1449 if getattr(ssl, 'HAS_SNI', False): | |
| 1450 kwargs['server_hostname'] = self.host | |
| 1451 self.sock = context.wrap_socket(sock, **kwargs) | |
| 1452 if self.ca_certs and self.check_domain: | |
| 1453 try: | |
| 1454 match_hostname(self.sock.getpeercert(), self.host) | |
| 1455 logger.debug('Host verified: %s', self.host) | |
| 1456 except CertificateError: # pragma: no cover | |
| 1457 self.sock.shutdown(socket.SHUT_RDWR) | |
| 1458 self.sock.close() | |
| 1459 raise | |
| 1460 | |
| 1461 class HTTPSHandler(BaseHTTPSHandler): | |
| 1462 def __init__(self, ca_certs, check_domain=True): | |
| 1463 BaseHTTPSHandler.__init__(self) | |
| 1464 self.ca_certs = ca_certs | |
| 1465 self.check_domain = check_domain | |
| 1466 | |
| 1467 def _conn_maker(self, *args, **kwargs): | |
| 1468 """ | |
| 1469 This is called to create a connection instance. Normally you'd | |
| 1470 pass a connection class to do_open, but it doesn't actually check for | |
| 1471 a class, and just expects a callable. As long as we behave just as a | |
| 1472 constructor would have, we should be OK. If it ever changes so that | |
| 1473 we *must* pass a class, we'll create an UnsafeHTTPSConnection class | |
| 1474 which just sets check_domain to False in the class definition, and | |
| 1475 choose which one to pass to do_open. | |
| 1476 """ | |
| 1477 result = HTTPSConnection(*args, **kwargs) | |
| 1478 if self.ca_certs: | |
| 1479 result.ca_certs = self.ca_certs | |
| 1480 result.check_domain = self.check_domain | |
| 1481 return result | |
| 1482 | |
| 1483 def https_open(self, req): | |
| 1484 try: | |
| 1485 return self.do_open(self._conn_maker, req) | |
| 1486 except URLError as e: | |
| 1487 if 'certificate verify failed' in str(e.reason): | |
| 1488 raise CertificateError('Unable to verify server certificate ' | |
| 1489 'for %s' % req.host) | |
| 1490 else: | |
| 1491 raise | |
| 1492 | |
| 1493 # | |
| 1494 # To prevent against mixing HTTP traffic with HTTPS (examples: A Man-In-The- | |
| 1495 # Middle proxy using HTTP listens on port 443, or an index mistakenly serves | |
| 1496 # HTML containing a http://xyz link when it should be https://xyz), | |
| 1497 # you can use the following handler class, which does not allow HTTP traffic. | |
| 1498 # | |
| 1499 # It works by inheriting from HTTPHandler - so build_opener won't add a | |
| 1500 # handler for HTTP itself. | |
| 1501 # | |
| 1502 class HTTPSOnlyHandler(HTTPSHandler, HTTPHandler): | |
| 1503 def http_open(self, req): | |
| 1504 raise URLError('Unexpected HTTP request on what should be a secure ' | |
| 1505 'connection: %s' % req) | |
| 1506 | |
| 1507 # | |
| 1508 # XML-RPC with timeouts | |
| 1509 # | |
| 1510 | |
| 1511 _ver_info = sys.version_info[:2] | |
| 1512 | |
| 1513 if _ver_info == (2, 6): | |
| 1514 class HTTP(httplib.HTTP): | |
| 1515 def __init__(self, host='', port=None, **kwargs): | |
| 1516 if port == 0: # 0 means use port 0, not the default port | |
| 1517 port = None | |
| 1518 self._setup(self._connection_class(host, port, **kwargs)) | |
| 1519 | |
| 1520 | |
| 1521 if ssl: | |
| 1522 class HTTPS(httplib.HTTPS): | |
| 1523 def __init__(self, host='', port=None, **kwargs): | |
| 1524 if port == 0: # 0 means use port 0, not the default port | |
| 1525 port = None | |
| 1526 self._setup(self._connection_class(host, port, **kwargs)) | |
| 1527 | |
| 1528 | |
| 1529 class Transport(xmlrpclib.Transport): | |
| 1530 def __init__(self, timeout, use_datetime=0): | |
| 1531 self.timeout = timeout | |
| 1532 xmlrpclib.Transport.__init__(self, use_datetime) | |
| 1533 | |
| 1534 def make_connection(self, host): | |
| 1535 h, eh, x509 = self.get_host_info(host) | |
| 1536 if _ver_info == (2, 6): | |
| 1537 result = HTTP(h, timeout=self.timeout) | |
| 1538 else: | |
| 1539 if not self._connection or host != self._connection[0]: | |
| 1540 self._extra_headers = eh | |
| 1541 self._connection = host, httplib.HTTPConnection(h) | |
| 1542 result = self._connection[1] | |
| 1543 return result | |
| 1544 | |
| 1545 if ssl: | |
| 1546 class SafeTransport(xmlrpclib.SafeTransport): | |
| 1547 def __init__(self, timeout, use_datetime=0): | |
| 1548 self.timeout = timeout | |
| 1549 xmlrpclib.SafeTransport.__init__(self, use_datetime) | |
| 1550 | |
| 1551 def make_connection(self, host): | |
| 1552 h, eh, kwargs = self.get_host_info(host) | |
| 1553 if not kwargs: | |
| 1554 kwargs = {} | |
| 1555 kwargs['timeout'] = self.timeout | |
| 1556 if _ver_info == (2, 6): | |
| 1557 result = HTTPS(host, None, **kwargs) | |
| 1558 else: | |
| 1559 if not self._connection or host != self._connection[0]: | |
| 1560 self._extra_headers = eh | |
| 1561 self._connection = host, httplib.HTTPSConnection(h, None, | |
| 1562 **kwargs) | |
| 1563 result = self._connection[1] | |
| 1564 return result | |
| 1565 | |
| 1566 | |
| 1567 class ServerProxy(xmlrpclib.ServerProxy): | |
| 1568 def __init__(self, uri, **kwargs): | |
| 1569 self.timeout = timeout = kwargs.pop('timeout', None) | |
| 1570 # The above classes only come into play if a timeout | |
| 1571 # is specified | |
| 1572 if timeout is not None: | |
| 1573 scheme, _ = splittype(uri) | |
| 1574 use_datetime = kwargs.get('use_datetime', 0) | |
| 1575 if scheme == 'https': | |
| 1576 tcls = SafeTransport | |
| 1577 else: | |
| 1578 tcls = Transport | |
| 1579 kwargs['transport'] = t = tcls(timeout, use_datetime=use_datetime) | |
| 1580 self.transport = t | |
| 1581 xmlrpclib.ServerProxy.__init__(self, uri, **kwargs) | |
| 1582 | |
| 1583 # | |
| 1584 # CSV functionality. This is provided because on 2.x, the csv module can't | |
| 1585 # handle Unicode. However, we need to deal with Unicode in e.g. RECORD files. | |
| 1586 # | |
| 1587 | |
| 1588 def _csv_open(fn, mode, **kwargs): | |
| 1589 if sys.version_info[0] < 3: | |
| 1590 mode += 'b' | |
| 1591 else: | |
| 1592 kwargs['newline'] = '' | |
| 1593 # Python 3 determines encoding from locale. Force 'utf-8' | |
| 1594 # file encoding to match other forced utf-8 encoding | |
| 1595 kwargs['encoding'] = 'utf-8' | |
| 1596 return open(fn, mode, **kwargs) | |
| 1597 | |
| 1598 | |
| 1599 class CSVBase(object): | |
| 1600 defaults = { | |
| 1601 'delimiter': str(','), # The strs are used because we need native | |
| 1602 'quotechar': str('"'), # str in the csv API (2.x won't take | |
| 1603 'lineterminator': str('\n') # Unicode) | |
| 1604 } | |
| 1605 | |
| 1606 def __enter__(self): | |
| 1607 return self | |
| 1608 | |
| 1609 def __exit__(self, *exc_info): | |
| 1610 self.stream.close() | |
| 1611 | |
| 1612 | |
| 1613 class CSVReader(CSVBase): | |
| 1614 def __init__(self, **kwargs): | |
| 1615 if 'stream' in kwargs: | |
| 1616 stream = kwargs['stream'] | |
| 1617 if sys.version_info[0] >= 3: | |
| 1618 # needs to be a text stream | |
| 1619 stream = codecs.getreader('utf-8')(stream) | |
| 1620 self.stream = stream | |
| 1621 else: | |
| 1622 self.stream = _csv_open(kwargs['path'], 'r') | |
| 1623 self.reader = csv.reader(self.stream, **self.defaults) | |
| 1624 | |
| 1625 def __iter__(self): | |
| 1626 return self | |
| 1627 | |
| 1628 def next(self): | |
| 1629 result = next(self.reader) | |
| 1630 if sys.version_info[0] < 3: | |
| 1631 for i, item in enumerate(result): | |
| 1632 if not isinstance(item, text_type): | |
| 1633 result[i] = item.decode('utf-8') | |
| 1634 return result | |
| 1635 | |
| 1636 __next__ = next | |
| 1637 | |
| 1638 class CSVWriter(CSVBase): | |
| 1639 def __init__(self, fn, **kwargs): | |
| 1640 self.stream = _csv_open(fn, 'w') | |
| 1641 self.writer = csv.writer(self.stream, **self.defaults) | |
| 1642 | |
| 1643 def writerow(self, row): | |
| 1644 if sys.version_info[0] < 3: | |
| 1645 r = [] | |
| 1646 for item in row: | |
| 1647 if isinstance(item, text_type): | |
| 1648 item = item.encode('utf-8') | |
| 1649 r.append(item) | |
| 1650 row = r | |
| 1651 self.writer.writerow(row) | |
| 1652 | |
| 1653 # | |
| 1654 # Configurator functionality | |
| 1655 # | |
| 1656 | |
| 1657 class Configurator(BaseConfigurator): | |
| 1658 | |
| 1659 value_converters = dict(BaseConfigurator.value_converters) | |
| 1660 value_converters['inc'] = 'inc_convert' | |
| 1661 | |
| 1662 def __init__(self, config, base=None): | |
| 1663 super(Configurator, self).__init__(config) | |
| 1664 self.base = base or os.getcwd() | |
| 1665 | |
| 1666 def configure_custom(self, config): | |
| 1667 def convert(o): | |
| 1668 if isinstance(o, (list, tuple)): | |
| 1669 result = type(o)([convert(i) for i in o]) | |
| 1670 elif isinstance(o, dict): | |
| 1671 if '()' in o: | |
| 1672 result = self.configure_custom(o) | |
| 1673 else: | |
| 1674 result = {} | |
| 1675 for k in o: | |
| 1676 result[k] = convert(o[k]) | |
| 1677 else: | |
| 1678 result = self.convert(o) | |
| 1679 return result | |
| 1680 | |
| 1681 c = config.pop('()') | |
| 1682 if not callable(c): | |
| 1683 c = self.resolve(c) | |
| 1684 props = config.pop('.', None) | |
| 1685 # Check for valid identifiers | |
| 1686 args = config.pop('[]', ()) | |
| 1687 if args: | |
| 1688 args = tuple([convert(o) for o in args]) | |
| 1689 items = [(k, convert(config[k])) for k in config if valid_ident(k)] | |
| 1690 kwargs = dict(items) | |
| 1691 result = c(*args, **kwargs) | |
| 1692 if props: | |
| 1693 for n, v in props.items(): | |
| 1694 setattr(result, n, convert(v)) | |
| 1695 return result | |
| 1696 | |
| 1697 def __getitem__(self, key): | |
| 1698 result = self.config[key] | |
| 1699 if isinstance(result, dict) and '()' in result: | |
| 1700 self.config[key] = result = self.configure_custom(result) | |
| 1701 return result | |
| 1702 | |
| 1703 def inc_convert(self, value): | |
| 1704 """Default converter for the inc:// protocol.""" | |
| 1705 if not os.path.isabs(value): | |
| 1706 value = os.path.join(self.base, value) | |
| 1707 with codecs.open(value, 'r', encoding='utf-8') as f: | |
| 1708 result = json.load(f) | |
| 1709 return result | |
| 1710 | |
| 1711 | |
| 1712 class SubprocessMixin(object): | |
| 1713 """ | |
| 1714 Mixin for running subprocesses and capturing their output | |
| 1715 """ | |
| 1716 def __init__(self, verbose=False, progress=None): | |
| 1717 self.verbose = verbose | |
| 1718 self.progress = progress | |
| 1719 | |
| 1720 def reader(self, stream, context): | |
| 1721 """ | |
| 1722 Read lines from a subprocess' output stream and either pass to a progress | |
| 1723 callable (if specified) or write progress information to sys.stderr. | |
| 1724 """ | |
| 1725 progress = self.progress | |
| 1726 verbose = self.verbose | |
| 1727 while True: | |
| 1728 s = stream.readline() | |
| 1729 if not s: | |
| 1730 break | |
| 1731 if progress is not None: | |
| 1732 progress(s, context) | |
| 1733 else: | |
| 1734 if not verbose: | |
| 1735 sys.stderr.write('.') | |
| 1736 else: | |
| 1737 sys.stderr.write(s.decode('utf-8')) | |
| 1738 sys.stderr.flush() | |
| 1739 stream.close() | |
| 1740 | |
| 1741 def run_command(self, cmd, **kwargs): | |
| 1742 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, | |
| 1743 stderr=subprocess.PIPE, **kwargs) | |
| 1744 t1 = threading.Thread(target=self.reader, args=(p.stdout, 'stdout')) | |
| 1745 t1.start() | |
| 1746 t2 = threading.Thread(target=self.reader, args=(p.stderr, 'stderr')) | |
| 1747 t2.start() | |
| 1748 p.wait() | |
| 1749 t1.join() | |
| 1750 t2.join() | |
| 1751 if self.progress is not None: | |
| 1752 self.progress('done.', 'main') | |
| 1753 elif self.verbose: | |
| 1754 sys.stderr.write('done.\n') | |
| 1755 return p | |
| 1756 | |
| 1757 | |
| 1758 def normalize_name(name): | |
| 1759 """Normalize a python package name a la PEP 503""" | |
| 1760 # https://www.python.org/dev/peps/pep-0503/#normalized-names | |
| 1761 return re.sub('[-_.]+', '-', name).lower() |
