1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2012 The Python Software Foundation. 4# See LICENSE.txt and CONTRIBUTORS.txt. 5# 6"""Utility functions for copying and archiving files and directory trees. 7 8XXX The functions here don't copy the resource fork or other metadata on Mac. 9 10""" 11 12import os 13import sys 14import stat 15from os.path import abspath 16import fnmatch 17try: 18 from collections.abc import Callable 19except ImportError: 20 from collections import Callable 21import errno 22from . import tarfile 23 24try: 25 import bz2 26 _BZ2_SUPPORTED = True 27except ImportError: 28 _BZ2_SUPPORTED = False 29 30try: 31 from pwd import getpwnam 32except ImportError: 33 getpwnam = None 34 35try: 36 from grp import getgrnam 37except ImportError: 38 getgrnam = None 39 40__all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2", 41 "copytree", "move", "rmtree", "Error", "SpecialFileError", 42 "ExecError", "make_archive", "get_archive_formats", 43 "register_archive_format", "unregister_archive_format", 44 "get_unpack_formats", "register_unpack_format", 45 "unregister_unpack_format", "unpack_archive", "ignore_patterns"] 46 47class Error(EnvironmentError): 48 pass 49 50class SpecialFileError(EnvironmentError): 51 """Raised when trying to do a kind of operation (e.g. copying) which is 52 not supported on a special file (e.g. a named pipe)""" 53 54class ExecError(EnvironmentError): 55 """Raised when a command could not be executed""" 56 57class ReadError(EnvironmentError): 58 """Raised when an archive cannot be read""" 59 60class RegistryError(Exception): 61 """Raised when a registry operation with the archiving 62 and unpacking registries fails""" 63 64 65try: 66 WindowsError 67except NameError: 68 WindowsError = None 69 70def copyfileobj(fsrc, fdst, length=16*1024): 71 """copy data from file-like object fsrc to file-like object fdst""" 72 while 1: 73 buf = fsrc.read(length) 74 if not buf: 75 break 76 fdst.write(buf) 77 78def _samefile(src, dst): 79 # Macintosh, Unix. 80 if hasattr(os.path, 'samefile'): 81 try: 82 return os.path.samefile(src, dst) 83 except OSError: 84 return False 85 86 # All other platforms: check for same pathname. 87 return (os.path.normcase(os.path.abspath(src)) == 88 os.path.normcase(os.path.abspath(dst))) 89 90def copyfile(src, dst): 91 """Copy data from src to dst""" 92 if _samefile(src, dst): 93 raise Error("`%s` and `%s` are the same file" % (src, dst)) 94 95 for fn in [src, dst]: 96 try: 97 st = os.stat(fn) 98 except OSError: 99 # File most likely does not exist 100 pass 101 else: 102 # XXX What about other special files? (sockets, devices...) 103 if stat.S_ISFIFO(st.st_mode): 104 raise SpecialFileError("`%s` is a named pipe" % fn) 105 106 with open(src, 'rb') as fsrc: 107 with open(dst, 'wb') as fdst: 108 copyfileobj(fsrc, fdst) 109 110def copymode(src, dst): 111 """Copy mode bits from src to dst""" 112 if hasattr(os, 'chmod'): 113 st = os.stat(src) 114 mode = stat.S_IMODE(st.st_mode) 115 os.chmod(dst, mode) 116 117def copystat(src, dst): 118 """Copy all stat info (mode bits, atime, mtime, flags) from src to dst""" 119 st = os.stat(src) 120 mode = stat.S_IMODE(st.st_mode) 121 if hasattr(os, 'utime'): 122 os.utime(dst, (st.st_atime, st.st_mtime)) 123 if hasattr(os, 'chmod'): 124 os.chmod(dst, mode) 125 if hasattr(os, 'chflags') and hasattr(st, 'st_flags'): 126 try: 127 os.chflags(dst, st.st_flags) 128 except OSError as why: 129 if (not hasattr(errno, 'EOPNOTSUPP') or 130 why.errno != errno.EOPNOTSUPP): 131 raise 132 133def copy(src, dst): 134 """Copy data and mode bits ("cp src dst"). 135 136 The destination may be a directory. 137 138 """ 139 if os.path.isdir(dst): 140 dst = os.path.join(dst, os.path.basename(src)) 141 copyfile(src, dst) 142 copymode(src, dst) 143 144def copy2(src, dst): 145 """Copy data and all stat info ("cp -p src dst"). 146 147 The destination may be a directory. 148 149 """ 150 if os.path.isdir(dst): 151 dst = os.path.join(dst, os.path.basename(src)) 152 copyfile(src, dst) 153 copystat(src, dst) 154 155def ignore_patterns(*patterns): 156 """Function that can be used as copytree() ignore parameter. 157 158 Patterns is a sequence of glob-style patterns 159 that are used to exclude files""" 160 def _ignore_patterns(path, names): 161 ignored_names = [] 162 for pattern in patterns: 163 ignored_names.extend(fnmatch.filter(names, pattern)) 164 return set(ignored_names) 165 return _ignore_patterns 166 167def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, 168 ignore_dangling_symlinks=False): 169 """Recursively copy a directory tree. 170 171 The destination directory must not already exist. 172 If exception(s) occur, an Error is raised with a list of reasons. 173 174 If the optional symlinks flag is true, symbolic links in the 175 source tree result in symbolic links in the destination tree; if 176 it is false, the contents of the files pointed to by symbolic 177 links are copied. If the file pointed by the symlink doesn't 178 exist, an exception will be added in the list of errors raised in 179 an Error exception at the end of the copy process. 180 181 You can set the optional ignore_dangling_symlinks flag to true if you 182 want to silence this exception. Notice that this has no effect on 183 platforms that don't support os.symlink. 184 185 The optional ignore argument is a callable. If given, it 186 is called with the `src` parameter, which is the directory 187 being visited by copytree(), and `names` which is the list of 188 `src` contents, as returned by os.listdir(): 189 190 callable(src, names) -> ignored_names 191 192 Since copytree() is called recursively, the callable will be 193 called once for each directory that is copied. It returns a 194 list of names relative to the `src` directory that should 195 not be copied. 196 197 The optional copy_function argument is a callable that will be used 198 to copy each file. It will be called with the source path and the 199 destination path as arguments. By default, copy2() is used, but any 200 function that supports the same signature (like copy()) can be used. 201 202 """ 203 names = os.listdir(src) 204 if ignore is not None: 205 ignored_names = ignore(src, names) 206 else: 207 ignored_names = set() 208 209 os.makedirs(dst) 210 errors = [] 211 for name in names: 212 if name in ignored_names: 213 continue 214 srcname = os.path.join(src, name) 215 dstname = os.path.join(dst, name) 216 try: 217 if os.path.islink(srcname): 218 linkto = os.readlink(srcname) 219 if symlinks: 220 os.symlink(linkto, dstname) 221 else: 222 # ignore dangling symlink if the flag is on 223 if not os.path.exists(linkto) and ignore_dangling_symlinks: 224 continue 225 # otherwise let the copy occurs. copy2 will raise an error 226 copy_function(srcname, dstname) 227 elif os.path.isdir(srcname): 228 copytree(srcname, dstname, symlinks, ignore, copy_function) 229 else: 230 # Will raise a SpecialFileError for unsupported file types 231 copy_function(srcname, dstname) 232 # catch the Error from the recursive copytree so that we can 233 # continue with other files 234 except Error as err: 235 errors.extend(err.args[0]) 236 except EnvironmentError as why: 237 errors.append((srcname, dstname, str(why))) 238 try: 239 copystat(src, dst) 240 except OSError as why: 241 if WindowsError is not None and isinstance(why, WindowsError): 242 # Copying file access times may fail on Windows 243 pass 244 else: 245 errors.extend((src, dst, str(why))) 246 if errors: 247 raise Error(errors) 248 249def rmtree(path, ignore_errors=False, onerror=None): 250 """Recursively delete a directory tree. 251 252 If ignore_errors is set, errors are ignored; otherwise, if onerror 253 is set, it is called to handle the error with arguments (func, 254 path, exc_info) where func is os.listdir, os.remove, or os.rmdir; 255 path is the argument to that function that caused it to fail; and 256 exc_info is a tuple returned by sys.exc_info(). If ignore_errors 257 is false and onerror is None, an exception is raised. 258 259 """ 260 if ignore_errors: 261 def onerror(*args): 262 pass 263 elif onerror is None: 264 def onerror(*args): 265 raise 266 try: 267 if os.path.islink(path): 268 # symlinks to directories are forbidden, see bug #1669 269 raise OSError("Cannot call rmtree on a symbolic link") 270 except OSError: 271 onerror(os.path.islink, path, sys.exc_info()) 272 # can't continue even if onerror hook returns 273 return 274 names = [] 275 try: 276 names = os.listdir(path) 277 except os.error: 278 onerror(os.listdir, path, sys.exc_info()) 279 for name in names: 280 fullname = os.path.join(path, name) 281 try: 282 mode = os.lstat(fullname).st_mode 283 except os.error: 284 mode = 0 285 if stat.S_ISDIR(mode): 286 rmtree(fullname, ignore_errors, onerror) 287 else: 288 try: 289 os.remove(fullname) 290 except os.error: 291 onerror(os.remove, fullname, sys.exc_info()) 292 try: 293 os.rmdir(path) 294 except os.error: 295 onerror(os.rmdir, path, sys.exc_info()) 296 297 298def _basename(path): 299 # A basename() variant which first strips the trailing slash, if present. 300 # Thus we always get the last component of the path, even for directories. 301 return os.path.basename(path.rstrip(os.path.sep)) 302 303def move(src, dst): 304 """Recursively move a file or directory to another location. This is 305 similar to the Unix "mv" command. 306 307 If the destination is a directory or a symlink to a directory, the source 308 is moved inside the directory. The destination path must not already 309 exist. 310 311 If the destination already exists but is not a directory, it may be 312 overwritten depending on os.rename() semantics. 313 314 If the destination is on our current filesystem, then rename() is used. 315 Otherwise, src is copied to the destination and then removed. 316 A lot more could be done here... A look at a mv.c shows a lot of 317 the issues this implementation glosses over. 318 319 """ 320 real_dst = dst 321 if os.path.isdir(dst): 322 if _samefile(src, dst): 323 # We might be on a case insensitive filesystem, 324 # perform the rename anyway. 325 os.rename(src, dst) 326 return 327 328 real_dst = os.path.join(dst, _basename(src)) 329 if os.path.exists(real_dst): 330 raise Error("Destination path '%s' already exists" % real_dst) 331 try: 332 os.rename(src, real_dst) 333 except OSError: 334 if os.path.isdir(src): 335 if _destinsrc(src, dst): 336 raise Error("Cannot move a directory '%s' into itself '%s'." % (src, dst)) 337 copytree(src, real_dst, symlinks=True) 338 rmtree(src) 339 else: 340 copy2(src, real_dst) 341 os.unlink(src) 342 343def _destinsrc(src, dst): 344 src = abspath(src) 345 dst = abspath(dst) 346 if not src.endswith(os.path.sep): 347 src += os.path.sep 348 if not dst.endswith(os.path.sep): 349 dst += os.path.sep 350 return dst.startswith(src) 351 352def _get_gid(name): 353 """Returns a gid, given a group name.""" 354 if getgrnam is None or name is None: 355 return None 356 try: 357 result = getgrnam(name) 358 except KeyError: 359 result = None 360 if result is not None: 361 return result[2] 362 return None 363 364def _get_uid(name): 365 """Returns an uid, given a user name.""" 366 if getpwnam is None or name is None: 367 return None 368 try: 369 result = getpwnam(name) 370 except KeyError: 371 result = None 372 if result is not None: 373 return result[2] 374 return None 375 376def _make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0, 377 owner=None, group=None, logger=None): 378 """Create a (possibly compressed) tar file from all the files under 379 'base_dir'. 380 381 'compress' must be "gzip" (the default), "bzip2", or None. 382 383 'owner' and 'group' can be used to define an owner and a group for the 384 archive that is being built. If not provided, the current owner and group 385 will be used. 386 387 The output tar file will be named 'base_name' + ".tar", possibly plus 388 the appropriate compression extension (".gz", or ".bz2"). 389 390 Returns the output filename. 391 """ 392 tar_compression = {'gzip': 'gz', None: ''} 393 compress_ext = {'gzip': '.gz'} 394 395 if _BZ2_SUPPORTED: 396 tar_compression['bzip2'] = 'bz2' 397 compress_ext['bzip2'] = '.bz2' 398 399 # flags for compression program, each element of list will be an argument 400 if compress is not None and compress not in compress_ext: 401 raise ValueError("bad value for 'compress', or compression format not " 402 "supported : {0}".format(compress)) 403 404 archive_name = base_name + '.tar' + compress_ext.get(compress, '') 405 archive_dir = os.path.dirname(archive_name) 406 407 if not os.path.exists(archive_dir): 408 if logger is not None: 409 logger.info("creating %s", archive_dir) 410 if not dry_run: 411 os.makedirs(archive_dir) 412 413 # creating the tarball 414 if logger is not None: 415 logger.info('Creating tar archive') 416 417 uid = _get_uid(owner) 418 gid = _get_gid(group) 419 420 def _set_uid_gid(tarinfo): 421 if gid is not None: 422 tarinfo.gid = gid 423 tarinfo.gname = group 424 if uid is not None: 425 tarinfo.uid = uid 426 tarinfo.uname = owner 427 return tarinfo 428 429 if not dry_run: 430 tar = tarfile.open(archive_name, 'w|%s' % tar_compression[compress]) 431 try: 432 tar.add(base_dir, filter=_set_uid_gid) 433 finally: 434 tar.close() 435 436 return archive_name 437 438def _call_external_zip(base_dir, zip_filename, verbose=False, dry_run=False): 439 # XXX see if we want to keep an external call here 440 if verbose: 441 zipoptions = "-r" 442 else: 443 zipoptions = "-rq" 444 from distutils.errors import DistutilsExecError 445 from distutils.spawn import spawn 446 try: 447 spawn(["zip", zipoptions, zip_filename, base_dir], dry_run=dry_run) 448 except DistutilsExecError: 449 # XXX really should distinguish between "couldn't find 450 # external 'zip' command" and "zip failed". 451 raise ExecError("unable to create zip file '%s': " 452 "could neither import the 'zipfile' module nor " 453 "find a standalone zip utility") % zip_filename 454 455def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0, logger=None): 456 """Create a zip file from all the files under 'base_dir'. 457 458 The output zip file will be named 'base_name' + ".zip". Uses either the 459 "zipfile" Python module (if available) or the InfoZIP "zip" utility 460 (if installed and found on the default search path). If neither tool is 461 available, raises ExecError. Returns the name of the output zip 462 file. 463 """ 464 zip_filename = base_name + ".zip" 465 archive_dir = os.path.dirname(base_name) 466 467 if not os.path.exists(archive_dir): 468 if logger is not None: 469 logger.info("creating %s", archive_dir) 470 if not dry_run: 471 os.makedirs(archive_dir) 472 473 # If zipfile module is not available, try spawning an external 'zip' 474 # command. 475 try: 476 import zipfile 477 except ImportError: 478 zipfile = None 479 480 if zipfile is None: 481 _call_external_zip(base_dir, zip_filename, verbose, dry_run) 482 else: 483 if logger is not None: 484 logger.info("creating '%s' and adding '%s' to it", 485 zip_filename, base_dir) 486 487 if not dry_run: 488 zip = zipfile.ZipFile(zip_filename, "w", 489 compression=zipfile.ZIP_DEFLATED) 490 491 for dirpath, dirnames, filenames in os.walk(base_dir): 492 for name in filenames: 493 path = os.path.normpath(os.path.join(dirpath, name)) 494 if os.path.isfile(path): 495 zip.write(path, path) 496 if logger is not None: 497 logger.info("adding '%s'", path) 498 zip.close() 499 500 return zip_filename 501 502_ARCHIVE_FORMATS = { 503 'gztar': (_make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"), 504 'bztar': (_make_tarball, [('compress', 'bzip2')], "bzip2'ed tar-file"), 505 'tar': (_make_tarball, [('compress', None)], "uncompressed tar file"), 506 'zip': (_make_zipfile, [], "ZIP file"), 507 } 508 509if _BZ2_SUPPORTED: 510 _ARCHIVE_FORMATS['bztar'] = (_make_tarball, [('compress', 'bzip2')], 511 "bzip2'ed tar-file") 512 513def get_archive_formats(): 514 """Returns a list of supported formats for archiving and unarchiving. 515 516 Each element of the returned sequence is a tuple (name, description) 517 """ 518 formats = [(name, registry[2]) for name, registry in 519 _ARCHIVE_FORMATS.items()] 520 formats.sort() 521 return formats 522 523def register_archive_format(name, function, extra_args=None, description=''): 524 """Registers an archive format. 525 526 name is the name of the format. function is the callable that will be 527 used to create archives. If provided, extra_args is a sequence of 528 (name, value) tuples that will be passed as arguments to the callable. 529 description can be provided to describe the format, and will be returned 530 by the get_archive_formats() function. 531 """ 532 if extra_args is None: 533 extra_args = [] 534 if not isinstance(function, Callable): 535 raise TypeError('The %s object is not callable' % function) 536 if not isinstance(extra_args, (tuple, list)): 537 raise TypeError('extra_args needs to be a sequence') 538 for element in extra_args: 539 if not isinstance(element, (tuple, list)) or len(element) !=2: 540 raise TypeError('extra_args elements are : (arg_name, value)') 541 542 _ARCHIVE_FORMATS[name] = (function, extra_args, description) 543 544def unregister_archive_format(name): 545 del _ARCHIVE_FORMATS[name] 546 547def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0, 548 dry_run=0, owner=None, group=None, logger=None): 549 """Create an archive file (eg. zip or tar). 550 551 'base_name' is the name of the file to create, minus any format-specific 552 extension; 'format' is the archive format: one of "zip", "tar", "bztar" 553 or "gztar". 554 555 'root_dir' is a directory that will be the root directory of the 556 archive; ie. we typically chdir into 'root_dir' before creating the 557 archive. 'base_dir' is the directory where we start archiving from; 558 ie. 'base_dir' will be the common prefix of all files and 559 directories in the archive. 'root_dir' and 'base_dir' both default 560 to the current directory. Returns the name of the archive file. 561 562 'owner' and 'group' are used when creating a tar archive. By default, 563 uses the current owner and group. 564 """ 565 save_cwd = os.getcwd() 566 if root_dir is not None: 567 if logger is not None: 568 logger.debug("changing into '%s'", root_dir) 569 base_name = os.path.abspath(base_name) 570 if not dry_run: 571 os.chdir(root_dir) 572 573 if base_dir is None: 574 base_dir = os.curdir 575 576 kwargs = {'dry_run': dry_run, 'logger': logger} 577 578 try: 579 format_info = _ARCHIVE_FORMATS[format] 580 except KeyError: 581 raise ValueError("unknown archive format '%s'" % format) 582 583 func = format_info[0] 584 for arg, val in format_info[1]: 585 kwargs[arg] = val 586 587 if format != 'zip': 588 kwargs['owner'] = owner 589 kwargs['group'] = group 590 591 try: 592 filename = func(base_name, base_dir, **kwargs) 593 finally: 594 if root_dir is not None: 595 if logger is not None: 596 logger.debug("changing back to '%s'", save_cwd) 597 os.chdir(save_cwd) 598 599 return filename 600 601 602def get_unpack_formats(): 603 """Returns a list of supported formats for unpacking. 604 605 Each element of the returned sequence is a tuple 606 (name, extensions, description) 607 """ 608 formats = [(name, info[0], info[3]) for name, info in 609 _UNPACK_FORMATS.items()] 610 formats.sort() 611 return formats 612 613def _check_unpack_options(extensions, function, extra_args): 614 """Checks what gets registered as an unpacker.""" 615 # first make sure no other unpacker is registered for this extension 616 existing_extensions = {} 617 for name, info in _UNPACK_FORMATS.items(): 618 for ext in info[0]: 619 existing_extensions[ext] = name 620 621 for extension in extensions: 622 if extension in existing_extensions: 623 msg = '%s is already registered for "%s"' 624 raise RegistryError(msg % (extension, 625 existing_extensions[extension])) 626 627 if not isinstance(function, Callable): 628 raise TypeError('The registered function must be a callable') 629 630 631def register_unpack_format(name, extensions, function, extra_args=None, 632 description=''): 633 """Registers an unpack format. 634 635 `name` is the name of the format. `extensions` is a list of extensions 636 corresponding to the format. 637 638 `function` is the callable that will be 639 used to unpack archives. The callable will receive archives to unpack. 640 If it's unable to handle an archive, it needs to raise a ReadError 641 exception. 642 643 If provided, `extra_args` is a sequence of 644 (name, value) tuples that will be passed as arguments to the callable. 645 description can be provided to describe the format, and will be returned 646 by the get_unpack_formats() function. 647 """ 648 if extra_args is None: 649 extra_args = [] 650 _check_unpack_options(extensions, function, extra_args) 651 _UNPACK_FORMATS[name] = extensions, function, extra_args, description 652 653def unregister_unpack_format(name): 654 """Removes the pack format from the registry.""" 655 del _UNPACK_FORMATS[name] 656 657def _ensure_directory(path): 658 """Ensure that the parent directory of `path` exists""" 659 dirname = os.path.dirname(path) 660 if not os.path.isdir(dirname): 661 os.makedirs(dirname) 662 663def _unpack_zipfile(filename, extract_dir): 664 """Unpack zip `filename` to `extract_dir` 665 """ 666 try: 667 import zipfile 668 except ImportError: 669 raise ReadError('zlib not supported, cannot unpack this archive.') 670 671 if not zipfile.is_zipfile(filename): 672 raise ReadError("%s is not a zip file" % filename) 673 674 zip = zipfile.ZipFile(filename) 675 try: 676 for info in zip.infolist(): 677 name = info.filename 678 679 # don't extract absolute paths or ones with .. in them 680 if name.startswith('/') or '..' in name: 681 continue 682 683 target = os.path.join(extract_dir, *name.split('/')) 684 if not target: 685 continue 686 687 _ensure_directory(target) 688 if not name.endswith('/'): 689 # file 690 data = zip.read(info.filename) 691 f = open(target, 'wb') 692 try: 693 f.write(data) 694 finally: 695 f.close() 696 del data 697 finally: 698 zip.close() 699 700def _unpack_tarfile(filename, extract_dir): 701 """Unpack tar/tar.gz/tar.bz2 `filename` to `extract_dir` 702 """ 703 try: 704 tarobj = tarfile.open(filename) 705 except tarfile.TarError: 706 raise ReadError( 707 "%s is not a compressed or uncompressed tar file" % filename) 708 try: 709 tarobj.extractall(extract_dir) 710 finally: 711 tarobj.close() 712 713_UNPACK_FORMATS = { 714 'gztar': (['.tar.gz', '.tgz'], _unpack_tarfile, [], "gzip'ed tar-file"), 715 'tar': (['.tar'], _unpack_tarfile, [], "uncompressed tar file"), 716 'zip': (['.zip'], _unpack_zipfile, [], "ZIP file") 717 } 718 719if _BZ2_SUPPORTED: 720 _UNPACK_FORMATS['bztar'] = (['.bz2'], _unpack_tarfile, [], 721 "bzip2'ed tar-file") 722 723def _find_unpack_format(filename): 724 for name, info in _UNPACK_FORMATS.items(): 725 for extension in info[0]: 726 if filename.endswith(extension): 727 return name 728 return None 729 730def unpack_archive(filename, extract_dir=None, format=None): 731 """Unpack an archive. 732 733 `filename` is the name of the archive. 734 735 `extract_dir` is the name of the target directory, where the archive 736 is unpacked. If not provided, the current working directory is used. 737 738 `format` is the archive format: one of "zip", "tar", or "gztar". Or any 739 other registered format. If not provided, unpack_archive will use the 740 filename extension and see if an unpacker was registered for that 741 extension. 742 743 In case none is found, a ValueError is raised. 744 """ 745 if extract_dir is None: 746 extract_dir = os.getcwd() 747 748 if format is not None: 749 try: 750 format_info = _UNPACK_FORMATS[format] 751 except KeyError: 752 raise ValueError("Unknown unpack format '{0}'".format(format)) 753 754 func = format_info[1] 755 func(filename, extract_dir, **dict(format_info[2])) 756 else: 757 # we need to look at the registered unpackers supported extensions 758 format = _find_unpack_format(filename) 759 if format is None: 760 raise ReadError("Unknown archive format '{0}'".format(filename)) 761 762 func = _UNPACK_FORMATS[format][1] 763 kwargs = dict(_UNPACK_FORMATS[format][2]) 764 func(filename, extract_dir, **kwargs) 765