1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00
This commit is contained in:
Carl Meyer 2009-09-04 15:15:40 -04:00
commit 3d97d94a3a
5 changed files with 300 additions and 12 deletions

View file

@ -41,8 +41,7 @@ pip is meant to improve on easy_install. Some of the improvements:
* Native support for other version control systems (Git, Mercurial and Bazaar)
* Maybe uninstallation will get added. (It might get added to easy_install,
but I think the chance for pip is higher.)
* Uninstallation of packages.
Also, pip will eventually be merged directly with pip, making it
simple to define fixed sets of requirements and reliably reproduce a set of

View file

@ -33,6 +33,8 @@ tip
* Added ``--no-index`` option to ignore the package index (PyPI) temporarily
* Added uninstall support.
0.4
---

208
pip.py Normal file → Executable file
View file

@ -34,6 +34,7 @@ import httplib
import time
import logging
import ConfigParser
from collections import defaultdict
class InstallationError(Exception):
"""General exception during installation"""
@ -55,6 +56,12 @@ else:
base_src_prefix = os.path.join(os.getcwd(), 'src')
base_download_prefix = os.path.join(os.getcwd(), 'download')
# FIXME doesn't account for venv linked to global site-packages
if sys.platform == 'win32':
lib_py = os.path.join(sys.prefix, 'Lib')
else:
lib_py = os.path.join(sys.prefix, 'lib', 'python%s' % sys.version[:3])
pypi_url = "http://pypi.python.org/simple"
default_timeout = 15
@ -470,6 +477,41 @@ class InstallCommand(Command):
InstallCommand()
class UninstallCommand(Command):
name = 'uninstall'
usage = '%prog [OPTIONS] PACKAGE_NAMES ...'
summary = 'Uninstall packages'
def __init__(self):
super(UninstallCommand, self).__init__()
self.parser.add_option(
'-r', '--requirement',
dest='requirements',
action='append',
default=[],
metavar='FILENAME',
help='Uninstall all the packages listed in the given requirements file. '
'This option can be used multiple times.')
self.parser.add_option(
'-y', '--yes',
dest='yes',
action='store_true',
help="Don't ask for confirmation of uninstall deletions. "
"If this breaks your system, you get to keep the pieces.")
def run(self, options, args):
requirement_set = RequirementSet(
build_dir=None,
src_dir=None)
for name in args:
requirement_set.add_requirement(
InstallRequirement.from_line(name))
for filename in options.requirements:
for req in parse_requirements(filename):
requirement_set.add_requirement(req)
requirement_set.uninstall(auto_confirm=options.yes)
UninstallCommand()
class BundleCommand(InstallCommand):
name = 'bundle'
@ -1642,6 +1684,94 @@ execfile(__file__)
'Unexpected version control type (in %s): %s'
% (self.url, vc_type))
def uninstall(self, auto_confirm=False):
"""
Uninstall the distribution currently satisfying this requirement.
Prompts before removing or modifying files unless
``auto_confirm`` is True.
Refuses to delete or modify files outside of ``sys.prefix`` -
thus uninstallation within a virtual environment can only
modify that virtual environment, even if the virtualenv is
linked to global site-packages.
"""
assert self.check_if_exists(), "Cannot uninstall requirement %s, not installed" % (self.name,)
dist = self.satisfied_by
paths_to_remove = set()
entries_to_remove = defaultdict(set)
logger.notify('Uninstalling %s' % self.name)
logger.indent += 2
try:
pip_egg_info_path = os.path.join(dist.location,
dist.egg_name()) + '.egg-info'
easy_install_egg = dist.egg_name() + '.egg'
# FIXME this won't find a globally-installed develop egg
# if we're in a virtualenv (lib_py is based on
# sys.prefix). (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).
# That's OK, because we restrict ourselves to making
# changes within sys.prefix anyway.
develop_egg_link = os.path.join(lib_py, 'site-packages',
dist.project_name) + '.egg-link'
if os.path.exists(pip_egg_info_path):
# package installed by pip
paths_to_remove.add(pip_egg_info_path)
if dist.has_metadata('installed-files.txt'):
for installed_file in dist.get_metadata('installed-files.txt').splitlines():
path = os.path.normpath(os.path.join(pip_egg_info_path, installed_file))
if os.path.exists(path):
paths_to_remove.add(path)
if dist.has_metadata('top_level.txt'):
for top_level_pkg in [p for p
in dist.get_metadata('top_level.txt').splitlines()
if p]:
path = os.path.join(dist.location, top_level_pkg)
if os.path.exists(path):
paths_to_remove.add(path)
elif os.path.exists(path + '.py'):
paths_to_remove.add(path + '.py')
if os.path.exists(path + '.pyc'):
paths_to_remove.add(path + '.pyc')
elif dist.location.endswith(easy_install_egg):
# package installed by easy_install
paths_to_remove.add(dist.location)
easy_install_pth = os.path.join(os.path.dirname(dist.location),
'easy-install.pth')
entries_to_remove[easy_install_pth].add('./' + easy_install_egg)
elif os.path.isfile(develop_egg_link):
# develop egg
fh = open(develop_egg_link, 'r')
link_pointer = fh.readline().strip()
fh.close()
assert (link_pointer == dist.location), 'Egg-link %s does not match installed location of %s (at %s)' % (link_pointer, self.name, dist.location)
paths_to_remove.add(develop_egg_link)
easy_install_pth = os.path.join(os.path.dirname(develop_egg_link),
'easy-install.pth')
entries_to_remove[easy_install_pth].add(dist.location)
for filename, entries in entries_to_remove.items():
if strip_sys_prefix(filename) is None:
logger.notify('Will not modify %s, outside local environment.' % filename)
continue
remove_entries_from_file(filename, entries, auto_confirm)
to_remove = set()
for path in paths_to_remove:
if strip_sys_prefix(path) is None:
logger.notify('Will not remove %s, outside of local environment.' % path)
else:
to_remove.add(path)
if to_remove:
remove_paths(to_remove, auto_confirm)
finally:
logger.indent -= 2
def archive(self, build_dir):
assert self.source_dir
archive_name = '%s-%s.zip' % (self.name, self.installed_version)
@ -1675,12 +1805,6 @@ execfile(__file__)
if self.editable:
self.install_editable()
return
## FIXME: this is not a useful record:
## Also a bad location
if sys.platform == 'win32':
install_location = os.path.join(sys.prefix, 'Lib')
else:
install_location = os.path.join(sys.prefix, 'lib', 'python%s' % sys.version[:3])
temp_location = tempfile.mkdtemp('-record', 'pip-')
record_filename = os.path.join(temp_location, 'install-record.txt')
## FIXME: I'm not sure if this is a reasonable location; probably not
@ -1896,6 +2020,10 @@ class RequirementSet(object):
return self.requirements[self.requirement_aliases[name]]
raise KeyError("No project with the name %r" % project_name)
def uninstall(self, auto_confirm=False):
for req in self.requirements.values():
req.uninstall(auto_confirm=auto_confirm)
def install_files(self, finder, force_root_egg_info=False, only_download=False):
unnamed = list(self.unnamed_requirements)
reqs = self.requirements.values()
@ -2134,7 +2262,6 @@ class RequirementSet(object):
and is_svn_page(file_contents(filename))):
# We don't really care about this
Subversion('svn+' + link.url).unpack(location)
else:
## FIXME: handle?
## FIXME: magic signatures?
@ -3561,7 +3688,7 @@ def get_file_content(url, comes_from=None):
f.close()
return url, content
def parse_requirements(filename, finder, comes_from=None):
def parse_requirements(filename, finder=None, comes_from=None):
skip_match = None
if os.environ.get('PIP_SKIP_REQUIREMENTS_REGEX'):
skip_match = re.compile(os.environ['PIP_SKIP_REQUIREMENTS_REGEX'])
@ -3589,7 +3716,7 @@ def parse_requirements(filename, finder, comes_from=None):
# No longer used, but previously these were used in
# requirement files, so we'll ignore.
pass
elif line.startswith('-f') or line.startswith('--find-links'):
elif finder and line.startswith('-f') or line.startswith('--find-links'):
if line.startswith('-f'):
line = line[2:].strip()
else:
@ -4105,6 +4232,69 @@ def package_to_requirement(package_name):
else:
return name
def strip_sys_prefix(path):
""" If ``path`` begins with sys.prefix, return ``path`` with
sys.prefix stripped off. Otherwise return None."""
sys_prefix = os.path.realpath(sys.prefix)
if path.startswith(sys_prefix):
return path.replace(sys_prefix, '')
return None
def remove_entries_from_file(filename, entries, auto_confirm=True):
"""Remove ``entries`` from text file ``filename``, with
confirmation (unless ``auto_confirm`` is True)."""
assert os.path.isfile(filename), "Cannot remove entries from nonexistent file %s" % filename
logger.notify('Removing entries from %s:' % filename)
logger.indent += 2
try:
if auto_confirm:
response = 'y'
else:
for entry in entries:
logger.notify(entry)
response = ask('Proceed with removal (y/n)? ', ('y', 'n'))
if response == 'y':
fh = open(filename, 'r')
lines = fh.readlines()
fh.close()
try:
for entry in entries:
logger.notify('Removing entry: %s' % entry)
try:
lines.remove(entry + '\n')
except ValueError:
pass
finally:
pass
fh = open(filename, 'w')
fh.writelines(lines)
fh.close()
finally:
logger.indent -= 2
def remove_paths(paths, auto_confirm=False):
"""Remove paths in iterable ``paths`` with confirmation
(unless ``auto_confirm`` is True)."""
logger.notify('Within environment %s, removing:' % os.path.realpath(sys.prefix))
logger.indent += 2
try:
if auto_confirm:
response = 'y'
else:
for path in sorted(paths):
logger.notify(strip_sys_prefix(path))
response = ask('Proceed with removal (y/n)? ', ('y', 'n'))
if response == 'y':
for path in sorted(paths):
if os.path.isdir(path):
logger.notify('Removing directory %s' % strip_sys_prefix(path))
shutil.rmtree(path)
elif os.path.isfile(path):
logger.notify('Removing file %s' % strip_sys_prefix(path))
os.remove(path)
finally:
logger.indent -= 2
def splitext(path):
"""Like os.path.splitext, but take off .tar too"""
base, ext = posixpath.splitext(path)

