summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/conf.py2
-rw-r--r--docs/how-to-contribute.txt33
-rw-r--r--docs/news.txt22
-rw-r--r--docs/requirement-format.txt12
-rw-r--r--pip/basecommand.py4
-rw-r--r--pip/commands/search.py2
-rw-r--r--pip/download.py23
-rw-r--r--pip/index.py16
-rw-r--r--pip/req.py47
-rw-r--r--pip/util.py15
-rw-r--r--pip/vcs/__init__.py5
-rw-r--r--pip/vcs/subversion.py2
-rw-r--r--setup.py6
-rw-r--r--tests/test_basic.py71
-rw-r--r--tests/test_requirements.py18
-rw-r--r--tests/test_search.py11
16 files changed, 231 insertions, 58 deletions
diff --git a/docs/conf.py b/docs/conf.py
index b0ed2b6c6..b9acdb90e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -47,7 +47,7 @@ copyright = '2008-2011, The pip developers'
# built documents.
#
# The short X.Y version.
-release = "1.0"
+release = "1.0.1"
version = '.'.join(release.split('.')[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
diff --git a/docs/how-to-contribute.txt b/docs/how-to-contribute.txt
index 23c16808a..be5beb638 100644
--- a/docs/how-to-contribute.txt
+++ b/docs/how-to-contribute.txt
@@ -7,6 +7,14 @@ All kinds of contributions are welcome - code, tests, documentation,
bug reports, ideas, etc.
+Release Schedule
+================
+
+Minor releases of pip (e.g. 1.1, 1.2, 1.3...) occur every four months
+(beginning with the release of pip 1.0 on April 4, 2011). Two weeks before a
+scheduled release, a new branch ``release/X.Y`` is created for release testing
+and preparation. This branch is only open to bugfixes.
+
.. _contributing-with-code:
Contributing with Code
@@ -23,27 +31,44 @@ Log in to Github, go to the `pip repository page
to copy the repository and then clone your fork, like::
$ git clone https://github.com/YOU_USER_NAME/pip
-
+
Now you can change whatever you want, commit, push to your fork and when your
contribution is done, follow the **pull request** link and send us a request
explaining what you did and why.
+Branches
+--------
+
+Pip uses the `git-flow`_ branching model. The default branch on GitHub is
+``develop``, and all development work (new features and bugfixes) should happen
+in that branch. The ``master`` branch is stable, and reflects the last released
+state.
+
+.. _git-flow: http://nvie.com/posts/a-successful-git-branching-model/
All tests should pass
---------------------
Almost all changes to pip should be accompanied by automated tests -
especially ones adding new behavior.
+
`Nose`_ is used to find and run all tests. Take a look at :doc:`running-tests`
to see what you need and how you should run the tests.
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
+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.
-Using a Continuous Integration server
--------------------------------------
+Continuous Integration server
+-----------------------------
-We have a continuous integration server running all pip related tests at
+We have a continuous integration server running all pip related tests at
http://ci.cloudsilverlining.org/view/pip. But if you want to have your own,
you can learn how to set up a Hudson CI server like that in the
:doc:`ci-server-step-by-step` page.
diff --git a/docs/news.txt b/docs/news.txt
index 1372d6b4b..ecc7a8ad3 100644
--- a/docs/news.txt
+++ b/docs/news.txt
@@ -1,8 +1,24 @@
News / Changelog
================
-1.0
----
+Next release (1.1) schedule
+---------------------------
+
+Beta release mid-July 2011, final release early August.
+
+1.0.1 (2011-04-30)
+------------------
+
+* Start to use git-flow.
+* Fixed issue #274 - `find_command` should not raise AttributeError
+* Fixed issue #273 - respect Content-Disposition header. Thanks Bradley Ayers.
+* Fixed issue #233 - pathext handling on Windows.
+* Fixed issue #252 - svn+svn protocol.
+* Fixed issue #44 - multiple CLI searches.
+* Fixed issue #266 - current working directory when running setup.py clean.
+
+1.0 (2011-04-04)
+----------------
* Added Python 3 support! Huge thanks to Vinay Sajip, Vitaly Babiy, Kelsey
Hightower, and Alex Gronholm, among others.
@@ -16,7 +32,7 @@ News / Changelog
python-setuptools package (workaround until fixed in Debian and Ubuntu).
* Added `get-pip.py <https://github.com/pypa/pip/raw/master/contrib/get-pip.py>`_
- installer. Simple download and execute it, using the Python interpreter of
+ installer. Simply download and execute it, using the Python interpreter of
your choice::
$ curl -O https://github.com/pypa/pip/raw/master/contrib/get-pip.py
diff --git a/docs/requirement-format.txt b/docs/requirement-format.txt
index b2071c3e2..d326ea63e 100644
--- a/docs/requirement-format.txt
+++ b/docs/requirement-format.txt
@@ -24,6 +24,15 @@ The ``#egg=MyProject`` part is important, because while you can
install simply given the svn location, the project name is useful in
other places.
+You can also specify the egg name for a non-editable url. This is useful to
+point to HEAD locations on the local filesystem:
+
+ file:///path/to/your/lib/project#egg=MyProject
+
+or relative paths:
+
+ file:../../lib/project#egg=MyProject
+
If you need to give pip (and by association easy_install) hints
about where to find a package, you can use the ``-f``
(``--find-links``) option, like::
@@ -48,9 +57,10 @@ Right now pip knows of the following major version control systems:
Subversion
~~~~~~~~~~
-Pip supports the URL schemes ``svn``, ``svn+http``, ``svn+https``, ``svn+ssh``.
+Pip supports the URL schemes ``svn``, ``svn+svn``, ``svn+http``, ``svn+https``, ``svn+ssh``.
You can also give specific revisions to an SVN URL, like::
+ -e svn+svn://svn.myproject.org/svn/MyProject#egg=MyProject
-e svn+http://svn.myproject.org/svn/MyProject/trunk@2019#egg=MyProject
which will check out revision 2019. ``@{20080101}`` would also check
diff --git a/pip/basecommand.py b/pip/basecommand.py
index 956dc9fa0..153ee52be 100644
--- a/pip/basecommand.py
+++ b/pip/basecommand.py
@@ -134,6 +134,10 @@ class Command(object):
logger.fatal(str(e))
logger.info('Exception information:\n%s' % format_exc())
exit = 1
+ except KeyboardInterrupt:
+ logger.fatal('Operation cancelled by user')
+ logger.info('Exception information:\n%s' % format_exc())
+ exit = 1
except:
logger.fatal('Exception:\n%s' % format_exc())
exit = 2
diff --git a/pip/commands/search.py b/pip/commands/search.py
index 561f0df13..1a6bf9c52 100644
--- a/pip/commands/search.py
+++ b/pip/commands/search.py
@@ -27,7 +27,7 @@ class SearchCommand(Command):
if not args:
logger.warn('ERROR: Missing required argument (search query).')
return
- query = ' '.join(args)
+ query = args
index_url = options.index
pypi_hits = self.search(query, index_url)
diff --git a/pip/download.py b/pip/download.py
index 969a88508..6fe73eaf8 100644
--- a/pip/download.py
+++ b/pip/download.py
@@ -1,9 +1,10 @@
-import re
+import cgi
import getpass
-import sys
-import os
import mimetypes
+import os
+import re
import shutil
+import sys
import tempfile
from pip.backwardcompat import (md5, copytree, xmlrpclib, urllib, urllib2,
urlparse, string_types, HTTPError)
@@ -330,7 +331,7 @@ def _check_md5(download_hash, link):
def _get_md5_from_file(target_file, link):
download_hash = md5()
fp = open(target_file, 'rb')
- while 1:
+ while True:
chunk = fp.read(4096)
if not chunk:
break
@@ -362,7 +363,7 @@ def _download_url(resp, link, temp_location):
logger.notify('Downloading %s' % show_url)
logger.debug('Downloading from URL %s' % link)
- while 1:
+ while True:
chunk = resp.read(4096)
if not chunk:
break
@@ -416,7 +417,7 @@ def unpack_http_url(link, location, download_cache, only_download):
create_download_cache_folder(download_cache)
if (target_file
and os.path.exists(target_file)
- and os.path.exists(target_file+'.content-type')):
+ and os.path.exists(target_file + '.content-type')):
fp = open(target_file+'.content-type')
content_type = fp.read().strip()
fp.close()
@@ -427,7 +428,14 @@ def unpack_http_url(link, location, download_cache, only_download):
else:
resp = _get_response_from_url(target_url, link)
content_type = resp.info()['content-type']
- filename = link.filename
+ filename = link.filename # fallback
+ # Have a look at the Content-Disposition header for a better guess
+ content_disposition = resp.info().get('content-disposition')
+ if content_disposition:
+ type, params = cgi.parse_header(content_disposition)
+ # We use ``or`` here because we don't want to use an "empty" value
+ # from the filename param.
+ filename = params.get('filename') or filename
ext = splitext(filename)[1]
if not ext:
ext = mimetypes.guess_extension(content_type)
@@ -466,6 +474,7 @@ def _get_response_from_url(target_url, link):
raise
return resp
+
class Urllib2HeadRequest(urllib2.Request):
def get_method(self):
return "HEAD"
diff --git a/pip/index.py b/pip/index.py
index 1325387c1..1b8a52b4f 100644
--- a/pip/index.py
+++ b/pip/index.py
@@ -578,13 +578,9 @@ class Link(object):
@property
def filename(self):
- url = self.url
- url = url.split('#', 1)[0]
- url = url.split('?', 1)[0]
- url = url.rstrip('/')
+ url = self.url_fragment
name = posixpath.basename(url)
- assert name, (
- 'URL %r produced no filename' % url)
+ assert name, ('URL %r produced no filename' % url)
return name
@property
@@ -598,6 +594,14 @@ class Link(object):
def splitext(self):
return splitext(posixpath.basename(self.path.rstrip('/')))
+ @property
+ def url_fragment(self):
+ url = self.url
+ url = url.split('#', 1)[0]
+ url = url.split('?', 1)[0]
+ url = url.rstrip('/')
+ return url
+
_egg_fragment_re = re.compile(r'#egg=([^&]*)')
@property
diff --git a/pip/req.py b/pip/req.py
index 6e520c5f0..9a7cd2d24 100644
--- a/pip/req.py
+++ b/pip/req.py
@@ -73,28 +73,34 @@ class InstallRequirement(object):
"""
url = None
name = name.strip()
- req = name
+ req = None
path = os.path.normpath(os.path.abspath(name))
+ link = None
if is_url(name):
- url = name
- ## FIXME: I think getting the requirement here is a bad idea:
- #req = get_requirement_from_url(url)
- req = None
+ 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)
- url = path_to_url(name)
- #req = get_requirement_from_url(url)
- req = None
+ 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):
- logger.warn('Requirement %r looks like a filename, but the file does not exist'
- % name)
- url = path_to_url(name)
- #req = get_requirement_from_url(url)
- req = None
+ logger.warn('Requirement %r looks like a filename, but the file does not exist', name)
+ link = Link(path_to_url(name))
+
+ # If the line has an egg= definition, but isn't editable, pull the requirement out.
+ # Otherwise, assume the name is the req for the non URL/path/archive case.
+ if link and req is None:
+ url = link.url_fragment
+ req = link.egg_fragment
+
+ # Handle relative file URLs
+ if link.scheme == 'file' and re.search(r'\.\./', url):
+ url = path_to_url(os.path.normpath(os.path.abspath(link.path)))
+
+ else:
+ req = name
+
return cls(req, comes_from, url=url)
def __str__(self):
@@ -752,12 +758,12 @@ class Requirements(object):
def __contains__(self, item):
return item in self._keys
-
+
def __setitem__(self, key, value):
if key not in self._keys:
self._keys.append(key)
self._dict[key] = value
-
+
def __getitem__(self, key):
return self._dict[key]
@@ -1054,10 +1060,11 @@ class RequirementSet(object):
def copy_to_build_dir(self, req_to_install):
target_dir = req_to_install.editable and self.src_dir or self.build_dir
- logger.info("Copying %s to %s" %(req_to_install.name, target_dir))
+ logger.info("Copying %s to %s" % (req_to_install.name, target_dir))
dest = os.path.join(target_dir, req_to_install.name)
copytree(req_to_install.source_dir, dest)
- call_subprocess(["python", "%s/setup.py"%dest, "clean"])
+ call_subprocess(["python", "%s/setup.py" % dest, "clean"], cwd=dest,
+ command_desc='python setup.py clean')
def unpack_url(self, link, location, only_download=False):
if only_download:
@@ -1077,7 +1084,7 @@ class RequirementSet(object):
if self.upgrade or not r.satisfied_by]
if to_install:
- logger.notify('Installing collected packages: %s' % (', '.join([req.name for req in to_install])))
+ logger.notify('Installing collected packages: %s' % ', '.join([req.name for req in to_install]))
logger.indent += 2
try:
for requirement in to_install:
diff --git a/pip/util.py b/pip/util.py
index d2dae97a8..b9c32525b 100644
--- a/pip/util.py
+++ b/pip/util.py
@@ -7,7 +7,7 @@ import posixpath
import pkg_resources
import zipfile
import tarfile
-from pip.exceptions import InstallationError
+from pip.exceptions import InstallationError, BadCommand
from pip.backwardcompat import WindowsError, string_types, raw_input
from pip.locations import site_packages, running_under_virtualenv
from pip.log import logger
@@ -71,12 +71,12 @@ def backup_dir(dir, ext='.bak'):
def find_command(cmd, paths=None, pathext=None):
"""Searches the PATH for the given command and returns its path"""
if paths is None:
- paths = os.environ.get('PATH', []).split(os.pathsep)
+ paths = os.environ.get('PATH', '').split(os.pathsep)
if isinstance(paths, string_types):
paths = [paths]
# check if there are funny path extensions for executables, e.g. Windows
if pathext is None:
- pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
+ pathext = get_pathext()
pathext = [ext for ext in pathext.lower().split(os.pathsep)]
# don't use extensions if the command ends with one of them
if os.path.splitext(cmd)[1].lower() in pathext:
@@ -92,9 +92,16 @@ def find_command(cmd, paths=None, pathext=None):
return cmd_path_ext
if os.path.isfile(cmd_path):
return cmd_path
- return None
+ raise BadCommand('Cannot find command %r' % cmd)
+def get_pathext(default_pathext=None):
+ """Returns the path extensions from environment or a default"""
+ if default_pathext is None:
+ default_pathext = os.pathsep.join([ '.COM', '.EXE', '.BAT', '.CMD' ])
+ pathext = os.environ.get('PATHEXT', default_pathext)
+ return pathext
+
def ask(message, options):
"""Ask the message interactively, with the given possible responses"""
while 1:
diff --git a/pip/vcs/__init__.py b/pip/vcs/__init__.py
index 302c32da4..33c9c7c5d 100644
--- a/pip/vcs/__init__.py
+++ b/pip/vcs/__init__.py
@@ -4,7 +4,6 @@ import os
import shutil
from pip.backwardcompat import urlparse, urllib
-from pip.exceptions import BadCommand
from pip.log import logger
from pip.util import display_path, backup_dir, find_command, ask, rmtree
@@ -14,7 +13,7 @@ __all__ = ['vcs', 'get_src_requirement']
class VcsSupport(object):
_registry = {}
- schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp']
+ schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']
def __init__(self):
# Register more schemes with urlparse for various version control systems
@@ -106,8 +105,6 @@ class VersionControl(object):
if self._cmd is not None:
return self._cmd
command = find_command(self.name)
- if command is None:
- raise BadCommand('Cannot find command %r' % self.name)
logger.info('Found command %r at %r' % (self.name, command))
self._cmd = command
return command
diff --git a/pip/vcs/subversion.py b/pip/vcs/subversion.py
index b1fa40e1b..79c31ec0e 100644
--- a/pip/vcs/subversion.py
+++ b/pip/vcs/subversion.py
@@ -16,7 +16,7 @@ class Subversion(VersionControl):
name = 'svn'
dirname = '.svn'
repo_name = 'checkout'
- schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+https')
+ schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn')
bundle_file = 'svn-checkout.txt'
guide = ('# This was an svn checkout; to make it a checkout again run:\n'
'svn checkout --force -r %(rev)s %(url)s .\n')
diff --git a/setup.py b/setup.py
index 1d6d703a4..3b072376b 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ import os
from setuptools import setup
# If you change this version, change it also in docs/conf.py
-version = "1.0"
+version = "1.0.1"
doc_dir = os.path.join(os.path.dirname(__file__), "docs")
index_filename = os.path.join(doc_dir, "index.txt")
@@ -11,7 +11,7 @@ news_filename = os.path.join(doc_dir, "news.txt")
long_description = """\
The main website for pip is `www.pip-installer.org
<http://www.pip-installer.org>`_. You can also install
-the `in-development version <https://github.com/pypa/pip/tarball/master#egg=pip-dev>`_
+the `in-development version <https://github.com/pypa/pip/tarball/develop#egg=pip-dev>`_
of pip with ``easy_install pip==dev``.
"""
f = open(index_filename)
@@ -47,5 +47,5 @@ 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.6', 'scripttest==1.1.1', 'mock'],
+ tests_require=['nose', 'virtualenv>=1.6', 'scripttest>=1.1.1', 'mock'],
zip_safe=False)
diff --git a/tests/test_basic.py b/tests/test_basic.py
index a4c1b3997..aa1129745 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -6,8 +6,11 @@ import sys
from os.path import abspath, join, curdir, pardir
from nose import SkipTest
+from nose.tools import assert_raises
+from mock import Mock, patch
-from pip.util import rmtree
+from pip.util import rmtree, find_command
+from pip.exceptions import BadCommand
from tests.test_pip import (here, reset_env, run_pip, pyversion, mkdir,
src_folder, write_file)
@@ -521,6 +524,70 @@ def test_find_command_folder_in_path():
mkdir(path_one/'foo')
mkdir('path_two'); path_two = env.scratch_path/'path_two'
write_file(path_two/'foo', '# nothing')
- from pip.util import find_command
found_path = find_command('foo', map(str, [path_one, path_two]))
assert found_path == path_two/'foo'
+
+def test_does_not_find_command_because_there_is_no_path():
+ """
+ Test calling `pip.utils.find_command` when there is no PATH env variable
+ """
+ environ_before = os.environ
+ os.environ = {}
+ try:
+ try:
+ find_command('anycommand')
+ except BadCommand:
+ e = sys.exc_info()[1]
+ assert e.args == ("Cannot find command 'anycommand'",)
+ else:
+ raise AssertionError("`find_command` should raise `BadCommand`")
+ finally:
+ os.environ = environ_before
+
+@patch('pip.util.get_pathext')
+@patch('os.path.isfile')
+def test_find_command_trys_all_pathext(mock_isfile, getpath_mock):
+ """
+ If no pathext should check default list of extensions, if file does not
+ exist.
+ """
+ mock_isfile.return_value = False
+ # Patching os.pathsep failed on type checking
+ old_sep = os.pathsep
+ os.pathsep = ':'
+
+ getpath_mock.return_value = os.pathsep.join([".COM", ".EXE"])
+
+ paths = [ os.path.join('path_one', f) for f in ['foo.com', 'foo.exe', 'foo'] ]
+ expected = [ ((p,),) for p in paths ]
+
+ try:
+ assert_raises(BadCommand, find_command, 'foo', 'path_one')
+ assert mock_isfile.call_args_list == expected, "Actual: %s\nExpected %s" % (mock_isfile.call_args_list, expected)
+ assert getpath_mock.called, "Should call get_pathext"
+ finally:
+ os.pathsep = old_sep
+
+@patch('pip.util.get_pathext')
+@patch('os.path.isfile')
+def test_find_command_trys_supplied_pathext(mock_isfile, getpath_mock):
+ """
+ If pathext supplied find_command should use all of its list of extensions to find file.
+ """
+ mock_isfile.return_value = False
+ # Patching os.pathsep failed on type checking
+ old_sep = os.pathsep
+ os.pathsep = ':'
+ getpath_mock.return_value = ".FOO"
+
+ pathext = os.pathsep.join([".RUN", ".CMD"])
+
+ paths = [ os.path.join('path_one', f) for f in ['foo.run', 'foo.cmd', 'foo'] ]
+ expected = [ ((p,),) for p in paths ]
+
+ try:
+ assert_raises(BadCommand, find_command, 'foo', 'path_one', pathext)
+ assert mock_isfile.call_args_list == expected, "Actual: %s\nExpected %s" % (mock_isfile.call_args_list, expected)
+ assert not getpath_mock.called, "Should not call get_pathext"
+ finally:
+ os.pathsep = old_sep
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
index 8c8439d56..d40f5e872 100644
--- a/tests/test_requirements.py
+++ b/tests/test_requirements.py
@@ -1,7 +1,10 @@
+import os.path
import textwrap
+from pip.backwardcompat import urllib
from pip.req import Requirements
-from tests.test_pip import reset_env, run_pip, write_file, pyversion
+from tests.test_pip import reset_env, run_pip, write_file, pyversion, here, path_to_url
from tests.local_repos import local_checkout
+from tests.path import Path
def test_requirements_file():
"""
@@ -22,6 +25,19 @@ def test_requirements_file():
fn = '%s-%s-py%s.egg-info' % (other_lib_name, other_lib_version, pyversion)
assert result.files_created[env.site_packages/fn].dir
+def test_relative_requirements_file():
+ """
+ Test installing from a requirements file with a relative path with an egg= definition..
+
+ """
+ url = path_to_url(os.path.join(here, 'packages', '..', 'packages', 'FSPkg')) + '#egg=FSPkg'
+ env = reset_env()
+ write_file('file-egg-req.txt', textwrap.dedent("""\
+ %s
+ """ % url))
+ result = run_pip('install', '-vvv', '-r', env.scratch_path / 'file-egg-req.txt')
+ assert (env.site_packages/'FSPkg-0.1dev-py%s.egg-info' % pyversion) in result.files_created, str(result)
+ assert (env.site_packages/'fspkg') in result.files_created, str(result.stdout)
def test_multiple_requirements_files():
"""
diff --git a/tests/test_search.py b/tests/test_search.py
index 74890ce6a..7a4a9ef12 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -50,6 +50,17 @@ def test_search():
assert 'pip installs packages' in output.stdout
+def test_multiple_search():
+ """
+ Test searching for multiple packages at once.
+
+ """
+ reset_env()
+ output = run_pip('search', 'pip', 'INITools')
+ assert 'pip installs packages' in output.stdout
+ assert 'Tools for parsing and using INI-style files' in output.stdout
+
+
def test_searching_through_Search_class():
"""
Verify if ``pip.vcs.Search`` uses tests xmlrpclib.Transport class