diff --git a/.gitignore b/.gitignore index 0fe94900b..e5df8c832 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ build/* pip-log.txt pip.log *.~ +.tox + diff --git a/.travis.yml b/.travis.yml index 0642da6fe..24be5ebaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - 2.7 - 3.1 - 3.2 + - pypy before_install: - sudo apt-get install subversion bzr mercurial - echo -e "[web]\ncacerts = /etc/ssl/certs/ca-certificates.crt" >> ~/.hgrc @@ -15,5 +16,7 @@ notifications: branches: only: - develop +matrix: + allow_failures: env: - PIP_USE_MIRRORS=true diff --git a/AUTHORS.txt b/AUTHORS.txt index c1ea6f49f..e1633b223 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,15 +1,20 @@ Alex Grönholm Alex Morega Alexandre Conrad +Andrey Bulgakov Antti Kaihola Armin Ronacher +Aziz Köksal Ben Rosser Brian Rosner Carl Meyer +Chris McDonough Christian Oudard +Clay McClure Cody Soyland Daniel Holth Dave Abrahams +Donald Stufft Francesco Hugo Lopes Tavares Ian Bicking @@ -36,12 +41,15 @@ Nowell Strite Oliver Tonnhofer Olivier Girardot Patrick Jenkins +Paul Moore Paul Nasrat Paul Oswald Paul van der Linden Peter Waller +Phil Whelan Piet Delport Qiangning Hong +Rafael Caricio Rene Dudfield Ronny Pfannschmidt Rory McCann diff --git a/LICENSE.txt b/LICENSE.txt index 7951a0325..c1024db59 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2008-2011 The pip developers (see AUTHORS.txt file) +Copyright (c) 2008-2012 The pip developers (see AUTHORS.txt file) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..251efd998 --- /dev/null +++ b/README.rst @@ -0,0 +1,7 @@ +pip +=== + +.. image:: https://secure.travis-ci.org/pypa/pip.png?branch=develop + :target: http://travis-ci.org/pypa/pip + +For documentation, see http://www.pip-installer.org diff --git a/docs/conf.py b/docs/conf.py index 2c03957b1..8678de5f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,12 +11,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(os.pardir)) #sys.path.append(os.path.join(os.path.dirname(__file__), '../')) # -- General configuration ----------------------------------------------------- @@ -40,15 +41,21 @@ master_doc = 'index' # General information about the project. project = 'pip' -copyright = '2008-2011, The pip developers' +copyright = '2008-2012, The pip developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -release = "1.1.post2" -version = '.'.join(release.split('.')[:2]) +try: + from pip import __version__ + # The short X.Y version. + version = '.'.join(__version__.split('.')[:2]) + # The full version, including alpha/beta/rc tags. + release = __version__ +except ImportError: + version = release = 'dev' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/contributing.txt b/docs/contributing.txt index c3d0c1356..6caddd449 100644 --- a/docs/contributing.txt +++ b/docs/contributing.txt @@ -60,10 +60,10 @@ Before sending us a pull request, please, be sure all tests pass. Supported Python versions ------------------------- -Pip supports Python versions 2.4, 2.5, 2.6, 2.7, 3.1, and 3.2, from a single +Pip supports Python versions 2.5, 2.6, 2.7, 3.1, and 3.2, from a single codebase (without use of 2to3 translation). Untested contributions frequently -break Python 2.4 or 3.x compatibility. Please run the tests on at least 2.4 and -3.2 and report your results when sending a pull request. +break Python 3.x compatibility. Please run the tests on at least 3.2 and +report your results when sending a pull request. Continuous Integration server ----------------------------- diff --git a/docs/news.txt b/docs/news.txt index 99e59489e..ac02e162f 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -5,13 +5,49 @@ News Changelog ========= -Next release (1.2) schedule +Next release (1.3) schedule --------------------------- -Beta and final releases planned for the second half of 2012. +Beta and final releases planned for the end of 2012. + develop (unreleased) -------------------- +-------------------- + +* --user/--upgrade install options now work together; thanks eevee. + +* Added check in ``install --download`` to prevent re-downloading if the target + file already exists. Thanks Andrey Bulgakov. + +* Added support for bare paths (including relative paths) as argument to + `--find-links`. Thanks Paul Moore for draft patch. + +* Added support for --no-index in requirements files. + +* Added "pip show" command to get information about an installed + package. Fixes #131. Thanks Kelsey Hightower and Rafael Caricio. + +1.2.1 (2012-09-06) +------------------ + +* Fixed a regression introduced in 1.2 about raising an exception when + not finding any files to uninstall in the current environment. Thanks for + the fix, Marcus Smith. + +1.2 (2012-09-01) +---------------- + +* **Dropped support for Python 2.4** The minimum supported Python version is + now Python 2.5. + +* Fixed issue #605 - pypi mirror support broken on some DNS responses. Thanks + philwhin. + +* Fixed issue #355 - pip uninstall removes files it didn't install. Thanks + pjdelport. + +* Fixed issues #493, #494, #440, and #573 related to improving support for the + user installation scheme. Thanks Marcus Smith. * Write failure log to temp file if default location is not writable. Thanks andreigc. @@ -38,9 +74,6 @@ develop (unreleased) * Fixed issue #504 - allow package URLS to have querystrings. Thanks W. Trevor King. -* Dropped support for Python 2.4. The minimum supported Python version is - now Python 2.5. - * Fixed issue #58 - pip freeze now falls back to non-editable format rather than blowing up if it can't determine the origin repository of an editable. Thanks Rory McCann. @@ -57,6 +90,21 @@ develop (unreleased) * Fixed issue #427 - clearer error message on a malformed VCS url. Thanks Thomas Fenzl. +* Added support for using any of the built in guaranteed algorithms in + ``hashlib`` as a checksum hash. + +* Fixed issue #321 - Raise an exception if current working directory can't be + found or accessed. + +* Fixed issue #82 - Removed special casing of the user directory and use the + Python default instead. + +* Fixed #436 - Only warn about version conflicts if there is actually one. + This re-enables using ``==dev`` in requirements files. + +* Moved tests to be run on Travis CI: http://travis-ci.org/pypa/pip + +* Added a better help formatter. 1.1 (2012-02-16) ---------------- diff --git a/docs/usage.txt b/docs/usage.txt index 991d0d54e..02309f2d6 100644 --- a/docs/usage.txt +++ b/docs/usage.txt @@ -26,8 +26,8 @@ local or remote:: .. _setuptools extras: http://peak.telecommunity.com/DevCenter/setuptools#declaring-extras-optional-features-with-their-own-dependencies -Edit mode -********* +Editable mode +************* Packages normally_ install under ``site-packages``, but when you're making changes, it makes more sense to run the package straight from the @@ -106,6 +106,16 @@ within a :ref:`requirements file ` in addition to on the command line directly. +Package Checksum Hashes +''''''''''''''''''''''' + +:term:`PyPI` provides a md5 hash of a package by having the link to the +package include a #md5=. pip supports this, as well as any of the +guaranteed hashlib algorithms (sha1, sha224, sha384, sha256, sha512, md5). + +The hash fragment is case sensitive (i.e. sha1 not SHA1). + + Uninstall packages ------------------ @@ -137,9 +147,23 @@ packages. With the ``--index`` option you can search in a different repository. +Checking installed package status +--------------------------------- + +To get info about an installed package, including its location and included +files, run ``pip show ProjectName``. + + Bundles ------- +.. note:: + + Pip bundles are poorly supported, poorly tested, not widely used, and + unnecessary (the same goals can be achieved by distributing a directory of + sdists and using `--find-links` to reference it). The feature will likely be + removed in a future version of pip. + Another way to distribute a set of libraries is a bundle format (specific to pip). This format is not stable at this time (there simply hasn't been any feedback, nor a great deal of thought). A bundle file contains all the diff --git a/pip/__init__.py b/pip/__init__.py index 62e3a910f..e0de1c61a 100755 --- a/pip/__init__.py +++ b/pip/__init__.py @@ -10,8 +10,12 @@ from pip.basecommand import command_dict, load_command, load_all_commands, comma from pip.baseparser import parser from pip.exceptions import InstallationError from pip.log import logger -from pip.util import get_installed_distributions -from pip.vcs import git, mercurial, subversion, bazaar +from pip.util import get_installed_distributions, get_prog +from pip.vcs import git, mercurial, subversion, bazaar # noqa + + +# The version as used in the setup.py and the docs conf.py +__version__ = "1.2.1.post1" def autocomplete(): @@ -26,7 +30,7 @@ def autocomplete(): cwords = os.environ['COMP_WORDS'].split()[1:] cword = int(os.environ['COMP_CWORD']) try: - current = cwords[cword-1] + current = cwords[cword - 1] except IndexError: current = '' load_all_commands() @@ -59,7 +63,7 @@ def autocomplete(): for opt in subcommand.parser.option_list if opt.help != optparse.SUPPRESS_HELP] # filter out previously specified options from available options - prev_opts = [x.split('=')[0] for x in cwords[1:cword-1]] + prev_opts = [x.split('=')[0] for x in cwords[1:cword - 1]] options = [(x, v) for (x, v) in options if x not in prev_opts] # filter options by current input options = [(k, v) for k, v in options if k.startswith(current)] @@ -87,7 +91,8 @@ def main(initial_args=None): if options.help and not args: args = ['help'] if not args: - parser.error('You must give a command (use "pip help" to see a list of commands)') + parser.error('You must give a command ' + '(use "%s help" to see a list of commands)' % get_prog()) command = args[0].lower() load_command(command) if command not in command_dict: @@ -184,7 +189,7 @@ class FrozenRequirement(object): req = self.req if self.editable: req = '-e %s' % req - return '\n'.join(list(self.comments)+[str(req)])+'\n' + return '\n'.join(list(self.comments) + [str(req)]) + '\n' if __name__ == '__main__': diff --git a/__main__.py b/pip/__main__.py similarity index 53% rename from __main__.py rename to pip/__main__.py index 258e87cf6..5ca374634 100644 --- a/__main__.py +++ b/pip/__main__.py @@ -1,7 +1,7 @@ -if __name__ == '__main__': - import sys - from . import main +import sys +from .runner import run - exit = main() +if __name__ == '__main__': + exit = run() if exit: sys.exit(exit) diff --git a/pip/backwardcompat.py b/pip/backwardcompat.py index d735dacc1..788023fae 100644 --- a/pip/backwardcompat.py +++ b/pip/backwardcompat.py @@ -1,10 +1,14 @@ """Stuff that differs in different Python versions""" +import os +import imp import sys import site __all__ = ['WindowsError'] +uses_pycache = hasattr(imp,'cache_from_source') + try: WindowsError = WindowsError except NameError: @@ -12,6 +16,7 @@ except NameError: """this exception should never be raised""" WindowsError = NeverUsedException + console_encoding = sys.__stdout__.encoding if sys.version_info >= (3,): @@ -97,3 +102,11 @@ def product(*args, **kwds): result = [x+[y] for x in result for y in pool] for prod in result: yield tuple(prod) + +def home_lib(home): + """Return the lib dir under the 'home' installation scheme""" + if hasattr(sys, 'pypy_version_info'): + lib = 'site-packages' + else: + lib = os.path.join('lib', 'python') + return os.path.join(home, lib) diff --git a/pip/basecommand.py b/pip/basecommand.py index d189d3987..48c390d9c 100644 --- a/pip/basecommand.py +++ b/pip/basecommand.py @@ -4,6 +4,7 @@ import os from pkgutil import walk_packages import socket import sys +import tempfile import traceback import time @@ -15,6 +16,7 @@ from pip.exceptions import (BadCommand, InstallationError, UninstallationError, CommandError) from pip.backwardcompat import StringIO from pip.status_codes import SUCCESS, ERROR, UNKNOWN_ERROR, VIRTUALENV_NOT_FOUND +from pip.util import get_prog __all__ = ['command_dict', 'Command', 'load_all_commands', @@ -35,7 +37,7 @@ class Command(object): assert self.name self.parser = ConfigOptionParser( usage=self.usage, - prog='%s %s' % (sys.argv[0], self.name), + prog='%s %s' % (get_prog(), self.name), version=parser.version, formatter=UpdatingDefaultsHelpFormatter(), name=self.name) @@ -144,7 +146,7 @@ class Command(object): temp = tempfile.NamedTemporaryFile(delete=False) log_fn = temp.name log_fp = open_logfile(log_fn, 'w') - logger.fatal('Storing complete log in %s' % log_fn) + logger.fatal('Storing complete log in %s' % log_fn) log_fp.write(text) log_fp.close() return exit diff --git a/pip/baseparser.py b/pip/baseparser.py index 0742ef6f6..0ebdd8f39 100644 --- a/pip/baseparser.py +++ b/pip/baseparser.py @@ -7,22 +7,17 @@ import os from distutils.util import strtobool from pip.backwardcompat import ConfigParser, string_types from pip.locations import default_config_file, default_log_file +from pip.util import get_terminal_size, get_prog -class PipPrettyHelpFormatter(optparse.IndentedHelpFormatter): +class PrettyHelpFormatter(optparse.IndentedHelpFormatter): """A prettier/less verbose help formatter for optparse.""" - def __init__(self, *args, **kw): - kw['max_help_position'] = 23 - kw['indent_increment'] = 1 - - # do as argparse does - try: - kw['width'] = int(os.environ['COLUMNS']) - 2 - except: - kw['width'] = 78 - - optparse.IndentedHelpFormatter.__init__(self, *args, **kw) + def __init__(self, *args, **kwargs): + kwargs['max_help_position'] = 30 + kwargs['indent_increment'] = 1 + kwargs['width'] = get_terminal_size()[0] - 2 + optparse.IndentedHelpFormatter.__init__(self, *args, **kwargs) def format_option_strings(self, option): return self._format_option_strings(option, ' <%s>', ', ') @@ -35,31 +30,34 @@ class PipPrettyHelpFormatter(optparse.IndentedHelpFormatter): :param mvarfmt: metavar format string - evaluated as mvarfmt % metavar :param optsep: separator """ - opts = [] - if option._short_opts: opts.append(option._short_opts[0]) - if option._long_opts: opts.append(option._long_opts[0]) - if len(opts) > 1: opts.insert(1, optsep) + if option._short_opts: + opts.append(option._short_opts[0]) + if option._long_opts: + opts.append(option._long_opts[0]) + if len(opts) > 1: + opts.insert(1, optsep) if option.takes_value(): metavar = option.metavar or option.dest.lower() - opts.append(mvarfmt % metavar) + opts.append(mvarfmt % metavar.upper()) return ''.join(opts) def format_heading(self, heading): - if heading == 'Options': return '' + if heading == 'Options': + return '' return heading + ':\n' def format_usage(self, usage): - # ensure there is only one newline between usage and the first heading - # if there is no description - + """ + ensure there is only one newline between usage and the first heading + if there is no description + """ msg = 'Usage: %s' % usage if self.parser.description: msg += '\n' - return msg def format_description(self, description): @@ -77,7 +75,7 @@ class PipPrettyHelpFormatter(optparse.IndentedHelpFormatter): return '' -class UpdatingDefaultsHelpFormatter(PipPrettyHelpFormatter): +class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter): """Custom help formatter for use in ConfigOptionParser that updates the defaults before expanding them, allowing them to show up correctly in the help listing""" @@ -148,7 +146,7 @@ class ConfigOptionParser(optparse.OptionParser): for key, val in items: key = key.replace('_', '-') if not key.startswith('--'): - key = '--%s' % key # only prefer long opts + key = '--%s' % key # only prefer long opts normalized[key] = val return normalized @@ -171,7 +169,7 @@ class ConfigOptionParser(optparse.OptionParser): # Old, pre-Optik 1.5 behaviour. return optparse.Values(self.defaults) - defaults = self.update_defaults(self.defaults.copy()) # ours + defaults = self.update_defaults(self.defaults.copy()) # ours for option in self._get_all_options(): default = defaults.get(option.dest) if isinstance(default, string_types): @@ -179,20 +177,27 @@ class ConfigOptionParser(optparse.OptionParser): defaults[option.dest] = option.check_value(opt_str, default) return optparse.Values(defaults) + def error(self, msg): + self.print_usage(sys.stderr) + self.exit(2, "%s\n" % msg) + + try: pip_dist = pkg_resources.get_distribution('pip') version = '%s from %s (python %s)' % ( pip_dist, pip_dist.location, sys.version[:3]) except pkg_resources.DistributionNotFound: # when running pip.py without installing - version=None + version = None + parser = ConfigOptionParser( usage='%prog COMMAND [OPTIONS]', version=version, add_help_option=False, formatter=UpdatingDefaultsHelpFormatter(), - name='global') + name='global', + prog=get_prog()) parser.add_option( '-h', '--help', @@ -253,7 +258,7 @@ parser.add_option( default='', help="Specify a proxy in the form user:passwd@proxy.server:port. " "Note that the user:password@ is optional and required only if you " - "are behind an authenticated proxy. If you provide " + "are behind an authenticated proxy. If you provide " "user@proxy.server:port then you will be prompted for a password.") parser.add_option( '--timeout', '--default-timeout', @@ -285,10 +290,10 @@ parser.add_option( choices=['s', 'i', 'w', 'b'], default=[], action='append', - help="Default action when a path already exists." - "Use this option more then one time to specify " + help="Default action when a path already exists. " + "Use this option more than one time to specify " "another action if a certain option is not " - "available, choices: " + "available. Choices: " "(s)witch, (i)gnore, (w)ipe, (b)ackup") parser.disable_interspersed_args() diff --git a/pip/commands/install.py b/pip/commands/install.py index 71ec42f51..9900c434a 100644 --- a/pip/commands/install.py +++ b/pip/commands/install.py @@ -9,6 +9,7 @@ from pip.locations import build_prefix, src_prefix, virtualenv_no_global from pip.basecommand import Command from pip.index import PackageFinder from pip.exceptions import InstallationError, CommandError +from pip.backwardcompat import home_lib class InstallCommand(Command): @@ -36,7 +37,7 @@ class InstallCommand(Command): action='append', default=[], metavar='FILENAME', - help='Install all the packages listed in the given requirements file. ' + help='Install all the packages listed in the given requirements file. ' 'This option can be used multiple times.') self.parser.add_option( '-f', '--find-links', @@ -148,15 +149,15 @@ class InstallCommand(Command): dest='install_options', action='append', help="Extra arguments to be supplied to the setup.py install " - "command (use like --install-option=\"--install-scripts=/usr/local/bin\"). " - "Use multiple --install-option options to pass multiple options to setup.py install. " + "command (use like --install-option=\"--install-scripts=/usr/local/bin\"). " + "Use multiple --install-option options to pass multiple options to setup.py install. " "If you are using an option with a directory path, be sure to use absolute path.") self.parser.add_option( '--global-option', dest='global_options', action='append', - help="Extra global options to be supplied to the setup.py" + help="Extra global options to be supplied to the setup.py " "call before the install command") self.parser.add_option( @@ -276,7 +277,7 @@ class InstallCommand(Command): if options.target_dir: if not os.path.exists(options.target_dir): os.makedirs(options.target_dir) - lib_dir = os.path.join(temp_target_dir, "lib/python/") + lib_dir = home_lib(temp_target_dir) for item in os.listdir(lib_dir): shutil.move( os.path.join(lib_dir, item), diff --git a/pip/commands/show.py b/pip/commands/show.py new file mode 100644 index 000000000..b5357f104 --- /dev/null +++ b/pip/commands/show.py @@ -0,0 +1,78 @@ +import os +import pkg_resources +from pip.basecommand import Command +from pip.log import logger + + +class ShowCommand(Command): + name = 'show' + usage = '%prog QUERY' + summary = 'Output installed distributions (exact versions, files) to stdout' + + def __init__(self): + super(ShowCommand, self).__init__() + self.parser.add_option( + '-f', '--files', + dest='files', + action='store_true', + default=False, + help='Show the full list of installed files for each package') + + def run(self, options, args): + if not args: + logger.warn('ERROR: Please provide a project name or names.') + return + query = args + + results = search_packages_info(query) + print_results(results, options.files) + + +def search_packages_info(query): + """ + Gather details from installed distributions. Print distribution name, + version, location, and installed files. Installed files requires a + pip generated 'installed-files.txt' in the distributions '.egg-info' + directory. + """ + installed_packages = dict( + [(p.project_name.lower(), p) for p in pkg_resources.working_set]) + for name in query: + normalized_name = name.lower() + if normalized_name in installed_packages: + dist = installed_packages[normalized_name] + package = { + 'name': dist.project_name, + 'version': dist.version, + 'location': dist.location, + 'requires': [dep.project_name for dep in dist.requires()], + } + filelist = os.path.join( + dist.location, + dist.egg_name() + '.egg-info', + 'installed-files.txt') + if os.path.isfile(filelist): + package['files'] = filelist + yield package + + +def print_results(distributions, list_all_files): + """ + Print the informations from installed distributions found. + """ + for dist in distributions: + logger.notify("---") + logger.notify("Name: %s" % dist['name']) + logger.notify("Version: %s" % dist['version']) + logger.notify("Location: %s" % dist['location']) + logger.notify("Requires: %s" % ', '.join(dist['requires'])) + if list_all_files: + logger.notify("Files:") + if 'files' in dist: + for line in open(dist['files']): + logger.notify(" %s" % line.strip()) + else: + logger.notify("Cannot locate installed-files.txt") + + +ShowCommand() diff --git a/pip/commands/zip.py b/pip/commands/zip.py index ebe1d791a..1c84210ac 100644 --- a/pip/commands/zip.py +++ b/pip/commands/zip.py @@ -231,7 +231,7 @@ class ZipCommand(Command): def add_filename_to_pth(self, filename): path = os.path.dirname(filename) - dest = os.path.join(path, filename + '.pth') + dest = filename + '.pth' if path not in self.paths(): logger.warn('Adding .pth file %s, but it is not on sys.path' % display_path(dest)) if not self.simulate: diff --git a/pip/download.py b/pip/download.py index d6f7936ce..b47627ecf 100644 --- a/pip/download.py +++ b/pip/download.py @@ -1,6 +1,6 @@ import cgi import getpass -from hashlib import md5 +import hashlib import mimetypes import os import re @@ -322,16 +322,24 @@ def is_file_url(link): return link.url.lower().startswith('file:') -def _check_md5(download_hash, link): - download_hash = download_hash.hexdigest() - if download_hash != link.md5_hash: - logger.fatal("MD5 hash of the package %s (%s) doesn't match the expected hash %s!" - % (link, download_hash, link.md5_hash)) - raise InstallationError('Bad MD5 hash for package %s' % link) +def _check_hash(download_hash, link): + if download_hash.digest_size != hashlib.new(link.hash_name).digest_size: + logger.fatal("Hash digest size of the package %d (%s) doesn't match the expected hash name %s!" + % (download_hash.digest_size, link, link.hash_name)) + raise InstallationError('Hash name mismatch for package %s' % link) + if download_hash.hexdigest() != link.hash: + logger.fatal("Hash of the package %s (%s) doesn't match the expected hash %s!" + % (link, download_hash, link.hash)) + raise InstallationError('Bad %s hash for package %s' % (link.hash_name, link)) -def _get_md5_from_file(target_file, link): - download_hash = md5() +def _get_hash_from_file(target_file, link): + try: + download_hash = hashlib.new(link.hash_name) + except (ValueError, TypeError): + logger.warn("Unsupported hash name %s for package %s" % (link.hash_name, link)) + return None + fp = open(target_file, 'rb') while True: chunk = fp.read(4096) @@ -345,8 +353,11 @@ def _get_md5_from_file(target_file, link): def _download_url(resp, link, temp_location): fp = open(temp_location, 'wb') download_hash = None - if link.md5_hash: - download_hash = md5() + if link.hash and link.hash_name: + try: + download_hash = hashlib.new(link.hash_name) + except ValueError: + logger.warn("Unsupported hash name %s for package %s" % (link.hash_name, link)) try: total_length = int(resp.info()['content-length']) except (ValueError, KeyError, TypeError): @@ -363,7 +374,7 @@ def _download_url(resp, link, temp_location): logger.start_progress('Downloading %s (unknown size): ' % show_url) else: logger.notify('Downloading %s' % show_url) - logger.debug('Downloading from URL %s' % link) + logger.info('Downloading from URL %s' % link) while True: chunk = resp.read(4096) @@ -375,7 +386,7 @@ def _download_url(resp, link, temp_location): logger.show_progress('%s' % format_size(downloaded)) else: logger.show_progress('%3i%% %s' % (100*downloaded/total_length, format_size(downloaded))) - if link.md5_hash: + if download_hash is not None: download_hash.update(chunk) fp.write(chunk) fp.close() @@ -418,16 +429,29 @@ def unpack_http_url(link, location, download_cache, download_dir=None): urllib.quote(target_url, '')) if not os.path.isdir(download_cache): create_download_cache_folder(download_cache) + + already_downloaded = None + if download_dir: + already_downloaded = os.path.join(download_dir, link.filename) + if not os.path.exists(already_downloaded): + already_downloaded = None + if (target_file and os.path.exists(target_file) and os.path.exists(target_file + '.content-type')): fp = open(target_file+'.content-type') content_type = fp.read().strip() fp.close() - if link.md5_hash: - download_hash = _get_md5_from_file(target_file, link) + if link.hash and link.hash_name: + download_hash = _get_hash_from_file(target_file, link) temp_location = target_file logger.notify('Using download cache from %s' % target_file) + elif already_downloaded: + temp_location = already_downloaded + content_type = mimetypes.guess_type(already_downloaded) + if link.hash: + download_hash = _get_hash_from_file(temp_location, link) + logger.notify('File was already downloaded %s' % already_downloaded) else: resp = _get_response_from_url(target_url, link) content_type = resp.info()['content-type'] @@ -450,14 +474,14 @@ def unpack_http_url(link, location, download_cache, download_dir=None): filename += ext temp_location = os.path.join(temp_dir, filename) download_hash = _download_url(resp, link, temp_location) - if link.md5_hash: - _check_md5(download_hash, link) - if download_dir: + if link.hash and link.hash_name: + _check_hash(download_hash, link) + if download_dir and not already_downloaded: _copy_file(temp_location, download_dir, content_type, link) unpack_file(temp_location, location, content_type, link) if target_file and target_file != temp_location: cache_download(target_file, temp_location, content_type) - if target_file is None: + if target_file is None and not already_downloaded: os.unlink(temp_location) os.rmdir(temp_dir) diff --git a/pip/index.py b/pip/index.py index 4e79de16c..7f212897e 100644 --- a/pip/index.py +++ b/pip/index.py @@ -20,7 +20,7 @@ from pip.util import Inf from pip.util import normalize_name, splitext from pip.exceptions import DistributionNotFound, BestVersionAlreadyInstalled from pip.backwardcompat import (WindowsError, BytesIO, - Queue, httplib, urlparse, + Queue, urlparse, URLError, HTTPError, u, product, url2pathname) from pip.backwardcompat import Empty as QueueEmpty @@ -29,7 +29,7 @@ from pip.download import urlopen, path_to_url2, url_to_path, geturl, Urllib2Head __all__ = ['PackageFinder'] -DEFAULT_MIRROR_URL = "last.pypi.python.org" +DEFAULT_MIRROR_HOSTNAME = "last.pypi.python.org" class PackageFinder(object): @@ -60,8 +60,7 @@ class PackageFinder(object): ## FIXME: also, we should track comes_from (i.e., use Link) self.dependency_links.extend(links) - @staticmethod - def _sort_locations(locations): + def _sort_locations(self, locations): """ Sort locations into "files" (archives) and "urls", and return a pair of lists (files,urls) @@ -69,8 +68,7 @@ class PackageFinder(object): files = [] urls = [] - # puts the url for the given file path into the appropriate - # list + # puts the url for the given file path into the appropriate list def sort_path(path): url = path_to_url2(path) if mimetypes.guess_type(url, strict=False)[0] == 'text/html': @@ -79,34 +77,30 @@ class PackageFinder(object): files.append(url) for url in locations: - if url.startswith('file:'): - path = url_to_path(url) - if os.path.isdir(path): + + is_local_path = os.path.exists(url) + is_file_url = url.startswith('file:') + is_find_link = url in self.find_links + + if is_local_path or is_file_url: + if is_local_path: + path = url + else: + path = url_to_path(url) + if is_find_link and os.path.isdir(path): path = os.path.realpath(path) for item in os.listdir(path): sort_path(os.path.join(path, item)) + elif is_file_url and os.path.isdir(path): + urls.append(url) elif os.path.isfile(path): sort_path(path) else: urls.append(url) + return files, urls def find_requirement(self, req, upgrade): - url_name = req.url_name - # Only check main index if index URL is given: - main_index_url = None - if self.index_urls: - # Check that we have the url_name correctly spelled: - main_index_url = Link(posixpath.join(self.index_urls[0], url_name)) - # This will also cache the page, so it's okay that we get it again later: - page = self._get_page(main_index_url, req) - if page is None: - url_name = self._find_url_name(Link(self.index_urls[0]), url_name, req) or req.url_name - - # Combine index URLs with mirror URLs here to allow - # adding more index URLs from requirements files - all_index_urls = self.index_urls + self.mirror_urls - def mkurl_pypi_url(url): loc = posixpath.join(url, url_name) # For maximum compatibility with easy_install, ensure the path @@ -116,6 +110,22 @@ class PackageFinder(object): if not loc.endswith('/'): loc = loc + '/' return loc + + url_name = req.url_name + # Only check main index if index URL is given: + main_index_url = None + if self.index_urls: + # Check that we have the url_name correctly spelled: + main_index_url = Link(mkurl_pypi_url(self.index_urls[0])) + # This will also cache the page, so it's okay that we get it again later: + page = self._get_page(main_index_url, req) + if page is None: + url_name = self._find_url_name(Link(self.index_urls[0]), url_name, req) or req.url_name + + # Combine index URLs with mirror URLs here to allow + # adding more index URLs from requirements files + all_index_urls = self.index_urls + self.mirror_urls + if url_name is not None: locations = [ mkurl_pypi_url(url) @@ -156,7 +166,7 @@ class PackageFinder(object): logger.fatal('Could not find any downloads that satisfy the requirement %s' % req) raise DistributionNotFound('No distributions at all found for %s' % req) if req.satisfied_by is not None: - found_versions.append((req.satisfied_by.parsed_version, Inf, req.satisfied_by.version)) + found_versions.append((req.satisfied_by.parsed_version, InfLink, req.satisfied_by.version)) if file_versions: file_versions.sort(reverse=True) logger.info('Local files found: %s' % ', '.join([url_to_path(link.url) for parsed, link, version in file_versions])) @@ -168,31 +178,31 @@ class PackageFinder(object): logger.info("Ignoring link %s, version %s doesn't match %s" % (link, version, ','.join([''.join(s) for s in req.req.specs]))) continue - applicable_versions.append((link, version)) - applicable_versions = sorted(applicable_versions, key=lambda v: pkg_resources.parse_version(v[1]), reverse=True) - existing_applicable = bool([link for link, version in applicable_versions if link is Inf]) + applicable_versions.append((parsed_version, link, version)) + applicable_versions = sorted(applicable_versions, reverse=True) + existing_applicable = bool([link for parsed_version, link, version in applicable_versions if link is InfLink]) if not upgrade and existing_applicable: - if applicable_versions[0][1] is Inf: + if applicable_versions[0][1] is InfLink: logger.info('Existing installed version (%s) is most up-to-date and satisfies requirement' % req.satisfied_by.version) - raise BestVersionAlreadyInstalled else: logger.info('Existing installed version (%s) satisfies requirement (most up-to-date version is %s)' - % (req.satisfied_by.version, applicable_versions[0][1])) + % (req.satisfied_by.version, applicable_versions[0][2])) return None if not applicable_versions: logger.fatal('Could not find a version that satisfies the requirement %s (from versions: %s)' % (req, ', '.join([version for parsed_version, link, version in found_versions]))) raise DistributionNotFound('No distributions matching the version for %s' % req) - if applicable_versions[0][0] is Inf: + if applicable_versions[0][1] is InfLink: # We have an existing version, and its the best version logger.info('Installed version (%s) is most up-to-date (past versions: %s)' - % (req.satisfied_by.version, ', '.join([version for link, version in applicable_versions[1:]]) or 'none')) + % (req.satisfied_by.version, ', '.join([version for parsed_version, link, version in applicable_versions[1:]]) or 'none')) raise BestVersionAlreadyInstalled if len(applicable_versions) > 1: logger.info('Using version %s (newest of versions: %s)' % - (applicable_versions[0][1], ', '.join([version for link, version in applicable_versions]))) - return applicable_versions[0][0] + (applicable_versions[0][2], ', '.join([version for parsed_version, link, version in applicable_versions]))) + return applicable_versions[0][1] + def _find_url_name(self, index_url, url_name, req): """Finds the true URL name of a package, when the given name isn't quite correct. @@ -586,7 +596,7 @@ class Link(object): if self.comes_from: return '%s (from %s)' % (self.url, self.comes_from) else: - return self.url + return str(self.url) def __repr__(self): return '' % self @@ -594,6 +604,21 @@ class Link(object): def __eq__(self, other): return self.url == other.url + def __ne__(self, other): + return self.url != other.url + + def __lt__(self, other): + return self.url < other.url + + def __le__(self, other): + return self.url <= other.url + + def __gt__(self, other): + return self.url > other.url + + def __ge__(self, other): + return self.url >= other.url + def __hash__(self): return hash(self.url) @@ -629,11 +654,18 @@ class Link(object): return None return match.group(1) - _md5_re = re.compile(r'md5=([a-f0-9]+)') + _hash_re = re.compile(r'(sha1|sha224|sha384|sha256|sha512|md5)=([a-f0-9]+)') @property - def md5_hash(self): - match = self._md5_re.search(self.url) + def hash(self): + match = self._hash_re.search(self.url) + if match: + return match.group(2) + return None + + @property + def hash_name(self): + match = self._hash_re.search(self.url) if match: return match.group(1) return None @@ -642,6 +674,9 @@ class Link(object): def show_url(self): return posixpath.basename(self.url.split('#', 1)[0].split('?', 1)[0]) +#An "Infinite Link" that compares greater than other links +InfLink = Link(Inf) + def get_requirement_from_url(url): """Get a requirement from the URL, if possible. This looks for #egg @@ -680,14 +715,17 @@ def get_mirrors(hostname=None): Originally written for the distutils2 project by Alexis Metaireau. """ if hostname is None: - hostname = DEFAULT_MIRROR_URL + hostname = DEFAULT_MIRROR_HOSTNAME # return the last mirror registered on PyPI. + last_mirror_hostname = None try: - hostname = socket.gethostbyname_ex(hostname)[0] + last_mirror_hostname = socket.gethostbyname_ex(hostname)[0] except socket.gaierror: return [] - end_letter = hostname.split(".", 1) + if not last_mirror_hostname or last_mirror_hostname == DEFAULT_MIRROR_HOSTNAME: + last_mirror_hostname = "z.pypi.python.org" + end_letter = last_mirror_hostname.split(".", 1) # determine the list from the last one. return ["%s.%s" % (s, end_letter[1]) for s in string_range(end_letter[0])] diff --git a/pip/locations.py b/pip/locations.py index daf86139c..996b00d23 100644 --- a/pip/locations.py +++ b/pip/locations.py @@ -15,26 +15,26 @@ def running_under_virtualenv(): """ return hasattr(sys, 'real_prefix') + def virtualenv_no_global(): """ Return True if in a venv and no system site packages. """ #this mirrors the logic in virtualenv.py for locating the no-global-site-packages.txt file site_mod_dir = os.path.dirname(os.path.abspath(site.__file__)) - no_global_file = os.path.join(site_mod_dir,'no-global-site-packages.txt') + no_global_file = os.path.join(site_mod_dir, 'no-global-site-packages.txt') if running_under_virtualenv() and os.path.isfile(no_global_file): return True if running_under_virtualenv(): - ## FIXME: is build/ a good name? build_prefix = os.path.join(sys.prefix, 'build') src_prefix = os.path.join(sys.prefix, 'src') else: # Use tempfile to create a temporary folder for build # Note: we are NOT using mkdtemp so we can have a consistent build dir build_prefix = os.path.join(tempfile.gettempdir(), 'pip-build') - + ## FIXME: keep src in cwd for now (it is not a temporary folder) try: src_prefix = os.path.join(os.getcwd(), 'src') diff --git a/pip/req.py b/pip/req.py index efea080f1..6fa43ac48 100644 --- a/pip/req.py +++ b/pip/req.py @@ -1,5 +1,6 @@ from email.parser import FeedParser import os +import imp import pkg_resources import re import sys @@ -16,10 +17,10 @@ from pip.log import logger from pip.util import display_path, rmtree from pip.util import ask, ask_path_exists, backup_dir from pip.util import is_installable_dir, is_local, dist_is_local, dist_in_usersite -from pip.util import renames, normalize_path, egg_link_path +from pip.util import renames, normalize_path, egg_link_path, dist_in_site_packages from pip.util import make_path_relative from pip.util import call_subprocess -from pip.backwardcompat import (urlparse, urllib, +from pip.backwardcompat import (urlparse, urllib, uses_pycache, ConfigParser, string_types, HTTPError, get_python_version, b) from pip.index import Link @@ -94,7 +95,7 @@ class InstallRequirement(object): link = Link(name) elif os.path.isdir(path) and (os.path.sep in name or name.startswith('.')): if not is_installable_dir(path): - raise InstallationError("Directory %r is not installable. File 'setup.py' not found.", name) + raise InstallationError("Directory %r is not installable. File 'setup.py' not found." % name) link = Link(path_to_url(name)) elif is_archive_file(path): if not os.path.isfile(path): @@ -245,14 +246,16 @@ class InstallRequirement(object): _run_setup_py = """ __file__ = __SETUP_PY__ from setuptools.command import egg_info +import pkg_resources +import os def replacement_run(self): self.mkpath(self.egg_info) installer = self.distribution.fetch_build_egg - for ep in egg_info.iter_entry_points('egg_info.writers'): + for ep in pkg_resources.iter_entry_points('egg_info.writers'): # require=False is the change we're making: writer = ep.load(require=False) if writer: - writer(self, ep.name, egg_info.os.path.join(self.egg_info,ep.name)) + writer(self, ep.name, os.path.join(self.egg_info,ep.name)) self.find_sources() egg_info.egg_info.run = replacement_run exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec')) @@ -365,18 +368,9 @@ exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec')) def assert_source_matches_version(self): assert self.source_dir - if self.comes_from is None: - # We don't check the versions of things explicitly installed. - # This makes, e.g., "pip Package==dev" possible - return version = self.installed_version if version not in self.req: - logger.fatal( - 'Source in %s has the version %s, which does not match the requirement %s' - % (display_path(self.source_dir), version, self)) - raise InstallationError( - 'Source in %s has version %s that conflicts with %s' - % (display_path(self.source_dir), version, self)) + logger.warn('Requested %s, but installing version %s' % (self, self.installed_version)) else: logger.debug('Source in %s has version %s, which satisfies requirement %s' % (display_path(self.source_dir), version, self)) @@ -446,7 +440,9 @@ exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec')) for installed_file in dist.get_metadata('installed-files.txt').splitlines(): path = os.path.normpath(os.path.join(egg_info_path, installed_file)) paths_to_remove.add(path) - if dist.has_metadata('top_level.txt'): + #FIXME: need a test for this elif block + #occurs with --single-version-externally-managed/--record outside of pip + elif dist.has_metadata('top_level.txt'): if dist.has_metadata('namespace_packages.txt'): namespaces = dist.get_metadata('namespace_packages.txt') else: @@ -466,7 +462,7 @@ exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec')) 'easy-install.pth') paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg) - elif os.path.isfile(develop_egg_link): + elif develop_egg_link: # develop egg fh = open(develop_egg_link, 'r') link_pointer = os.path.normcase(fh.readline().strip()) @@ -685,6 +681,9 @@ exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec')) if self.use_user_site: if dist_in_usersite(existing_dist): self.conflicts_with = existing_dist + elif running_under_virtualenv() and dist_in_site_packages(existing_dist): + raise InstallationError("Will not install to the user site because it will lack sys.path precedence to %s in %s" + %(existing_dist.project_name, existing_dist.location)) else: self.conflicts_with = existing_dist return True @@ -890,7 +889,7 @@ class RequirementSet(object): req.commit_uninstall() def locate_files(self): - ## FIXME: duplicates code from install_files; relevant code should + ## FIXME: duplicates code from prepare_files; relevant code should ## probably be factored out into a separate method unnamed = list(self.unnamed_requirements) reqs = list(self.requirements.values()) @@ -904,7 +903,9 @@ class RequirementSet(object): req_to_install.check_if_exists() if req_to_install.satisfied_by: if self.upgrade: - req_to_install.conflicts_with = req_to_install.satisfied_by + #don't uninstall conflict if user install and and conflict is not user install + if not (self.use_user_site and not dist_in_usersite(req_to_install.satisfied_by)): + req_to_install.conflicts_with = req_to_install.satisfied_by req_to_install.satisfied_by = None else: install_needed = False @@ -956,7 +957,9 @@ class RequirementSet(object): req_to_install.url = url.url if not best_installed: - req_to_install.conflicts_with = req_to_install.satisfied_by + #don't uninstall conflict if user install and conflict is not user install + if not (self.use_user_site and not dist_in_usersite(req_to_install.satisfied_by)): + req_to_install.conflicts_with = req_to_install.satisfied_by req_to_install.satisfied_by = None else: install = False @@ -998,7 +1001,9 @@ class RequirementSet(object): ##occurs when the script attempts to unpack the ##build directory + # NB: This call can result in the creation of a temporary build directory location = req_to_install.build_location(self.build_dir, not self.is_download) + ## FIXME: is the existance of the checkout good enough to use it? I don't think so. unpack = True url = None @@ -1053,7 +1058,9 @@ class RequirementSet(object): req_to_install.check_if_exists() if req_to_install.satisfied_by: if self.upgrade or self.ignore_installed: - req_to_install.conflicts_with = req_to_install.satisfied_by + #don't uninstall conflict if user install and and conflict is not user install + if not (self.use_user_site and not dist_in_usersite(req_to_install.satisfied_by)): + req_to_install.conflicts_with = req_to_install.satisfied_by req_to_install.satisfied_by = None else: install = False @@ -1079,7 +1086,7 @@ class RequirementSet(object): self.add_requirement(subreq) if req_to_install.name not in self.requirements: self.requirements[req_to_install.name] = req_to_install - if self.is_download: + if self.is_download or req_to_install._temp_build_dir is not None: self.reqs_to_cleanup.append(req_to_install) else: self.reqs_to_cleanup.append(req_to_install) @@ -1133,7 +1140,8 @@ class RequirementSet(object): loc = location if is_vcs_url(link): return unpack_vcs_link(link, loc, only_download) - elif is_file_url(link): + # a local file:// index could have links with hashes + elif not link.hash and is_file_url(link): return unpack_file_url(link, loc) else: if self.download_cache: @@ -1269,9 +1277,10 @@ _scheme_re = re.compile(r'^(http|https|file):', re.I) def parse_requirements(filename, finder=None, comes_from=None, options=None): skip_match = None - skip_regex = options.skip_requirements_regex + skip_regex = options.skip_requirements_regex if options else None if skip_regex: skip_match = re.compile(skip_regex) + reqs_file_dir = os.path.dirname(os.path.abspath(filename)) filename, content = get_file_content(filename, comes_from=comes_from) for line_number, line in enumerate(content.splitlines()): line_number += 1 @@ -1303,6 +1312,10 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None): line = line[len('--find-links'):].strip().lstrip('=') ## FIXME: it would be nice to keep track of the source of ## the find_links: + # support a find-links local path relative to a requirements file + relative_to_reqs_file = os.path.join(reqs_file_dir, line) + if os.path.exists(relative_to_reqs_file): + line = relative_to_reqs_file if finder: finder.find_links.append(line) elif line.startswith('-i') or line.startswith('--index-url'): @@ -1316,6 +1329,8 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None): line = line[len('--extra-index-url'):].strip().lstrip('=') if finder: finder.index_urls.append(line) + elif line.startswith('--no-index'): + finder.index_urls = [] else: comes_from = '-r %s (line %s)' % (filename, line_number) if line.startswith('-e') or line.startswith('--editable'): @@ -1347,7 +1362,7 @@ def parse_editable(editable_req, default_vcs=None): if os.path.isdir(url_no_extras): if not os.path.exists(os.path.join(url_no_extras, 'setup.py')): - raise InstallationError("Directory %r is not installable. File 'setup.py' not found.", url_no_extras) + raise InstallationError("Directory %r is not installable. File 'setup.py' not found." % url_no_extras) # Treating it as code that has already been checked out url_no_extras = path_to_url(url_no_extras) @@ -1428,6 +1443,11 @@ class UninstallPathSet(object): else: self._refuse.add(path) + # __pycache__ files can show up after 'installed-files.txt' is created, due to imports + if os.path.splitext(path)[1] == '.py' and uses_pycache: + self.add(imp.cache_from_source(path)) + + def add_pth(self, pth_file, entry): pth_file = normalize_path(pth_file) if self._permitted(pth_file): @@ -1459,6 +1479,9 @@ class UninstallPathSet(object): ``auto_confirm`` is True).""" if not self._can_uninstall(): return + if not self.paths: + logger.notify("Can't uninstall '%s'. No files were found to uninstall." % self.dist.project_name) + return logger.notify('Uninstalling %s:' % self.dist.project_name) logger.indent += 2 paths = sorted(self.compact(self.paths)) diff --git a/pip/util.py b/pip/util.py index 619f06531..7db95745c 100644 --- a/pip/util.py +++ b/pip/util.py @@ -10,7 +10,7 @@ import tarfile import subprocess from pip.exceptions import InstallationError, BadCommand from pip.backwardcompat import WindowsError, string_types, raw_input, console_to_str, user_site -from pip.locations import site_packages, running_under_virtualenv +from pip.locations import site_packages, running_under_virtualenv, virtualenv_no_global from pip.log import logger __all__ = ['rmtree', 'display_path', 'backup_dir', @@ -20,11 +20,20 @@ __all__ = ['rmtree', 'display_path', 'backup_dir', 'is_svn_page', 'file_contents', 'split_leading_dir', 'has_leading_dir', 'make_path_relative', 'normalize_path', - 'renames', 'get_terminal_size', + 'renames', 'get_terminal_size', 'get_prog', 'unzip_file', 'untar_file', 'create_download_cache_folder', 'cache_download', 'unpack_file', 'call_subprocess'] +def get_prog(): + try: + if os.path.basename(sys.argv[0]) in ('__main__.py', '-c'): + return "%s -m pip" % sys.executable + except (AttributeError, TypeError, IndexError): + pass + return 'pip' + + def rmtree(dir, ignore_errors=False): shutil.rmtree(dir, ignore_errors=ignore_errors, onerror=rmtree_errorhandler) @@ -78,7 +87,7 @@ def find_command(cmd, paths=None, pathext=None): # check if there are funny path extensions for executables, e.g. Windows if pathext is None: pathext = get_pathext() - pathext = [ext for ext in pathext.lower().split(os.pathsep)] + pathext = [ext for ext in pathext.lower().split(os.pathsep) if len(ext)] # don't use extensions if the command ends with one of them if os.path.splitext(cmd)[1].lower() in pathext: pathext = [''] @@ -127,10 +136,27 @@ def ask(message, options): class _Inf(object): """I am bigger than everything!""" - def __cmp__(self, a): - if self is a: - return 0 - return 1 + + def __eq__(self, other): + if self is other: + return True + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + return False + + def __le__(self, other): + return False + + def __gt__(self, other): + return True + + def __ge__(self, other): + return True def __repr__(self): return 'Inf' @@ -303,6 +329,12 @@ def dist_in_usersite(dist): else: return False +def dist_in_site_packages(dist): + """ + Return True if given Distribution is installed in distutils.sysconfig.get_python_lib(). + """ + return normalize_path(dist_location(dist)).startswith(normalize_path(site_packages)) + def get_installed_distributions(local_only=True, skip=('setuptools', 'pip', 'python')): """ @@ -325,16 +357,36 @@ def get_installed_distributions(local_only=True, skip=('setuptools', 'pip', 'pyt def egg_link_path(dist): """ - Return the path where we'd expect to find a .egg-link file for - this distribution. (There doesn't seem to be any metadata in the - Distribution object for a develop egg that points back to its - .egg-link and easy-install.pth files). + Return the path for the .egg-link file if it exists, otherwise, None. - This won't find a globally-installed develop egg if we're in a - virtualenv. + There's 3 scenarios: + 1) not in a virtualenv + try to find in site.USER_SITE, then site_packages + 2) in a no-global virtualenv + try to find in site_packages + 3) in a yes-global virtualenv + try to find in site_packages, then site.USER_SITE (don't look in global location) + For #1 and #3, there could be odd cases, where there's an egg-link in 2 locations. + This method will just return the first one found. """ - return os.path.join(site_packages, dist.project_name) + '.egg-link' + sites = [] + if running_under_virtualenv(): + if virtualenv_no_global(): + sites.append(site_packages) + else: + sites.append(site_packages) + if user_site: + sites.append(user_site) + else: + if user_site: + sites.append(user_site) + sites.append(site_packages) + + for site in sites: + egglink = os.path.join(site, dist.project_name) + '.egg-link' + if os.path.isfile(egglink): + return egglink def dist_location(dist): @@ -346,7 +398,7 @@ def dist_location(dist): """ egg_link = egg_link_path(dist) - if os.path.exists(egg_link): + if egg_link: return egg_link return dist.location diff --git a/pip/vcs/__init__.py b/pip/vcs/__init__.py index 7b122304b..a4b48a538 100644 --- a/pip/vcs/__init__.py +++ b/pip/vcs/__init__.py @@ -19,7 +19,7 @@ class VcsSupport(object): def __init__(self): # Register more schemes with urlparse for various version control systems urlparse.uses_netloc.extend(self.schemes) - # Python 3.3 doesn't have uses_fragment + # Python >= 2.7.4, 3.3 doesn't have uses_fragment if getattr(urlparse, 'uses_fragment', None): urlparse.uses_fragment.extend(self.schemes) super(VcsSupport, self).__init__() diff --git a/pip/vcs/bazaar.py b/pip/vcs/bazaar.py index d5419b2ff..86f4bbc62 100644 --- a/pip/vcs/bazaar.py +++ b/pip/vcs/bazaar.py @@ -19,8 +19,11 @@ class Bazaar(VersionControl): def __init__(self, url=None, *args, **kwargs): super(Bazaar, self).__init__(url, *args, **kwargs) - urlparse.non_hierarchical.extend(['lp']) - urlparse.uses_fragment.extend(['lp']) + # Python >= 2.7.4, 3.3 doesn't have uses_fragment or non_hierarchical + # Register lp but do not expose as a scheme to support bzr+lp. + if getattr(urlparse, 'uses_fragment', None): + urlparse.uses_fragment.extend(['lp']) + urlparse.non_hierarchical.extend(['lp']) def parse_vcs_bundle_file(self, content): url = rev = None diff --git a/pip/vcs/git.py b/pip/vcs/git.py index b90cc1ef7..abb57ac7a 100644 --- a/pip/vcs/git.py +++ b/pip/vcs/git.py @@ -214,8 +214,7 @@ class Git(VersionControl): def update_submodules(self, location): if not os.path.exists(os.path.join(location, '.gitmodules')): return - call_subprocess([self.cmd, 'submodule', 'init', '-q'], cwd=location) - call_subprocess([self.cmd, 'submodule', 'update', '--recursive', '-q'], + call_subprocess([self.cmd, 'submodule', 'update', '--init', '--recursive', '-q'], cwd=location) vcs.register(Git) diff --git a/setup.cfg b/setup.cfg index ce26f6afe..cffcdfd46 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [nosetests] where=tests + +[aliases] +dev = develop easy_install pip[testing] diff --git a/setup.py b/setup.py index 95a7c68ab..4ccc1025b 100644 --- a/setup.py +++ b/setup.py @@ -1,32 +1,39 @@ -import sys +import codecs import os +import re +import sys from setuptools import setup -# If you change this version, change it also in docs/conf.py -version = "1.1.post2" -doc_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "docs") -index_filename = os.path.join(doc_dir, "index.txt") -news_filename = os.path.join(doc_dir, "news.txt") +def read(*parts): + return codecs.open(os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts), 'r').read() + + +def find_version(*file_paths): + version_file = read(*file_paths) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + long_description = """ The main website for pip is `www.pip-installer.org -`_. You can also install +`_. You can also install the `in-development version `_ of pip with ``easy_install pip==dev``. """ -f = open(index_filename) # remove the toctree from sphinx index, as it breaks long_description -parts = f.read().split("split here", 2) -long_description = parts[0] + long_description + parts[2] -f.close() -f = open(news_filename) -long_description += "\n\n" + f.read() -f.close() +parts = read("docs", "index.txt").split("split here", 2) +long_description = (parts[0] + long_description + parts[2] + + "\n\n" + read("docs", "news.txt")) + +tests_require = ['nose', 'virtualenv>=1.7', 'scripttest>=1.1.1', 'mock'] setup(name="pip", - version=version, + version=find_version('pip', '__init__.py'), description="pip installs packages. Python packages. An easy_install replacement", long_description=long_description, classifiers=[ @@ -35,7 +42,6 @@ setup(name="pip", 'License :: OSI Approved :: MIT License', 'Topic :: Software Development :: Build Tools', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.4', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', @@ -51,5 +57,9 @@ setup(name="pip", packages=['pip', 'pip.commands', 'pip.vcs'], entry_points=dict(console_scripts=['pip=pip:main', 'pip-%s=pip:main' % sys.version[:3]]), test_suite='nose.collector', - tests_require=['nose', 'virtualenv>=1.7', 'scripttest>=1.1.1', 'mock'], - zip_safe=False) + tests_require=tests_require, + zip_safe=False, + extras_require = { + 'testing':tests_require, + }, + ) diff --git a/tests/git_submodule_helpers.py b/tests/git_submodule_helpers.py index ef08b2636..ecb789b10 100644 --- a/tests/git_submodule_helpers.py +++ b/tests/git_submodule_helpers.py @@ -44,19 +44,22 @@ def _create_test_package_with_submodule(env): packages=find_packages(), ) '''), version_pkg_path) - env.run('git', 'init', cwd=version_pkg_path) - env.run('git', 'add', '.', cwd=version_pkg_path) + env.run('git', 'init', cwd=version_pkg_path, expect_error=True) + env.run('git', 'add', '.', cwd=version_pkg_path, expect_error=True) env.run('git', 'commit', '-q', '--author', 'Pip ', - '-am', 'initial version', cwd=version_pkg_path) + '-am', 'initial version', cwd=version_pkg_path, + expect_error=True) submodule_path = _create_test_package_submodule(env) - env.run('git', 'submodule', 'add', submodule_path, 'testpkg/static', cwd=version_pkg_path) + env.run('git', 'submodule', 'add', submodule_path, 'testpkg/static', cwd=version_pkg_path, + expect_error=True) env.run('git', 'commit', '-q', '--author', 'Pip ', - '-am', 'initial version w submodule', cwd=version_pkg_path) + '-am', 'initial version w submodule', cwd=version_pkg_path, + expect_error=True) return version_pkg_path, submodule_path diff --git a/tests/in dex/FSPkg/FSPkg-0.1dev.tar.gz b/tests/in dex/FSPkg/FSPkg-0.1dev.tar.gz deleted file mode 100644 index 7fa7c10c2..000000000 Binary files a/tests/in dex/FSPkg/FSPkg-0.1dev.tar.gz and /dev/null differ diff --git a/tests/in dex/FSPkg/index.html b/tests/in dex/FSPkg/index.html deleted file mode 100644 index cf4f404e3..000000000 --- a/tests/in dex/FSPkg/index.html +++ /dev/null @@ -1,3 +0,0 @@ -Links for FSPkg

Links for FSPkg

FSPkg-0.1dev.tar.gz
-Source
- diff --git a/tests/indexes/README.txt b/tests/indexes/README.txt new file mode 100644 index 000000000..8e430effd --- /dev/null +++ b/tests/indexes/README.txt @@ -0,0 +1,15 @@ + +Details on Test Indexes +======================= + +empty_with_pkg +-------------- +empty index, but there's a package in the dir + +in dex +------ +for testing url quoting with indexes + +simple +------ +contains index page for "simple" pkg diff --git a/tests/indexes/empty_with_pkg/index.html b/tests/indexes/empty_with_pkg/index.html new file mode 100644 index 000000000..e69de29bb diff --git a/tests/indexes/empty_with_pkg/simple-1.0.tar.gz b/tests/indexes/empty_with_pkg/simple-1.0.tar.gz new file mode 100644 index 000000000..6d9f2dd8b Binary files /dev/null and b/tests/indexes/empty_with_pkg/simple-1.0.tar.gz differ diff --git a/tests/in dex/README.txt b/tests/indexes/in dex/README.txt similarity index 100% rename from tests/in dex/README.txt rename to tests/indexes/in dex/README.txt diff --git a/tests/indexes/in dex/simple/index.html b/tests/indexes/in dex/simple/index.html new file mode 100644 index 000000000..dba6cc3eb --- /dev/null +++ b/tests/indexes/in dex/simple/index.html @@ -0,0 +1,5 @@ + + + simple-1.0.tar.gz + + diff --git a/tests/indexes/simple/simple/index.html b/tests/indexes/simple/simple/index.html new file mode 100644 index 000000000..dba6cc3eb --- /dev/null +++ b/tests/indexes/simple/simple/index.html @@ -0,0 +1,5 @@ + + + simple-1.0.tar.gz + + diff --git a/tests/packages/HackedEggInfo/setup.py b/tests/packages/HackedEggInfo/setup.py new file mode 100755 index 000000000..c34b57c4b --- /dev/null +++ b/tests/packages/HackedEggInfo/setup.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from setuptools import setup +from setuptools.command import egg_info as orig_egg_info + +class egg_info (orig_egg_info.egg_info): + def run(self): + orig_egg_info.egg_info.run(self) + + +setup( + name = "hackedegginfo", + version = '0.0.0', + cmdclass = {'egg_info':egg_info }, + zip_safe = False, +) + diff --git a/tests/packages/LocalExtras/setup.py b/tests/packages/LocalExtras/setup.py index 646c1671f..c7965c303 100644 --- a/tests/packages/LocalExtras/setup.py +++ b/tests/packages/LocalExtras/setup.py @@ -2,12 +2,12 @@ import os from setuptools import setup, find_packages HERE = os.path.dirname(__file__) -INDEX = os.path.join(HERE, '..', '..', 'in dex', 'FSPkg') +INDEX = "file://" + os.path.join(HERE, '..', '..', 'indexes', 'simple', 'simple') setup( name='LocalExtras', version='0.0.1', packages=find_packages(), - extras_require={ 'bar': ['FSPkg'] }, - dependency_links=['file://' + INDEX] + extras_require={ 'bar': ['simple'] }, + dependency_links=[INDEX] ) diff --git a/tests/packages/README.txt b/tests/packages/README.txt index b6ecde635..0d25eb59d 100644 --- a/tests/packages/README.txt +++ b/tests/packages/README.txt @@ -1,6 +1,70 @@ -This package exists for testing uninstall-rollback. +Details on Test Packages +======================== + +broken-0.1.tar.gz +----------------- +This package exists for testing uninstall-rollback. + +broken-0.2broken.tar.gz +----------------------- Version 0.2broken has a setup.py crafted to fail on install (and only on install). If any earlier step would fail (i.e. egg-info-generation), the already-installed version would never be uninstalled, so uninstall-rollback would not come into play. + +BrokenEmitsUTF8 +--------------- +for generating unicode error in py3.x + +duplicate-1.0.tar.gz +-------------------- +for testing finding dupes across multiple find-links + +FSPkg +----- +for installing from the file system + +gmpy-1.15.tar.gz +---------------- +hash testing (altough this pkg isn't needed explicitly) + +gmpy2-2.0.tar.gz +---------------- +for testing finder logic when name *contains* the name of the package specified + +HackedEggInfo +------------- +has it's own egg_info class + +LineEndings +----------- +contains DOS line endings + +LocalExtras +----------- +has an extra in a local file:// dependency link + +parent/child-0.1.tar.gz +----------------------- +The parent-0.1.tar.gz and child-0.1.tar.gz packages are used by +test_uninstall:test_uninstall_overlapping_package. + +paxpkg.tar.bz2 +-------------- +tar with pax headers + +pkgwithmpkg-1.0.tar.gz; pkgwithmpkg-1.0-py2.7-macosx10.7.mpkg.zip +----------------------------------------------------------------- +used for osx test case (tests.test_finder:test_no_mpkg) + +simple-[123].0.tar.gz +--------------------- +contains "simple" package; good for basic testing and version logic. + + + + + + + diff --git a/tests/packages/child-0.1.tar.gz b/tests/packages/child-0.1.tar.gz new file mode 100644 index 000000000..2fb34ca5d Binary files /dev/null and b/tests/packages/child-0.1.tar.gz differ diff --git a/tests/packages/duplicate-1.0.tar.gz b/tests/packages/duplicate-1.0.tar.gz new file mode 100644 index 000000000..e69de29bb diff --git a/tests/packages/parent-0.1.tar.gz b/tests/packages/parent-0.1.tar.gz new file mode 100644 index 000000000..130e756f4 Binary files /dev/null and b/tests/packages/parent-0.1.tar.gz differ diff --git a/tests/packages/simple-1.0.tar.gz b/tests/packages/simple-1.0.tar.gz new file mode 100644 index 000000000..6d9f2dd8b Binary files /dev/null and b/tests/packages/simple-1.0.tar.gz differ diff --git a/tests/packages/simple-2.0.tar.gz b/tests/packages/simple-2.0.tar.gz new file mode 100644 index 000000000..c53a523f1 Binary files /dev/null and b/tests/packages/simple-2.0.tar.gz differ diff --git a/tests/packages/simple-3.0.tar.gz b/tests/packages/simple-3.0.tar.gz new file mode 100644 index 000000000..055dd5253 Binary files /dev/null and b/tests/packages/simple-3.0.tar.gz differ diff --git a/tests/packages2/duplicate-1.0.tar.gz b/tests/packages2/duplicate-1.0.tar.gz new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_basic.py b/tests/test_basic.py index c27e28fd1..e1b990a35 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -5,7 +5,6 @@ import textwrap import sys from os.path import abspath, join, curdir, pardir -from nose import SkipTest from nose.tools import assert_raises from mock import patch @@ -376,6 +375,16 @@ def test_install_with_pax_header(): run_pip('install', 'paxpkg.tar.bz2', cwd=run_from) +def test_install_with_hacked_egg_info(): + """ + test installing a package which defines its own egg_info class + """ + reset_env() + run_from = abspath(join(here, 'packages', 'HackedEggInfo')) + result = run_pip('install', '.', cwd=run_from) + assert 'Successfully installed hackedegginfo\n' in result.stdout + + def test_install_using_install_option_and_editable(): """ Test installing a tool using -e and --install-option diff --git a/tests/test_cleanup.py b/tests/test_cleanup.py index 15a050833..c57db91ee 100644 --- a/tests/test_cleanup.py +++ b/tests/test_cleanup.py @@ -17,6 +17,7 @@ def test_cleanup_after_install_from_pypi(): src = env.scratch_path/"src" assert not exists(build), "build/ dir still exists: %s" % build assert not exists(src), "unexpected src/ dir exists: %s" % src + env.assert_no_temp() def test_cleanup_after_install_editable_from_hg(): @@ -34,6 +35,7 @@ def test_cleanup_after_install_editable_from_hg(): src = env.venv_path/'src' assert not exists(build), "build/ dir still exists: %s" % build assert exists(src), "expected src/ dir doesn't exist: %s" % src + env.assert_no_temp() def test_cleanup_after_install_from_local_directory(): @@ -48,6 +50,7 @@ def test_cleanup_after_install_from_local_directory(): src = env.venv_path/'src' assert not exists(build), "unexpected build/ dir exists: %s" % build assert not exists(src), "unexpected src/ dir exist: %s" % src + env.assert_no_temp() def test_cleanup_after_create_bundle(): @@ -79,6 +82,7 @@ def test_cleanup_after_create_bundle(): src_bundle = env.scratch_path/"src-bundle" assert not exists(build_bundle), "build-bundle/ dir still exists: %s" % build_bundle assert not exists(src_bundle), "src-bundle/ dir still exists: %s" % src_bundle + env.assert_no_temp() # Make sure previously created src/ from editable still exists assert exists(src), "expected src dir doesn't exist: %s" % src @@ -96,6 +100,25 @@ def test_no_install_and_download_should_not_leave_build_dir(): assert not os.path.exists(env.venv_path/'/build'), "build/ dir should be deleted" +def test_cleanup_req_satisifed_no_name(): + """ + Test cleanup when req is already satisfied, and req has no 'name' + """ + #this test confirms Issue #420 is fixed + #reqs with no 'name' that were already satisfied were leaving behind tmp build dirs + #2 examples of reqs that would do this + # 1) https://bitbucket.org/ianb/initools/get/tip.zip + # 2) parent-0.1.tar.gz + + dist = abspath(join(here, 'packages', 'parent-0.1.tar.gz')) + env = reset_env() + result = run_pip('install', dist) + result = run_pip('install', dist) + build = env.venv_path/'build' + assert not exists(build), "unexpected build/ dir exists: %s" % build + env.assert_no_temp() + + def test_download_should_not_delete_existing_build_dir(): """ It should not delete build/ if existing before run the command diff --git a/tests/test_completion.py b/tests/test_completion.py index 9381cf11d..9a8a451f0 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -45,7 +45,7 @@ def test_completion_for_unknown_shell(): Test getting completion for an unknown shell """ reset_env() - error_msg = 'error: no such option: --myfooshell' + error_msg = 'no such option: --myfooshell' result = run_pip('completion', '--myfooshell', expect_error=True) assert error_msg in result.stderr, 'tests for an unknown shell failed' diff --git a/tests/test_download.py b/tests/test_download.py index 829d4495e..79ef96a0e 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -39,3 +39,32 @@ def test_download_should_download_dependencies(): openid_tarball_prefix = str(Path('scratch')/ 'python-openid-') assert any(path.startswith(openid_tarball_prefix) for path in result.files_created) assert env.site_packages/ 'openid' not in result.files_created + + +def test_download_should_skip_existing_files(): + """ + It should not download files already existing in the scratch dir + """ + env = reset_env() + + write_file('test-req.txt', textwrap.dedent(""" + INITools==0.1 + """)) + + result = run_pip('install', '-r', env.scratch_path/ 'test-req.txt', '-d', '.', expect_error=True) + assert Path('scratch')/ 'INITools-0.1.tar.gz' in result.files_created + assert env.site_packages/ 'initools' not in result.files_created + + # adding second package to test-req.txt + write_file('test-req.txt', textwrap.dedent(""" + INITools==0.1 + python-openid==2.2.5 + """)) + + # only the second package should be downloaded + result = run_pip('install', '-r', env.scratch_path/ 'test-req.txt', '-d', '.', expect_error=True) + openid_tarball_prefix = str(Path('scratch')/ 'python-openid-') + assert any(path.startswith(openid_tarball_prefix) for path in result.files_created) + assert Path('scratch')/ 'INITools-0.1.tar.gz' not in result.files_created + assert env.site_packages/ 'initools' not in result.files_created + assert env.site_packages/ 'openid' not in result.files_created diff --git a/tests/test_file_scheme_index.py b/tests/test_file_scheme_index.py deleted file mode 100644 index 38f6654b9..000000000 --- a/tests/test_file_scheme_index.py +++ /dev/null @@ -1,16 +0,0 @@ -from pip.backwardcompat import urllib -from tests.test_pip import here, reset_env, run_pip, pyversion -from tests.path import Path - -index_url = 'file://' + urllib.quote(str(Path(here).abspath/'in dex').replace('\\', '/')) - - -def test_install(): - """ - Test installing from a local index. - - """ - env = reset_env() - result = run_pip('install', '-vvv', '--index-url', index_url, 'FSPkg', expect_error=False) - assert (env.site_packages/'fspkg') in result.files_created, str(result.stdout) - assert (env.site_packages/'FSPkg-0.1dev-py%s.egg-info' % pyversion) in result.files_created, str(result) diff --git a/tests/test_find_links.py b/tests/test_find_links.py new file mode 100644 index 000000000..b9f3cde55 --- /dev/null +++ b/tests/test_find_links.py @@ -0,0 +1,38 @@ +import textwrap + +from tests.test_pip import reset_env, run_pip, pyversion, here, write_file + + +def test_find_links_relative_path(): + """Test find-links as a relative path.""" + e = reset_env() + result = run_pip( + 'install', + 'parent==0.1', + '--no-index', + '--find-links', + 'packages/', + cwd=here) + egg_info_folder = e.site_packages / 'parent-0.1-py%s.egg-info' % pyversion + initools_folder = e.site_packages / 'parent' + assert egg_info_folder in result.files_created, str(result) + assert initools_folder in result.files_created, str(result) + + +def test_find_links_requirements_file_relative_path(): + """Test find-links as a relative path to a reqs file.""" + e = reset_env() + write_file('test-req.txt', textwrap.dedent(""" + --no-index + --find-links=../../../packages/ + parent==0.1 + """)) + result = run_pip( + 'install', + '-r', + e.scratch_path / "test-req.txt", + cwd=here) + egg_info_folder = e.site_packages / 'parent-0.1-py%s.egg-info' % pyversion + initools_folder = e.site_packages / 'parent' + assert egg_info_folder in result.files_created, str(result) + assert initools_folder in result.files_created, str(result) diff --git a/tests/test_finder.py b/tests/test_finder.py index cdff3a932..dfd3b6512 100644 --- a/tests/test_finder.py +++ b/tests/test_finder.py @@ -1,12 +1,15 @@ +from pkg_resources import parse_version from pip.backwardcompat import urllib - from pip.req import InstallRequirement from pip.index import PackageFinder - +from pip.exceptions import BestVersionAlreadyInstalled from tests.path import Path from tests.test_pip import here +from nose.tools import assert_raises +from mock import Mock find_links = 'file://' + urllib.quote(str(Path(here).abspath/'packages').replace('\\', '/')) +find_links2 = 'file://' + urllib.quote(str(Path(here).abspath/'packages2').replace('\\', '/')) def test_no_mpkg(): @@ -25,3 +28,50 @@ def test_no_partial_name_match(): found = finder.find_requirement(req, False) assert found.url.endswith("gmpy-1.15.tar.gz"), found + +def test_duplicates_sort_ok(): + """Finder successfully finds one of a set of duplicates in different + locations""" + finder = PackageFinder([find_links, find_links2], []) + req = InstallRequirement.from_line("duplicate") + found = finder.find_requirement(req, False) + + assert found.url.endswith("duplicate-1.0.tar.gz"), found + + +def test_finder_detects_latest_find_links(): + """Test PackageFinder detects latest using find-links""" + req = InstallRequirement.from_line('simple', None) + finder = PackageFinder([find_links], []) + link = finder.find_requirement(req, False) + assert link.url.endswith("simple-3.0.tar.gz") + + +def test_finder_detects_latest_already_satisfied_find_links(): + """Test PackageFinder detects latest already satisified using find-links""" + req = InstallRequirement.from_line('simple', None) + #the latest simple in local pkgs is 3.0 + latest_version = "3.0" + satisfied_by = Mock( + location = "/path", + parsed_version = parse_version(latest_version), + version = latest_version + ) + req.satisfied_by = satisfied_by + finder = PackageFinder([find_links], []) + assert_raises(BestVersionAlreadyInstalled, finder.find_requirement, req, True) + + +def test_finder_detects_latest_already_satisfied_pypi_links(): + """Test PackageFinder detects latest already satisified using pypi links""" + req = InstallRequirement.from_line('initools', None) + #the latest initools on pypi is 0.3.1 + latest_version = "0.3.1" + satisfied_by = Mock( + location = "/path", + parsed_version = parse_version(latest_version), + version = latest_version + ) + req.satisfied_by = satisfied_by + finder = PackageFinder([], ["http://pypi.python.org/simple"]) + assert_raises(BestVersionAlreadyInstalled, finder.find_requirement, req, True) diff --git a/tests/test_freeze.py b/tests/test_freeze.py index 4412c1435..7e2a8b3ea 100644 --- a/tests/test_freeze.py +++ b/tests/test_freeze.py @@ -91,7 +91,7 @@ def test_freeze_git_clone(): expected = textwrap.dedent("""\ Script result: ...pip freeze -- stdout: -------------------- - -e %s@...#egg=pip_test_package-... + ...-e %s@...#egg=pip_test_package-... ...""" % local_checkout('git+http://github.com/pypa/pip-test-package.git')) _check_output(result, expected) @@ -101,7 +101,7 @@ def test_freeze_git_clone(): expected = textwrap.dedent("""\ Script result: pip freeze -f %(repo)s#egg=pip_test_package -- stdout: -------------------- - -f %(repo)s#egg=pip_test_package + -f %(repo)s#egg=pip_test_package... -e %(repo)s@...#egg=pip_test_package-dev ...""" % {'repo': local_checkout('git+http://github.com/pypa/pip-test-package.git')}) _check_output(result, expected) @@ -124,7 +124,7 @@ def test_freeze_mercurial_clone(): expected = textwrap.dedent("""\ Script result: ...pip freeze -- stdout: -------------------- - -e %s@...#egg=django_authority-... + ...-e %s@...#egg=django_authority-... ...""" % local_checkout('hg+http://bitbucket.org/jezdez/django-authority')) _check_output(result, expected) @@ -135,7 +135,7 @@ def test_freeze_mercurial_clone(): Script result: ...pip freeze -f %(repo)s#egg=django_authority -- stdout: -------------------- -f %(repo)s#egg=django_authority - -e %(repo)s@...#egg=django_authority-dev + ...-e %(repo)s@...#egg=django_authority-dev ...""" % {'repo': local_checkout('hg+http://bitbucket.org/jezdez/django-authority')}) _check_output(result, expected) @@ -156,7 +156,7 @@ def test_freeze_bazaar_clone(): expected = textwrap.dedent("""\ Script result: ...pip freeze -- stdout: -------------------- - -e %s@...#egg=django_wikiapp-... + ...-e %s@...#egg=django_wikiapp-... ...""" % local_checkout('bzr+http://bazaar.launchpad.net/%7Edjango-wikiapp/django-wikiapp/release-0.1')) _check_output(result, expected) @@ -168,7 +168,7 @@ def test_freeze_bazaar_clone(): Script result: ...pip freeze -f %(repo)s/#egg=django-wikiapp -- stdout: -------------------- -f %(repo)s/#egg=django-wikiapp - -e %(repo)s@...#egg=django_wikiapp-... + ...-e %(repo)s@...#egg=django_wikiapp-... ...""" % {'repo': local_checkout('bzr+http://bazaar.launchpad.net/%7Edjango-wikiapp/django-wikiapp/release-0.1')}) _check_output(result, expected) diff --git a/tests/test_hashes.py b/tests/test_hashes.py new file mode 100644 index 000000000..79c4f1649 --- /dev/null +++ b/tests/test_hashes.py @@ -0,0 +1,194 @@ +import os + +from nose.tools import assert_raises + +from pip.download import _get_hash_from_file, _check_hash +from pip.exceptions import InstallationError +from pip.index import Link + + +def test_get_hash_from_file_md5(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#md5=d41d8cd98f00b204e9800998ecf8427e") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert download_hash.digest_size == 16 + assert download_hash.hexdigest() == "d41d8cd98f00b204e9800998ecf8427e" + + +def test_get_hash_from_file_sha1(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha1=da39a3ee5e6b4b0d3255bfef95601890afd80709") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert download_hash.digest_size == 20 + assert download_hash.hexdigest() == "da39a3ee5e6b4b0d3255bfef95601890afd80709" + + +def test_get_hash_from_file_sha224(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha224=d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert download_hash.digest_size == 28 + assert download_hash.hexdigest() == "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f" + + +def test_get_hash_from_file_sha384(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha384=38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert download_hash.digest_size == 48 + assert download_hash.hexdigest() == "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b" + + +def test_get_hash_from_file_sha256(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert download_hash.digest_size == 32 + assert download_hash.hexdigest() == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + +def test_get_hash_from_file_sha512(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha512=cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert download_hash.digest_size == 64 + assert download_hash.hexdigest() == "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" + + +def test_get_hash_from_file_unknown(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#unknown_hash=d41d8cd98f00b204e9800998ecf8427e") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert download_hash is None + + +def test_check_hash_md5_valid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#md5=d41d8cd98f00b204e9800998ecf8427e") + + download_hash = _get_hash_from_file(file_path, file_link) + + _check_hash(download_hash, file_link) + + +def test_check_hash_md5_invalid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#md5=deadbeef") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert_raises(InstallationError, _check_hash, download_hash, file_link) + + +def test_check_hash_sha1_valid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha1=da39a3ee5e6b4b0d3255bfef95601890afd80709") + + download_hash = _get_hash_from_file(file_path, file_link) + + _check_hash(download_hash, file_link) + + +def test_check_hash_sha1_invalid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha1=deadbeef") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert_raises(InstallationError, _check_hash, download_hash, file_link) + + +def test_check_hash_sha224_valid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha224=d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f'") + + download_hash = _get_hash_from_file(file_path, file_link) + + _check_hash(download_hash, file_link) + + +def test_check_hash_sha224_invalid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha224=deadbeef") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert_raises(InstallationError, _check_hash, download_hash, file_link) + + +def test_check_hash_sha384_valid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha384=38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b") + + download_hash = _get_hash_from_file(file_path, file_link) + + _check_hash(download_hash, file_link) + + +def test_check_hash_sha384_invalid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha384=deadbeef") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert_raises(InstallationError, _check_hash, download_hash, file_link) + + +def test_check_hash_sha256_valid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + + download_hash = _get_hash_from_file(file_path, file_link) + + _check_hash(download_hash, file_link) + + +def test_check_hash_sha256_invalid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha256=deadbeef") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert_raises(InstallationError, _check_hash, download_hash, file_link) + + +def test_check_hash_sha512_valid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha512=cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + + download_hash = _get_hash_from_file(file_path, file_link) + + _check_hash(download_hash, file_link) + + +def test_check_hash_sha512_invalid(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#sha512=deadbeef") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert_raises(InstallationError, _check_hash, download_hash, file_link) + + +def test_check_hasher_mismsatch(): + file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages", "gmpy-1.15.tar.gz") + file_link = Link("http://testserver/gmpy-1.15.tar.gz#md5=d41d8cd98f00b204e9800998ecf8427e") + other_link = Link("http://testserver/gmpy-1.15.tar.gz#sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + + download_hash = _get_hash_from_file(file_path, file_link) + + assert_raises(InstallationError, _check_hash, download_hash, other_link) diff --git a/tests/test_index.py b/tests/test_index.py index 6f9d216dc..a107b2c58 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,4 +1,11 @@ -from pip.index import package_to_requirement, HTMLPage +import os +from pip.backwardcompat import urllib +from tests.path import Path +from pip.index import package_to_requirement, HTMLPage, get_mirrors, DEFAULT_MIRROR_HOSTNAME +from pip.index import PackageFinder, Link, InfLink +from tests.test_pip import reset_env, run_pip, pyversion, here +from string import ascii_lowercase +from mock import patch def test_package_name_should_be_converted_to_requirement(): @@ -26,3 +33,75 @@ def test_html_page_should_be_able_to_scrap_rel_links(): assert len(links) == 1 assert links[0].url == 'http://supervisord.org/' +@patch('socket.gethostbyname_ex') +def test_get_mirrors(mock_gethostbyname_ex): + # Test when the expected result comes back + # from socket.gethostbyname_ex + mock_gethostbyname_ex.return_value = ('g.pypi.python.org', [DEFAULT_MIRROR_HOSTNAME], ['129.21.171.98']) + mirrors = get_mirrors() + # Expect [a-g].pypi.python.org, since last mirror + # is returned as g.pypi.python.org + assert len(mirrors) == 7 + for c in "abcdefg": + assert c + ".pypi.python.org" in mirrors + +@patch('socket.gethostbyname_ex') +def test_get_mirrors_no_cname(mock_gethostbyname_ex): + # Test when the UNexpected result comes back + # from socket.gethostbyname_ex + # (seeing this in Japan and was resulting in 216k + # invalid mirrors and a hot CPU) + mock_gethostbyname_ex.return_value = (DEFAULT_MIRROR_HOSTNAME, [DEFAULT_MIRROR_HOSTNAME], ['129.21.171.98']) + mirrors = get_mirrors() + # Falls back to [a-z].pypi.python.org + assert len(mirrors) == 26 + for c in ascii_lowercase: + assert c + ".pypi.python.org" in mirrors + + +def test_sort_locations_file_find_link(): + """ + Test that a file:// find-link dir gets listdir run + """ + find_links_url = 'file://' + os.path.join(here, 'packages') + find_links = [find_links_url] + finder = PackageFinder(find_links, []) + files, urls = finder._sort_locations(find_links) + assert files and not urls, "files and not urls should have been found at find-links url: %s" % find_links_url + + +def test_sort_locations_file_not_find_link(): + """ + Test that a file:// url dir that's not a find-link, doesn't get a listdir run + """ + index_url = 'file://' + os.path.join(here, 'indexes', 'empty_with_pkg') + finder = PackageFinder([], []) + files, urls = finder._sort_locations([index_url]) + assert urls and not files, "urls, but not files should have been found" + + +def test_install_from_file_index_hash_link(): + """ + Test that a pkg can be installed from a file:// index using a link with a hash + """ + env = reset_env() + index_url = 'file://' + os.path.join(here, 'indexes', 'simple') + result = run_pip('install', '-i', index_url, 'simple==1.0') + egg_info_folder = env.site_packages / 'simple-1.0-py%s.egg-info' % pyversion + assert egg_info_folder in result.files_created, str(result) + + +def test_file_index_url_quoting(): + """ + Test url quoting of file index url with a space + """ + index_url = 'file://' + urllib.quote(str(Path(here).abspath/'indexes'/'in dex').replace('\\', '/')) + env = reset_env() + result = run_pip('install', '-vvv', '--index-url', index_url, 'simple', expect_error=False) + assert (env.site_packages/'simple') in result.files_created, str(result.stdout) + assert (env.site_packages/'simple-1.0-py%s.egg-info' % pyversion) in result.files_created, str(result) + + +def test_inflink_greater(): + """Test InfLink compares greater.""" + assert InfLink > Link(object()) diff --git a/tests/test_pip.py b/tests/test_pip.py index 10f075b98..8612232cf 100644 --- a/tests/test_pip.py +++ b/tests/test_pip.py @@ -314,12 +314,18 @@ class TestPipEnvironment(TestFileEnvironment): assert self.venv_path == virtualenv_paths[0] # sanity check for id, path in zip(('venv', 'lib', 'include', 'bin'), virtualenv_paths): + #fix for virtualenv issue #306 + if hasattr(sys, "pypy_version_info") and id == 'lib': + path = os.path.join(self.venv_path, 'lib-python', pyversion) setattr(self, id+'_path', Path(path)) setattr(self, id, relpath(self.root_path, path)) assert self.venv == TestPipEnvironment.venv # sanity check - self.site_packages = self.lib/'site-packages' + if hasattr(sys, "pypy_version_info"): + self.site_packages = self.venv/'site-packages' + else: + self.site_packages = self.lib/'site-packages' self.user_base_path = self.venv_path/'user' self.user_site_path = self.venv_path/'user'/site_packages_suffix @@ -362,6 +368,10 @@ class TestPipEnvironment(TestFileEnvironment): if sitecustomize: self._add_to_sitecustomize(sitecustomize) + # Ensure that $TMPDIR exists (because we use start_clear=False, it's not created for us) + if self.temp_path and not os.path.exists(self.temp_path): + os.makedirs(self.temp_path) + def _ignore_file(self, fn): if fn.endswith('__pycache__') or fn.endswith(".pyc"): result = True @@ -444,12 +454,18 @@ class FastTestPipEnvironment(TestPipEnvironment): virtualenv_paths = virtualenv.path_locations(self.venv_path) for id, path in zip(('venv', 'lib', 'include', 'bin'), virtualenv_paths): + #fix for virtualenv issue #306 + if hasattr(sys, "pypy_version_info") and id == 'lib': + path = os.path.join(self.venv_path, 'lib-python', pyversion) setattr(self, id+'_path', Path(path)) setattr(self, id, relpath(self.root_path, path)) assert self.venv == TestPipEnvironment.venv # sanity check - self.site_packages = self.lib/'site-packages' + if hasattr(sys, "pypy_version_info"): + self.site_packages = self.venv/'site-packages' + else: + self.site_packages = self.lib/'site-packages' self.user_base_path = self.venv_path/'user' self.user_site_path = self.venv_path/'user'/'lib'/self.lib.name/'site-packages' @@ -510,6 +526,10 @@ class FastTestPipEnvironment(TestPipEnvironment): assert self.root_path.exists + # Ensure that $TMPDIR exists (because we use start_clear=False, it's not created for us) + if self.temp_path and not os.path.exists(self.temp_path): + os.makedirs(self.temp_path) + def __del__(self): pass # shutil.rmtree(str(self.root_path), ignore_errors=True) @@ -663,5 +683,5 @@ def _change_test_package_version(env, version_pkg_path): if __name__ == '__main__': - sys.stderr.write("Run pip's tests using nosetests. Requires virtualenv, ScriptTest, and nose.\n") + sys.stderr.write("Run pip's tests using nosetests. Requires virtualenv, ScriptTest, mock, and nose.\n") sys.exit(1) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 6fd8c67a5..47a274824 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -80,20 +80,23 @@ def test_multiple_requirements_files(): def test_respect_order_in_requirements_file(): env = reset_env() write_file('frameworks-req.txt', textwrap.dedent("""\ - bidict - ordereddict - initools + parent + child + simple """)) - result = run_pip('install', '-r', env.scratch_path / 'frameworks-req.txt') + + find_links = 'file://' + os.path.join(here, 'packages') + result = run_pip('install', '--no-index', '-f', find_links, '-r', env.scratch_path / 'frameworks-req.txt') + downloaded = [line for line in result.stdout.split('\n') if 'Downloading/unpacking' in line] - assert 'bidict' in downloaded[0], 'First download should ' \ - 'be "bidict" but was "%s"' % downloaded[0] - assert 'ordereddict' in downloaded[1], 'Second download should ' \ - 'be "ordereddict" but was "%s"' % downloaded[1] - assert 'initools' in downloaded[2], 'Third download should ' \ - 'be "initools" but was "%s"' % downloaded[2] + assert 'parent' in downloaded[0], 'First download should ' \ + 'be "parent" but was "%s"' % downloaded[0] + assert 'child' in downloaded[1], 'Second download should ' \ + 'be "child" but was "%s"' % downloaded[1] + assert 'simple' in downloaded[2], 'Third download should ' \ + 'be "simple" but was "%s"' % downloaded[2] def test_requirements_data_structure_keeps_order(): @@ -121,19 +124,22 @@ def test_requirements_data_structure_implements__contains__(): assert 'pip' in requirements assert 'nose' not in requirements +@patch('os.path.normcase') @patch('pip.req.os.getcwd') @patch('pip.req.os.path.exists') @patch('pip.req.os.path.isdir') -def test_parse_editable_local(isdir_mock, exists_mock, getcwd_mock): +def test_parse_editable_local(isdir_mock, exists_mock, getcwd_mock, normcase_mock): exists_mock.return_value = isdir_mock.return_value = True - getcwd_mock.return_value = "/some/path" + # mocks needed to support path operations on windows tests + normcase_mock.return_value = getcwd_mock.return_value = "/some/path" assert_equal( parse_editable('.', 'git'), (None, 'file:///some/path', None) ) + normcase_mock.return_value = "/some/path/foo" assert_equal( parse_editable('foo', 'git'), - (None, 'file://' + os.path.join("/some/path", 'foo'), None) + (None, 'file:///some/path/foo', None) ) def test_parse_editable_default_vcs(): @@ -154,19 +160,21 @@ def test_parse_editable_vcs_extras(): ('foo[extras]', 'svn+https://foo#egg=foo[extras]', None) ) +@patch('os.path.normcase') @patch('pip.req.os.getcwd') @patch('pip.req.os.path.exists') @patch('pip.req.os.path.isdir') -def test_parse_editable_local_extras(isdir_mock, exists_mock, getcwd_mock): +def test_parse_editable_local_extras(isdir_mock, exists_mock, getcwd_mock, normcase_mock): exists_mock.return_value = isdir_mock.return_value = True - getcwd_mock.return_value = "/some/path" + normcase_mock.return_value = getcwd_mock.return_value = "/some/path" assert_equal( parse_editable('.[extras]', 'git'), (None, 'file://' + "/some/path", ('extras',)) ) + normcase_mock.return_value = "/some/path/foo" assert_equal( parse_editable('foo[bar,baz]', 'git'), - (None, 'file://' + os.path.join("/some/path", 'foo'), ('bar', 'baz')) + (None, 'file:///some/path/foo', ('bar', 'baz')) ) def test_install_local_editable_with_extras(): @@ -175,4 +183,4 @@ def test_install_local_editable_with_extras(): res = run_pip('install', '-e', to_install + '[bar]', expect_error=False) assert env.site_packages/'easy-install.pth' in res.files_updated assert env.site_packages/'LocalExtras.egg-link' in res.files_created - assert env.site_packages/'fspkg' in res.files_created + assert env.site_packages/'simple' in res.files_created diff --git a/tests/test_show.py b/tests/test_show.py new file mode 100644 index 000000000..774fe9a7b --- /dev/null +++ b/tests/test_show.py @@ -0,0 +1,88 @@ +import re +from pip import __version__ +from pip.commands.show import search_packages_info +from tests.test_pip import reset_env, run_pip + + +def test_show(): + """ + Test end to end test for show command. + + """ + reset_env() + result = run_pip('show', 'pip') + lines = result.stdout.split('\n') + assert len(lines) == 6 + assert lines[0] == '---', lines[0] + assert lines[1] == 'Name: pip', lines[1] + assert lines[2] == 'Version: %s' % __version__, lines[2] + assert lines[3].startswith('Location: '), lines[3] + assert lines[4] == 'Requires: ' + + +def test_show_with_files_not_found(): + """ + Test for show command with installed files listing enabled and + installed-files.txt not found. + + """ + reset_env() + result = run_pip('show', '-f', 'pip') + lines = result.stdout.split('\n') + assert len(lines) == 8 + assert lines[0] == '---', lines[0] + assert lines[1] == 'Name: pip', lines[1] + assert lines[2] == 'Version: %s' % __version__, lines[2] + assert lines[3].startswith('Location: '), lines[3] + assert lines[4] == 'Requires: ' + assert lines[5] == 'Files:', lines[4] + assert lines[6] == 'Cannot locate installed-files.txt', lines[5] + + +def test_show_with_all_files(): + """ + Test listing all files in the show command. + + """ + reset_env() + result = run_pip('install', 'initools==0.2') + result = run_pip('show', '--files', 'initools') + assert re.search(r"Files:\n( .+\n)+", result.stdout) + + +def test_missing_argument(): + """ + Test show command with no arguments. + + """ + reset_env() + result = run_pip('show') + assert 'ERROR: Please provide a project name or names.' in result.stdout + + +def test_find_package_not_found(): + """ + Test trying to get info about a nonexistent package. + + """ + result = search_packages_info(['abcd3']) + assert len(list(result)) == 0 + + +def test_search_any_case(): + """ + Search for a package in any case. + + """ + result = list(search_packages_info(['PIP'])) + assert len(result) == 1 + assert 'pip' == result[0]['name'] + + +def test_more_than_one_package(): + """ + Search for more than one package. + + """ + result = list(search_packages_info(['Pip', 'Nose', 'Virtualenv'])) + assert len(result) == 3 diff --git a/tests/test_test.py b/tests/test_test.py index a52939575..498652d95 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -66,3 +66,24 @@ def test_sitecustomize_not_growing_in_fast_environment(): size2 = os.stat(sc2).st_size assert size1==size2, "size before, %d != size after, %d" %(size1, size2) + +def test_tmp_dir_exists_in_env(): + """ + Test that $TMPDIR == env.temp_path and path exists, and env.assert_no_temp() passes + """ + #need these tests to ensure the assert_no_temp feature of scripttest is working + env = reset_env(use_distribute=True) + env.assert_no_temp() #this fails if env.tmp_path doesn't exist + assert env.environ['TMPDIR'] == env.temp_path + assert isdir(env.temp_path) + + +def test_tmp_dir_exists_in_fast_env(): + """ + Test that $TMPDIR == env.temp_path and path exists and env.assert_no_temp() passes (in fast env) + """ + #need these tests to ensure the assert_no_temp feature of scripttest is working + env = reset_env() + env.assert_no_temp() #this fails if env.tmp_path doesn't exist + assert env.environ['TMPDIR'] == env.temp_path + assert isdir(env.temp_path) diff --git a/tests/test_unicode.py b/tests/test_unicode.py index d9196e750..eb926494e 100644 --- a/tests/test_unicode.py +++ b/tests/test_unicode.py @@ -20,6 +20,6 @@ def test_install_package_that_emits_unicode(): env = reset_env() to_install = os.path.abspath(os.path.join(here, 'packages', 'BrokenEmitsUTF8')) - result = run_pip('install', to_install, expect_error=True) - assert '__main__.FakeError: this package designed to fail on install' in result.stdout + result = run_pip('install', to_install, expect_error=True, expect_temp=True, quiet=True) + assert 'FakeError: this package designed to fail on install' in result.stdout assert 'UnicodeDecodeError' not in result.stdout diff --git a/tests/test_uninstall.py b/tests/test_uninstall.py index 21b111710..359d59688 100644 --- a/tests/test_uninstall.py +++ b/tests/test_uninstall.py @@ -1,7 +1,10 @@ +from __future__ import with_statement + import textwrap import sys -from os.path import join, abspath +from os.path import join, abspath, normpath from tempfile import mkdtemp +from mock import patch from tests.test_pip import here, reset_env, run_pip, assert_all_changes, write_file, pyversion from tests.local_repos import local_repo, local_checkout @@ -16,6 +19,8 @@ def test_simple_uninstall(): env = reset_env() result = run_pip('install', 'INITools==0.2') assert join(env.site_packages, 'initools') in result.files_created, sorted(result.files_created.keys()) + #the import forces the generation of __pycache__ if the version of python supports it + env.run('python', '-c', "import initools") result2 = run_pip('uninstall', 'INITools', '-y') assert_all_changes(result, result2, [env.venv/'build', 'cache']) @@ -34,6 +39,19 @@ def test_uninstall_with_scripts(): assert_all_changes(result, result2, [env.venv/'build', 'cache']) +def test_uninstall_easy_install_after_import(): + """ + Uninstall an easy_installed package after it's been imported + + """ + env = reset_env() + result = env.run('easy_install', 'INITools==0.2', expect_stderr=True) + #the import forces the generation of __pycache__ if the version of python supports it + env.run('python', '-c', "import initools") + result2 = run_pip('uninstall', 'INITools', '-y') + assert_all_changes(result, result2, [env.venv/'build', 'cache']) + + def test_uninstall_namespace_package(): """ Uninstall a distribution with a namespace package without clobbering @@ -48,6 +66,33 @@ def test_uninstall_namespace_package(): assert join(env.site_packages, 'pd', 'find') in result2.files_deleted, sorted(result2.files_deleted.keys()) +def test_uninstall_overlapping_package(): + """ + Uninstalling a distribution that adds modules to a pre-existing package + should only remove those added modules, not the rest of the existing + package. + + See: GitHub issue #355 (pip uninstall removes things it didn't install) + """ + parent_pkg = abspath(join(here, 'packages', 'parent-0.1.tar.gz')) + child_pkg = abspath(join(here, 'packages', 'child-0.1.tar.gz')) + env = reset_env() + result1 = run_pip('install', parent_pkg, expect_error=False) + assert join(env.site_packages, 'parent') in result1.files_created, sorted(result1.files_created.keys()) + result2 = run_pip('install', child_pkg, expect_error=False) + assert join(env.site_packages, 'child') in result2.files_created, sorted(result2.files_created.keys()) + assert normpath(join(env.site_packages, 'parent/plugins/child_plugin.py')) in result2.files_created, sorted(result2.files_created.keys()) + #the import forces the generation of __pycache__ if the version of python supports it + env.run('python', '-c', "import parent.plugins.child_plugin, child") + result3 = run_pip('uninstall', '-y', 'child', expect_error=False) + assert join(env.site_packages, 'child') in result3.files_deleted, sorted(result3.files_created.keys()) + assert normpath(join(env.site_packages, 'parent/plugins/child_plugin.py')) in result3.files_deleted, sorted(result3.files_deleted.keys()) + assert join(env.site_packages, 'parent') not in result3.files_deleted, sorted(result3.files_deleted.keys()) + # Additional check: uninstalling 'child' should return things to the + # previous state, without unintended side effects. + assert_all_changes(result2, result3, []) + + def test_uninstall_console_scripts(): """ Test uninstalling a package with more files (console_script entry points, extra directories). @@ -155,3 +200,42 @@ def test_uninstall_as_egg(): result2 = run_pip('uninstall', 'FSPkg', '-y', expect_error=True) assert_all_changes(result, result2, [env.venv/'build', 'cache']) + +@patch('pip.req.logger') +def test_uninstallpathset_no_paths(mock_logger): + """ + Test UninstallPathSet logs notification when there are no paths to uninstall + + """ + from pip.req import UninstallPathSet + from pkg_resources import get_distribution + test_dist = get_distribution('pip') + # ensure that the distribution is "local" + with patch("pip.req.dist_is_local") as mock_dist_is_local: + mock_dist_is_local.return_value = True + uninstall_set = UninstallPathSet(test_dist) + uninstall_set.remove() #with no files added to set + mock_logger.notify.assert_any_call("Can't uninstall 'pip'. No files were found to uninstall.") + + +@patch('pip.req.logger') +def test_uninstallpathset_non_local(mock_logger): + """ + Test UninstallPathSet logs notification and returns (with no exception) when dist is non-local + + """ + from pip.req import UninstallPathSet + from pkg_resources import get_distribution + test_dist = get_distribution('pip') + test_dist.location = "/NON_LOCAL" + # ensure that the distribution is "non-local" + # setting location isn't enough, due to egg-link file checking for + # develop-installs + with patch("pip.req.dist_is_local") as mock_dist_is_local: + mock_dist_is_local.return_value = False + uninstall_set = UninstallPathSet(test_dist) + uninstall_set.remove() #with no files added to set; which is the case when trying to remove non-local dists + mock_logger.notify.assert_any_call("Not uninstalling pip at /NON_LOCAL, outside environment %s" % sys.prefix) + + + diff --git a/tests/test_upgrade.py b/tests/test_upgrade.py index 795e68f85..d67c02865 100644 --- a/tests/test_upgrade.py +++ b/tests/test_upgrade.py @@ -49,11 +49,12 @@ def test_upgrade_with_newest_already_installed(): not be reinstalled and the user should be informed. """ + find_links = 'file://' + join(here, 'packages') env = reset_env() - run_pip('install', 'INITools') - result = run_pip('install', '--upgrade', 'INITools') - assert not result.files_created, 'pip install --upgrade INITools upgraded when it should not have' - assert 'already up-to-date' in result.stdout + run_pip('install', '-f', find_links, '--no-index', 'simple') + result = run_pip('install', '--upgrade', '-f', find_links, '--no-index', 'simple') + assert not result.files_created, 'simple upgraded when it should not have' + assert 'already up-to-date' in result.stdout, result.stdout def test_upgrade_force_reinstall_newest(): @@ -207,8 +208,8 @@ def test_upgrade_vcs_req_with_no_dists_found(): def test_upgrade_vcs_req_with_dist_found(): """It can upgrade a VCS requirement that has distributions on the index.""" reset_env() - req = "%s#egg=virtualenv" % local_checkout( - "git+git://github.com/pypa/virtualenv@c21fef2c2d53cf19f49bcc37f9c058a33fb50499") + # TODO(pnasrat) Using local_checkout fails on windows - oddness with the test path urls/git. + req = "%s#egg=virtualenv" % "git+git://github.com/pypa/virtualenv@c21fef2c2d53cf19f49bcc37f9c058a33fb50499" run_pip("install", req) result = run_pip("install", "-U", req) assert not "pypi.python.org" in result.stdout, result.stdout diff --git a/tests/test_user_site.py b/tests/test_user_site.py index 4e997692e..e4f5c73a8 100644 --- a/tests/test_user_site.py +++ b/tests/test_user_site.py @@ -6,7 +6,15 @@ import sys from os.path import abspath, join, curdir, isdir, isfile from nose import SkipTest from tests.local_repos import local_checkout -from tests.test_pip import here, reset_env, run_pip, pyversion +from tests.test_pip import here, reset_env, run_pip, pyversion, assert_all_changes + + +patch_dist_in_site_packages = """ + def dist_in_site_packages(dist): + return False + import pip + pip.util.dist_in_site_packages=dist_in_site_packages +""" def test_install_curdir_usersite_fails_in_old_python(): @@ -27,6 +35,10 @@ class Tests_UserSite: # --user only works on 2.6 or higher if sys.version_info < (2, 6): raise SkipTest() + # --user option is broken in pypy + if hasattr(sys, "pypy_version_info"): + raise SkipTest() + def test_reset_env_system_site_packages_usersite(self): """ @@ -58,9 +70,6 @@ class Tests_UserSite: """ Test installing current directory ('.') into usersite after installing distribute """ - # FIXME distutils --user option seems to be broken in pypy - if hasattr(sys, "pypy_version_info"): - raise SkipTest() env = reset_env(use_distribute=True, system_site_packages=True) result = run_pip('install', '--user', '-e', '%s#egg=initools-dev' % @@ -72,9 +81,6 @@ class Tests_UserSite: """ Test installing current directory ('.') into usersite """ - # FIXME distutils --user option seems to be broken in pypy - if hasattr(sys, "pypy_version_info"): - raise SkipTest() env = reset_env(use_distribute=True, system_site_packages=True) run_from = abspath(join(here, 'packages', 'FSPkg')) result = run_pip('install', '--user', curdir, cwd=run_from, expect_error=False) @@ -111,15 +117,21 @@ class Tests_UserSite: assert not isfile(initools_v3_file), initools_v3_file - def test_install_user_conflict_in_site(self): + def test_install_user_conflict_in_globalsite(self): """ - Test user install with conflict in site ignores site and installs to usersite + Test user install with conflict in global site ignores site and installs to usersite """ - #the test framework only supports testing using virtualenvs - #this test will use a --system_site_packages virtualenv to achieve the conflict scenario. + # the test framework only supports testing using virtualenvs + # the sys.path ordering for virtualenvs with --system-site-packages is this: virtualenv-site, user-site, global-site + # this test will use 2 modifications to simulate the user-site/global-site relationship + # 1) a monkey patch which will make it appear INITools==0.2 is not in in the virtualenv site + # if we don't patch this, pip will return an installation error: "Will not install to the usersite because it will lack sys.path precedence..." + # 2) adding usersite to PYTHONPATH, so usersite as sys.path precedence over the virtualenv site + + env = reset_env(system_site_packages=True, sitecustomize=patch_dist_in_site_packages) + env.environ["PYTHONPATH"] = env.root_path / env.user_site - env = reset_env(system_site_packages=True) result1 = run_pip('install', 'INITools==0.2') result2 = run_pip('install', '--user', 'INITools==0.1') @@ -136,19 +148,50 @@ class Tests_UserSite: assert isdir(initools_folder) + def test_upgrade_user_conflict_in_globalsite(self): + """ + Test user install/upgrade with conflict in global site ignores site and installs to usersite + """ + + # the test framework only supports testing using virtualenvs + # the sys.path ordering for virtualenvs with --system-site-packages is this: virtualenv-site, user-site, global-site + # this test will use 2 modifications to simulate the user-site/global-site relationship + # 1) a monkey patch which will make it appear INITools==0.2 is not in in the virtualenv site + # if we don't patch this, pip will return an installation error: "Will not install to the usersite because it will lack sys.path precedence..." + # 2) adding usersite to PYTHONPATH, so usersite as sys.path precedence over the virtualenv site + + env = reset_env(system_site_packages=True, sitecustomize=patch_dist_in_site_packages) + env.environ["PYTHONPATH"] = env.root_path / env.user_site + + result1 = run_pip('install', 'INITools==0.2') + result2 = run_pip('install', '--user', '--upgrade', 'INITools') + + #usersite has 0.3.1 + egg_info_folder = env.user_site / 'INITools-0.3.1-py%s.egg-info' % pyversion + initools_folder = env.user_site / 'initools' + assert egg_info_folder in result2.files_created, str(result2) + assert initools_folder in result2.files_created, str(result2) + + #site still has 0.2 (can't look in result1; have to check) + egg_info_folder = env.root_path / env.site_packages / 'INITools-0.2-py%s.egg-info' % pyversion + initools_folder = env.root_path / env.site_packages / 'initools' + assert isdir(egg_info_folder), result2.stdout + assert isdir(initools_folder) + + def test_install_user_conflict_in_globalsite_and_usersite(self): """ Test user install with conflict in globalsite and usersite ignores global site and updates usersite. """ - #the test framework only supports testing using virtualenvs - #this test will use a --system_site_packages virtualenv to achieve the conflict scenario. + # the test framework only supports testing using virtualenvs. + # the sys.path ordering for virtualenvs with --system-site-packages is this: virtualenv-site, user-site, global-site. + # this test will use 2 modifications to simulate the user-site/global-site relationship + # 1) a monkey patch which will make it appear INITools==0.2 is not in in the virtualenv site + # if we don't patch this, pip will return an installation error: "Will not install to the usersite because it will lack sys.path precedence..." + # 2) adding usersite to PYTHONPATH, so usersite as sys.path precedence over the virtualenv site - env = reset_env(system_site_packages=True) - - # the sys.path ordering for virtualenvs with --system-site-packages is this: virtualenv site, usersite, global site - # given this ordering you *can't* use it to simulate the scenario for this test. - # this test will add the usersite to PYTHONPATH to simulate the desired ordering + env = reset_env(system_site_packages=True, sitecustomize=patch_dist_in_site_packages) env.environ["PYTHONPATH"] = env.root_path / env.user_site result1 = run_pip('install', 'INITools==0.2') @@ -166,3 +209,48 @@ class Tests_UserSite: initools_folder = env.root_path / env.site_packages / 'initools' assert isdir(egg_info_folder) assert isdir(initools_folder) + + + def test_install_user_in_global_virtualenv_with_conflict_fails(self): + """ + Test user install in --system-site-packages virtualenv with conflict in site fails. + """ + env = reset_env(system_site_packages=True) + result1 = run_pip('install', 'INITools==0.2') + result2 = run_pip('install', '--user', 'INITools==0.1', expect_error=True) + resultp = env.run('python', '-c', "import pkg_resources; print(pkg_resources.get_distribution('initools').location)") + dist_location = resultp.stdout.strip() + assert result2.stdout.startswith("Will not install to the user site because it will lack sys.path precedence to %s in %s" + %('INITools', dist_location)), result2.stdout + + + def test_uninstall_from_usersite(self): + """ + Test uninstall from usersite + """ + env = reset_env(system_site_packages=True) + result1 = run_pip('install', '--user', 'INITools==0.3') + result2 = run_pip('uninstall', '-y', 'INITools') + assert_all_changes(result1, result2, [env.venv/'build', 'cache']) + + + def test_uninstall_editable_from_usersite(self): + """ + Test uninstall editable local user install + """ + env = reset_env(use_distribute=True, system_site_packages=True) + + #install + to_install = abspath(join(here, 'packages', 'FSPkg')) + result1 = run_pip('install', '--user', '-e', to_install, expect_error=False) + egg_link = env.user_site/'FSPkg.egg-link' + assert egg_link in result1.files_created, str(result1.stdout) + + #uninstall + result2 = run_pip('uninstall', '-y', 'FSPkg') + assert not isfile(env.root_path / egg_link) + + assert_all_changes(result1, result2, + [env.venv/'build', 'cache', env.user_site/'easy-install.pth']) + + diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 000000000..305591b37 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,148 @@ +""" +util tests + +""" +import os +import pkg_resources +from mock import Mock +from nose.tools import eq_ +from tests.path import Path +from pip.util import egg_link_path +from pip.util import Inf + + +class Tests_EgglinkPath: + "util.egg_link_path() tests" + + def setup(self): + + project = 'foo' + + self.mock_dist = Mock(project_name=project) + self.site_packages = 'SITE_PACKAGES' + self.user_site = 'USER_SITE' + self.user_site_egglink = os.path.join(self.user_site,'%s.egg-link' % project) + self.site_packages_egglink = os.path.join(self.site_packages,'%s.egg-link' % project) + + #patches + from pip import util + self.old_site_packages = util.site_packages + self.mock_site_packages = util.site_packages = 'SITE_PACKAGES' + self.old_running_under_virtualenv = util.running_under_virtualenv + self.mock_running_under_virtualenv = util.running_under_virtualenv = Mock() + self.old_virtualenv_no_global = util.virtualenv_no_global + self.mock_virtualenv_no_global = util.virtualenv_no_global = Mock() + self.old_user_site = util.user_site + self.mock_user_site = util.user_site = self.user_site + from os import path + self.old_isfile = path.isfile + self.mock_isfile = path.isfile = Mock() + + + def teardown(self): + from pip import util + util.site_packages = self.old_site_packages + util.running_under_virtualenv = self.old_running_under_virtualenv + util.virtualenv_no_global = self.old_virtualenv_no_global + util.user_site = self.old_user_site + from os import path + path.isfile = self.old_isfile + + + def eggLinkInUserSite(self,egglink): + return egglink==self.user_site_egglink + + def eggLinkInSitePackages(self,egglink): + return egglink==self.site_packages_egglink + + ######################### + ## egglink in usersite ## + ######################### + def test_egglink_in_usersite_notvenv(self): + self.mock_virtualenv_no_global.return_value = False + self.mock_running_under_virtualenv.return_value = False + self.mock_isfile.side_effect = self.eggLinkInUserSite + eq_(egg_link_path(self.mock_dist), self.user_site_egglink) + + def test_egglink_in_usersite_venv_noglobal(self): + self.mock_virtualenv_no_global.return_value = True + self.mock_running_under_virtualenv.return_value = True + self.mock_isfile.side_effect = self.eggLinkInUserSite + eq_(egg_link_path(self.mock_dist), None) + + def test_egglink_in_usersite_venv_global(self): + self.mock_virtualenv_no_global.return_value = False + self.mock_running_under_virtualenv.return_value = True + self.mock_isfile.side_effect = self.eggLinkInUserSite + eq_(egg_link_path(self.mock_dist), self.user_site_egglink) + + ######################### + ## egglink in sitepkgs ## + ######################### + def test_egglink_in_sitepkgs_notvenv(self): + self.mock_virtualenv_no_global.return_value = False + self.mock_running_under_virtualenv.return_value = False + self.mock_isfile.side_effect = self.eggLinkInSitePackages + eq_(egg_link_path(self.mock_dist), self.site_packages_egglink) + + def test_egglink_in_sitepkgs_venv_noglobal(self): + self.mock_virtualenv_no_global.return_value = True + self.mock_running_under_virtualenv.return_value = True + self.mock_isfile.side_effect = self.eggLinkInSitePackages + eq_(egg_link_path(self.mock_dist), self.site_packages_egglink) + + def test_egglink_in_sitepkgs_venv_global(self): + self.mock_virtualenv_no_global.return_value = False + self.mock_running_under_virtualenv.return_value = True + self.mock_isfile.side_effect = self.eggLinkInSitePackages + eq_(egg_link_path(self.mock_dist), self.site_packages_egglink) + + #################################### + ## egglink in usersite & sitepkgs ## + #################################### + def test_egglink_in_both_notvenv(self): + self.mock_virtualenv_no_global.return_value = False + self.mock_running_under_virtualenv.return_value = False + self.mock_isfile.return_value = True + eq_(egg_link_path(self.mock_dist), self.user_site_egglink) + + def test_egglink_in_both_venv_noglobal(self): + self.mock_virtualenv_no_global.return_value = True + self.mock_running_under_virtualenv.return_value = True + self.mock_isfile.return_value = True + eq_(egg_link_path(self.mock_dist), self.site_packages_egglink) + + def test_egglink_in_both_venv_global(self): + self.mock_virtualenv_no_global.return_value = False + self.mock_running_under_virtualenv.return_value = True + self.mock_isfile.return_value = True + eq_(egg_link_path(self.mock_dist), self.site_packages_egglink) + + ################ + ## no egglink ## + ################ + def test_noegglink_in_sitepkgs_notvenv(self): + self.mock_virtualenv_no_global.return_value = False + self.mock_running_under_virtualenv.return_value = False + self.mock_isfile.return_value = False + eq_(egg_link_path(self.mock_dist), None) + + def test_noegglink_in_sitepkgs_venv_noglobal(self): + self.mock_virtualenv_no_global.return_value = True + self.mock_running_under_virtualenv.return_value = True + self.mock_isfile.return_value = False + eq_(egg_link_path(self.mock_dist), None) + + def test_noegglink_in_sitepkgs_venv_global(self): + self.mock_virtualenv_no_global.return_value = False + self.mock_running_under_virtualenv.return_value = True + self.mock_isfile.return_value = False + eq_(egg_link_path(self.mock_dist), None) + +def test_Inf_greater(): + """Test Inf compares greater.""" + assert Inf > object() + +def test_Inf_equals_Inf(): + """Test Inf compares greater.""" + assert Inf == Inf diff --git a/tests/test_vcs_git.py b/tests/test_vcs_git.py index 206e80512..b27c12d8b 100644 --- a/tests/test_vcs_git.py +++ b/tests/test_vcs_git.py @@ -1,4 +1,6 @@ +import sys from mock import patch +from nose import SkipTest from pip.vcs.git import Git from tests.test_pip import (reset_env, run_pip, _create_test_package,) @@ -87,6 +89,9 @@ def test_check_submodule_addition(): Submodules are pulled in on install and updated on upgrade. """ + # TODO(pnasrat) fix all helpers to do right things with paths on windows. + if sys.platform == 'win32': + raise SkipTest() env = reset_env() module_path, submodule_path = _create_test_package_with_submodule(env) diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..731f8a691 --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +envlist = + py25,py26,py27,py32,py33,pypy + +[testenv] +commands = + python setup.py dev + python setup.py nosetests