zip.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import sys
  2. import re
  3. import fnmatch
  4. import os
  5. import shutil
  6. import zipfile
  7. from pip.util import display_path, backup_dir, rmtree
  8. from pip.log import logger
  9. from pip.exceptions import InstallationError
  10. from pip.basecommand import Command
  11. class ZipCommand(Command):
  12. """Zip individual packages."""
  13. name = 'zip'
  14. usage = """
  15. %prog [options] <package> ..."""
  16. summary = 'DEPRECATED. Zip individual packages.'
  17. def __init__(self, *args, **kw):
  18. super(ZipCommand, self).__init__(*args, **kw)
  19. if self.name == 'zip':
  20. self.cmd_opts.add_option(
  21. '--unzip',
  22. action='store_true',
  23. dest='unzip',
  24. help='Unzip (rather than zip) a package.')
  25. else:
  26. self.cmd_opts.add_option(
  27. '--zip',
  28. action='store_false',
  29. dest='unzip',
  30. default=True,
  31. help='Zip (rather than unzip) a package.')
  32. self.cmd_opts.add_option(
  33. '--no-pyc',
  34. action='store_true',
  35. dest='no_pyc',
  36. help='Do not include .pyc files in zip files (useful on Google App Engine).')
  37. self.cmd_opts.add_option(
  38. '-l', '--list',
  39. action='store_true',
  40. dest='list',
  41. help='List the packages available, and their zip status.')
  42. self.cmd_opts.add_option(
  43. '--sort-files',
  44. action='store_true',
  45. dest='sort_files',
  46. help='With --list, sort packages according to how many files they contain.')
  47. self.cmd_opts.add_option(
  48. '--path',
  49. action='append',
  50. dest='paths',
  51. help='Restrict operations to the given paths (may include wildcards).')
  52. self.cmd_opts.add_option(
  53. '-n', '--simulate',
  54. action='store_true',
  55. help='Do not actually perform the zip/unzip operation.')
  56. self.parser.insert_option_group(0, self.cmd_opts)
  57. def paths(self):
  58. """All the entries of sys.path, possibly restricted by --path"""
  59. if not self.select_paths:
  60. return sys.path
  61. result = []
  62. match_any = set()
  63. for path in sys.path:
  64. path = os.path.normcase(os.path.abspath(path))
  65. for match in self.select_paths:
  66. match = os.path.normcase(os.path.abspath(match))
  67. if '*' in match:
  68. if re.search(fnmatch.translate(match + '*'), path):
  69. result.append(path)
  70. match_any.add(match)
  71. break
  72. else:
  73. if path.startswith(match):
  74. result.append(path)
  75. match_any.add(match)
  76. break
  77. else:
  78. logger.debug("Skipping path %s because it doesn't match %s"
  79. % (path, ', '.join(self.select_paths)))
  80. for match in self.select_paths:
  81. if match not in match_any and '*' not in match:
  82. result.append(match)
  83. logger.debug("Adding path %s because it doesn't match "
  84. "anything already on sys.path" % match)
  85. return result
  86. def run(self, options, args):
  87. logger.deprecated('1.7', "DEPRECATION: 'pip zip' and 'pip unzip` are deprecated, and will be removed in a future release.")
  88. self.select_paths = options.paths
  89. self.simulate = options.simulate
  90. if options.list:
  91. return self.list(options, args)
  92. if not args:
  93. raise InstallationError(
  94. 'You must give at least one package to zip or unzip')
  95. packages = []
  96. for arg in args:
  97. module_name, filename = self.find_package(arg)
  98. if options.unzip and os.path.isdir(filename):
  99. raise InstallationError(
  100. 'The module %s (in %s) is not a zip file; cannot be unzipped'
  101. % (module_name, filename))
  102. elif not options.unzip and not os.path.isdir(filename):
  103. raise InstallationError(
  104. 'The module %s (in %s) is not a directory; cannot be zipped'
  105. % (module_name, filename))
  106. packages.append((module_name, filename))
  107. last_status = None
  108. for module_name, filename in packages:
  109. if options.unzip:
  110. last_status = self.unzip_package(module_name, filename)
  111. else:
  112. last_status = self.zip_package(module_name, filename, options.no_pyc)
  113. return last_status
  114. def unzip_package(self, module_name, filename):
  115. zip_filename = os.path.dirname(filename)
  116. if not os.path.isfile(zip_filename) and zipfile.is_zipfile(zip_filename):
  117. raise InstallationError(
  118. 'Module %s (in %s) isn\'t located in a zip file in %s'
  119. % (module_name, filename, zip_filename))
  120. package_path = os.path.dirname(zip_filename)
  121. if not package_path in self.paths():
  122. logger.warn(
  123. 'Unpacking %s into %s, but %s is not on sys.path'
  124. % (display_path(zip_filename), display_path(package_path),
  125. display_path(package_path)))
  126. logger.notify('Unzipping %s (in %s)' % (module_name, display_path(zip_filename)))
  127. if self.simulate:
  128. logger.notify('Skipping remaining operations because of --simulate')
  129. return
  130. logger.indent += 2
  131. try:
  132. ## FIXME: this should be undoable:
  133. zip = zipfile.ZipFile(zip_filename)
  134. to_save = []
  135. for info in zip.infolist():
  136. name = info.filename
  137. if name.startswith(module_name + os.path.sep):
  138. content = zip.read(name)
  139. dest = os.path.join(package_path, name)
  140. if not os.path.exists(os.path.dirname(dest)):
  141. os.makedirs(os.path.dirname(dest))
  142. if not content and dest.endswith(os.path.sep):
  143. if not os.path.exists(dest):
  144. os.makedirs(dest)
  145. else:
  146. f = open(dest, 'wb')
  147. f.write(content)
  148. f.close()
  149. else:
  150. to_save.append((name, zip.read(name)))
  151. zip.close()
  152. if not to_save:
  153. logger.info('Removing now-empty zip file %s' % display_path(zip_filename))
  154. os.unlink(zip_filename)
  155. self.remove_filename_from_pth(zip_filename)
  156. else:
  157. logger.info('Removing entries in %s/ from zip file %s' % (module_name, display_path(zip_filename)))
  158. zip = zipfile.ZipFile(zip_filename, 'w')
  159. for name, content in to_save:
  160. zip.writestr(name, content)
  161. zip.close()
  162. finally:
  163. logger.indent -= 2
  164. def zip_package(self, module_name, filename, no_pyc):
  165. orig_filename = filename
  166. logger.notify('Zip %s (in %s)' % (module_name, display_path(filename)))
  167. logger.indent += 2
  168. if filename.endswith('.egg'):
  169. dest_filename = filename
  170. else:
  171. dest_filename = filename + '.zip'
  172. try:
  173. ## FIXME: I think this needs to be undoable:
  174. if filename == dest_filename:
  175. filename = backup_dir(orig_filename)
  176. logger.notify('Moving %s aside to %s' % (orig_filename, filename))
  177. if not self.simulate:
  178. shutil.move(orig_filename, filename)
  179. try:
  180. logger.info('Creating zip file in %s' % display_path(dest_filename))
  181. if not self.simulate:
  182. zip = zipfile.ZipFile(dest_filename, 'w')
  183. zip.writestr(module_name + '/', '')
  184. for dirpath, dirnames, filenames in os.walk(filename):
  185. if no_pyc:
  186. filenames = [f for f in filenames
  187. if not f.lower().endswith('.pyc')]
  188. for fns, is_dir in [(dirnames, True), (filenames, False)]:
  189. for fn in fns:
  190. full = os.path.join(dirpath, fn)
  191. dest = os.path.join(module_name, dirpath[len(filename):].lstrip(os.path.sep), fn)
  192. if is_dir:
  193. zip.writestr(dest + '/', '')
  194. else:
  195. zip.write(full, dest)
  196. zip.close()
  197. logger.info('Removing old directory %s' % display_path(filename))
  198. if not self.simulate:
  199. rmtree(filename)
  200. except:
  201. ## FIXME: need to do an undo here
  202. raise
  203. ## FIXME: should also be undone:
  204. self.add_filename_to_pth(dest_filename)
  205. finally:
  206. logger.indent -= 2
  207. def remove_filename_from_pth(self, filename):
  208. for pth in self.pth_files():
  209. f = open(pth, 'r')
  210. lines = f.readlines()
  211. f.close()
  212. new_lines = [
  213. l for l in lines if l.strip() != filename]
  214. if lines != new_lines:
  215. logger.info('Removing reference to %s from .pth file %s'
  216. % (display_path(filename), display_path(pth)))
  217. if not [line for line in new_lines if line]:
  218. logger.info('%s file would be empty: deleting' % display_path(pth))
  219. if not self.simulate:
  220. os.unlink(pth)
  221. else:
  222. if not self.simulate:
  223. f = open(pth, 'wb')
  224. f.writelines(new_lines)
  225. f.close()
  226. return
  227. logger.warn('Cannot find a reference to %s in any .pth file' % display_path(filename))
  228. def add_filename_to_pth(self, filename):
  229. path = os.path.dirname(filename)
  230. dest = filename + '.pth'
  231. if path not in self.paths():
  232. logger.warn('Adding .pth file %s, but it is not on sys.path' % display_path(dest))
  233. if not self.simulate:
  234. if os.path.exists(dest):
  235. f = open(dest)
  236. lines = f.readlines()
  237. f.close()
  238. if lines and not lines[-1].endswith('\n'):
  239. lines[-1] += '\n'
  240. lines.append(filename + '\n')
  241. else:
  242. lines = [filename + '\n']
  243. f = open(dest, 'wb')
  244. f.writelines(lines)
  245. f.close()
  246. def pth_files(self):
  247. for path in self.paths():
  248. if not os.path.exists(path) or not os.path.isdir(path):
  249. continue
  250. for filename in os.listdir(path):
  251. if filename.endswith('.pth'):
  252. yield os.path.join(path, filename)
  253. def find_package(self, package):
  254. for path in self.paths():
  255. full = os.path.join(path, package)
  256. if os.path.exists(full):
  257. return package, full
  258. if not os.path.isdir(path) and zipfile.is_zipfile(path):
  259. zip = zipfile.ZipFile(path, 'r')
  260. try:
  261. zip.read(os.path.join(package, '__init__.py'))
  262. except KeyError:
  263. pass
  264. else:
  265. zip.close()
  266. return package, full
  267. zip.close()
  268. ## FIXME: need special error for package.py case:
  269. raise InstallationError(
  270. 'No package with the name %s found' % package)
  271. def list(self, options, args):
  272. if args:
  273. raise InstallationError(
  274. 'You cannot give an argument with --list')
  275. for path in sorted(self.paths()):
  276. if not os.path.exists(path):
  277. continue
  278. basename = os.path.basename(path.rstrip(os.path.sep))
  279. if os.path.isfile(path) and zipfile.is_zipfile(path):
  280. if os.path.dirname(path) not in self.paths():
  281. logger.notify('Zipped egg: %s' % display_path(path))
  282. continue
  283. if (basename != 'site-packages' and basename != 'dist-packages'
  284. and not path.replace('\\', '/').endswith('lib/python')):
  285. continue
  286. logger.notify('In %s:' % display_path(path))
  287. logger.indent += 2
  288. zipped = []
  289. unzipped = []
  290. try:
  291. for filename in sorted(os.listdir(path)):
  292. ext = os.path.splitext(filename)[1].lower()
  293. if ext in ('.pth', '.egg-info', '.egg-link'):
  294. continue
  295. if ext == '.py':
  296. logger.info('Not displaying %s: not a package' % display_path(filename))
  297. continue
  298. full = os.path.join(path, filename)
  299. if os.path.isdir(full):
  300. unzipped.append((filename, self.count_package(full)))
  301. elif zipfile.is_zipfile(full):
  302. zipped.append(filename)
  303. else:
  304. logger.info('Unknown file: %s' % display_path(filename))
  305. if zipped:
  306. logger.notify('Zipped packages:')
  307. logger.indent += 2
  308. try:
  309. for filename in zipped:
  310. logger.notify(filename)
  311. finally:
  312. logger.indent -= 2
  313. else:
  314. logger.notify('No zipped packages.')
  315. if unzipped:
  316. if options.sort_files:
  317. unzipped.sort(key=lambda x: -x[1])
  318. logger.notify('Unzipped packages:')
  319. logger.indent += 2
  320. try:
  321. for filename, count in unzipped:
  322. logger.notify('%s (%i files)' % (filename, count))
  323. finally:
  324. logger.indent -= 2
  325. else:
  326. logger.notify('No unzipped packages.')
  327. finally:
  328. logger.indent -= 2
  329. def count_package(self, path):
  330. total = 0
  331. for dirpath, dirnames, filenames in os.walk(path):
  332. filenames = [f for f in filenames
  333. if not f.lower().endswith('.pyc')]
  334. total += len(filenames)
  335. return total