diff options
-rw-r--r-- | docs/conf.py | 2 | ||||
-rw-r--r-- | docs/how-to-contribute.txt | 33 | ||||
-rw-r--r-- | docs/news.txt | 22 | ||||
-rw-r--r-- | docs/requirement-format.txt | 12 | ||||
-rw-r--r-- | pip/basecommand.py | 4 | ||||
-rw-r--r-- | pip/commands/search.py | 2 | ||||
-rw-r--r-- | pip/download.py | 23 | ||||
-rw-r--r-- | pip/index.py | 16 | ||||
-rw-r--r-- | pip/req.py | 47 | ||||
-rw-r--r-- | pip/util.py | 15 | ||||
-rw-r--r-- | pip/vcs/__init__.py | 5 | ||||
-rw-r--r-- | pip/vcs/subversion.py | 2 | ||||
-rw-r--r-- | setup.py | 6 | ||||
-rw-r--r-- | tests/test_basic.py | 71 | ||||
-rw-r--r-- | tests/test_requirements.py | 18 | ||||
-rw-r--r-- | tests/test_search.py | 11 |
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') @@ -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 |