View file

@ -40,6 +40,46 @@ def write_file(filename, text):
def get_env():
return env
# FIXME ScriptTest does something similar, but only within a single
# ProcResult; this generalizes it so states can be compared across
# multiple commands. Maybe should be rolled into ScriptTest?
def diff_states(start, end, ignore=None):
"""
Differences two "filesystem states" as represented by dictionaries
of FoundFile and FoundDir objects.
Returns a dictionary with following keys:
``deleted``
Dictionary of files/directories found only in the start state.
``created``
Dictionary of files/directories found only in the end state.
``updated``
Dictionary of files whose size has changed (FIXME not entirely
reliable, but comparing contents is not possible because
FoundFile.bytes is lazy, and comparing mtime doesn't help if
we want to know if a file has been returned to its earlier
state).
Ignores mtime and other file attributes; only presence/absence and
size are considered.
"""
ignore = ignore or []
start_keys = set([k for k in start.keys()
if not [k.startswith(i) for i in ignore]])
end_keys = set([k for k in end.keys()
if not [k.startswith(i) for i in ignore]])
deleted = dict([(k, start[k]) for k in start_keys.difference(end_keys)])
created = dict([(k, end[k]) for k in end_keys.difference(start_keys)])
updated = {}
for k in start_keys.intersection(end_keys):
if (start[k].size != end[k].size):
updated[k] = end[k]
return dict(deleted=deleted, created=created, updated=updated)
import optparse
parser = optparse.OptionParser(usage='%prog [OPTIONS] [TEST_FILE...]')
parser.add_option('--first', action='store_true',
@ -56,7 +96,7 @@ def main():
options, args = parser.parse_args()
reset_env()
if not args:
args = ['test_basic.txt', 'test_requirements.txt', 'test_freeze.txt', 'test_proxy.txt']
args = ['test_basic.txt', 'test_requirements.txt', 'test_freeze.txt', 'test_proxy.txt', 'test_uninstall.txt']
optionflags = doctest.ELLIPSIS
if options.first:
optionflags |= doctest.REPORT_ONLY_FIRST_FAILURE

57
tests/test_uninstall.txt Normal file
View file

@ -0,0 +1,57 @@
Basic setup::
>>> from __main__ import here, reset_env, run_pip, pyversion, lib_py, get_env, diff_states, write_file
>>> from os.path import join
>>> easy_install_pth = join(lib_py, 'site-packages', 'easy-install.pth')
Simple install and uninstall::
>>> reset_env()
>>> result = run_pip('install', 'INITools==0.2', expect_error=True)
>>> assert join(lib_py + 'site-packages', 'initools') in result.files_created, sorted(result.files_created.keys())
>>> result2 = run_pip('uninstall', 'INITools', '-y', expect_error=True)
>>> diff_states(result.files_before, result2.files_after, ignore=['build']).values()
[{}, {}, {}]
Uninstall an easy_installed package::
>>> reset_env()
>>> env = get_env()
>>> result = env.run(join(env.base_path, 'bin', 'easy_install'), 'INITools')
>>> assert('INITools' in result.files_updated[easy_install_pth].bytes), result.files_after[easy-install_pth].bytes
>>> result2 = run_pip('uninstall', 'INITools', '-y', expect_error=True)
>>> diff_states(result.files_before, result2.files_after, ignore=['build']).values()
[{}, {}, {}]
Uninstall a package with more files (script entry points, etc)::
>>> reset_env()
>>> result = run_pip('install', 'virtualenv', expect_error=True)
>>> assert ('bin/virtualenv') in result.files_created, sorted(result.files_created.keys())
>>> result2 = run_pip('uninstall', 'virtualenv', '-y', expect_error=True)
>>> diff_states(result.files_before, result2.files_after,
... ignore=['build', lib_py + 'site-packages/support-files']).values()
[{}, {}, {}]
Uninstall an editable installation from svn::
>>> reset_env()
>>> result = run_pip('install', '-e', 'svn+http://svn.colorstudy.com/INITools/trunk#egg=initools-dev', expect_error=True)
>>> egg_link = result.files_created[lib_py + 'site-packages/INITools.egg-link']
>>> result2 = run_pip('uninstall', '-y', 'initools', expect_error=True)
>>> assert ('src/initools' in result2.files_after), 'oh noes, pip deleted my sources!'
>>> diff_states(result.files_before, result2.files_after, ignore=['src/initools']).values()
[{}, {}, {}]
Uninstall from a requirements file::
>>> reset_env()
>>> write_file('test-req.txt', '''\
... -e svn+http://svn.colorstudy.com/INITools/trunk#egg=initools-dev
... # and something else to test out:
... simplejson<=1.7.4
... ''')
>>> result = run_pip('install', '-r', 'test-req.txt')
>>> result2 = run_pip('uninstall', '-r', 'test-req.txt', '-y')
>>> diff_states(result.files_before, result2.files_after, ignore=['build']).values()
[{}, {}, {}]