1. Armin Ronacher
  2. pip

Commits

Ian Bicking  committed 68c1c19 Merge

merge carljm trunk

  • Participants
  • Parent commits 43586b9, 163feab
  • Branches trunk

Comments (0)

Files changed (9)

File .hgignore

View file
 # use glob syntax.
 syntax: glob
+MANIFEST
 tests/test-scratch/*
 testenv
 pip.egg-info/*
 dist/*
+docs/_build/*
+build/*
+*.pyc
+pip-log.txt

File MANIFEST.in

View file
+recursive-include docs *.txt
+recursive-include docs *.html
+prune docs/_build/*.txt
+prune docs/_build/_sources/*.txt
+prune docs/_build/_sources

File docs/conf.py

View file
 # The short X.Y version.
 version = '0.3'
 # The full version, including alpha/beta/rc tags.
-release = '0.3'
+release = '0.3.1'
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:

File docs/index.txt

View file
 
     -e svn+http://myrepo/svn/MyApp#egg=MyApp
 
-You have to start the URL with ``svn+`` (``git+``, ``hg+``or ``bzr+``), and
+You have to start the URL with ``svn+`` (``git+``, ``hg+`` or ``bzr+``), and
 you have to include ``#egg=Package`` so pip knows what to expect at that URL.
 You can also include ``@rev`` in the URL, e.g., ``@275`` to check out
 revision 275.
 Once you have the bundle file further network access won't be necessary.  To
 build a bundle file, do::
 
-    $ pip install bundle=MyApp.pybundle MyApp
+    $ pip bundle MyApp.pybundle MyApp
 
 (Using a `requirements file`_ would be wise.)  Then someone else can get the
 file ``MyApp.pybundle`` and run::
 for it.
 
 pip does not have to be installed to use it, you can run ``python
-pip`` and it will work.  This is intended to avoid the bootstrapping
-problem of installation.  You can also run pip inside any virtualenv
-environment, like::
+path/to/pip.py`` and it will work.  This is intended to avoid the
+bootstrapping problem of installation.  You can also run pip inside
+any virtualenv environment, like::
 
     $ virtualenv new-env/
     ... creates new-env/ ...
 
 This is exactly equivalent to::
 
-    $ ./new-env/bin/python pip install MyPackage
+    $ ./new-env/bin/python path/to/pip.py install MyPackage
 
 Except, if you have ``virtualenv`` installed and the path ``new-env/``
 doesn't exist, then a new virtualenv will be created.
 -----------------------
 
 If you are using `zc.buildout
-<http://pypi.python.org/pypi/zc.buildout>` you should look at
+<http://pypi.python.org/pypi/zc.buildout>`_ you should look at
 `gp.recipe.pip <http://pypi.python.org/pypi/gp.recipe.pip>`_ as an
 option to use pip and virtualenv in your buildouts.

File docs/news.txt

View file
 News for pip
 ============
 
+hg tip
+------
+
+* Make ``-e`` work better with local hg repositories
+
+* Construct PyPI URLs the exact way easy_install constructs URLs (you
+  might notice this if you use a custom index that is
+  slash-sensitive).
+
+* Improvements on Windows (from `Ionel Maries Cristian
+  <http://ionelmc.wordpress.com/>`_).
+
+* Fixed problem with not being able to install private git repositories.
+
+* Make ``pip zip`` zip all its arguments, not just the first.
+
+* Fix some filename issues on Windows.
+
+0.3.1
+-----
+
+* Improved virtualenv restart and various path/cleanup problems on win32.
+
+* Fixed a regression with installing from svn repositories (when not
+  using ``-e``).
+
+* Fixes when installing editable packages that put their source in a
+  subdirectory (like ``src/``).
+
+* Improve ``pip -h``
+
 0.3
 ---
 

File pip.py

View file
 #!/usr/bin/env python
 import sys
 import os
+import errno
+import stat
 import optparse
 import pkg_resources
 import urllib2
 # Choose a Git command based on platform.
 if sys.platform == 'win32':
     GIT_CMD = 'git.cmd'
+    BZR_CMD = 'bzr.bat'
 else:
     GIT_CMD = 'git'
+    BZR_CMD = 'bzr'
 
 ## FIXME: this shouldn't be a module setting
 default_vcs = None
     # when running pip.py without installing
     version=None
 
+def rmtree_errorhandler(func, path, exc_info):
+    typ, val, tb = exc_info
+    if issubclass(typ, OSError) and val.errno == errno.EACCES:
+        os.chmod(path, stat.S_IWRITE)
+        func(path)
+    else:
+        raise typ, val, tb
 
 class VcsSupport(object):
     _registry = {}
     def backends(self):
         return self._registry.values()
 
+    @property
+    def dirnames(self):
+        return [backend.dirname for backend in self.backends]
+
     def register(self, cls):
         if not hasattr(cls, 'name'):
             logger.warn('Cannot register VCS %s' % cls.__name__)
 
 parser = optparse.OptionParser(
     usage='%prog COMMAND [OPTIONS]',
-    version=version)
-
+    version=version,
+    add_help_option=False)
+
+parser.add_option(
+    '-h', '--help',
+    dest='help',
+    action='store_true',
+    help='Show help')
 parser.add_option(
     '-E', '--environment',
     dest='venv',
             prog='%s %s' % (sys.argv[0], self.name),
             version=parser.version)
         for option in parser.option_list:
-            if not option.dest:
+            if not option.dest or option.dest == 'help':
                 # -h, --version, etc
                 continue
             self.parser.add_option(option)
             command = _commands[command]
             command.parser.print_help()
             return
-        print 'Usage: %s [COMMAND] ...' % os.path.basename(sys.argv[0])
+        parser.print_help()
+        print
         print 'Commands available:'
         commands = list(set(_commands.values()))
         commands.sort(key=lambda x: x.name)
             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"
+            "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."
             )
 
     def run(self, options, args):
                     'The module %s (in %s) is not a directory; cannot be zipped'
                     % (module_name, filename))
             packages.append((module_name, filename))
+        last_status = None
         for module_name, filename in packages:
             if options.unzip:
-                return self.unzip_package(module_name, filename)
+                last_status = self.unzip_package(module_name, filename)
             else:
-                return self.zip_package(module_name, filename, options.no_pyc)
+                last_status = self.zip_package(module_name, filename, options.no_pyc)
+        return last_status
 
     def unzip_package(self, module_name, filename):
         zip_filename = os.path.dirname(filename)
     if initial_args is None:
         initial_args = sys.argv[1:]
     options, args = parser.parse_args(initial_args)
+    if options.help and not args:
+        args = ['help']
     if not args:
         parser.error('You must give a command (use "pip help" see a list of commands)')
     command = args[0].lower()
     file = __file__
     if file.endswith('.pyc'):
         file = file[:-1]
-    os.execv(python, [python, file] + args + [base, '___VENV_RESTART___'])
+    call_subprocess([python, file] + args + [base, '___VENV_RESTART___'])
+    sys.exit(0)
+    #~ os.execv(python, )
 
 class PackageFinder(object):
     """This finds packages.
         # 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)
+            url_name = self._find_url_name(Link(self.index_urls[0]), url_name, req) or req.url_name
+        def mkurl_pypi_url(url):
+            loc =  posixpath.join(url, url_name)
+            # For maximum compatibility with easy_install, ensure the path
+            # ends in a trailing slash.  Although this isn't in the spec
+            # (and PyPI can handle it without the slash) some other index
+            # implementations might break if they relied on easy_install's behavior.
+            if not loc.endswith('/'):
+                loc = loc + '/'
+            return loc
         if url_name is not None:
             locations = [
-                posixpath.join(url, url_name)
+                mkurl_pypi_url(url)
                 for url in self.index_urls] + self.find_links
         else:
             locations = list(self.find_links)
         locations.extend(self.dependency_links)
         for version in req.absolute_versions:
-            locations = [
-                posixpath.join(url, url_name, version)] + locations
+            if url_name is not None:
+                locations = [
+                    posixpath.join(url, url_name, version)] + locations
         locations = [Link(url) for url in locations]
         logger.debug('URLs to search for versions for %s:' % req)
         for location in locations:
         requirement, filename, or URL.
         """
         url = None
+        name = name.strip()
         req = name
         if is_url(name):
             url = name
             s = self.url
         if self.satisfied_by is not None:
             s += ' in %s' % display_path(self.satisfied_by.location)
-        if self.editable:
-            if self.req:
-                s += ' checkout from %s' % self.url
         if self.comes_from:
             if isinstance(self.comes_from, basestring):
                 comes_from = self.comes_from
                 base = os.path.join(self.source_dir, 'pip-egg-info')
             filenames = os.listdir(base)
             if self.editable:
+                filenames = []
+                for root, dirs, files in os.walk(base):
+                    for dir in vcs.dirnames:
+                        if dir in dirs:
+                            dirs.remove(dir)
+                    filenames.extend([os.path.join(root, dir)
+                                     for dir in dirs])
                 filenames = [f for f in filenames if f.endswith('.egg-info')]
             assert len(filenames) == 1, "Unexpected files/directories in %s: %s" % (base, ' '.join(filenames))
             self._egg_info_path = os.path.join(base, filenames[0])
         if self.is_bundle or os.path.exists(self.delete_marker_filename):
             logger.info('Removing source in %s' % self.source_dir)
             if self.source_dir:
-                shutil.rmtree(self.source_dir)
+                shutil.rmtree(self.source_dir, ignore_errors=True, onerror=rmtree_errorhandler)
             self.source_dir = None
             if self._temp_build_dir and os.path.exists(self._temp_build_dir):
-                shutil.rmtree(self._temp_build_dir)
+                shutil.rmtree(self._temp_build_dir, ignore_errors=True, onerror=rmtree_errorhandler)
             self._temp_build_dir = None
 
     def install_editable(self):
                         fp = open(vcs_bundle_file)
                         content = fp.read()
                         fp.close()
-                        url, rev = vcs_backend().parse_checkout_text(content)
+                        url, rev = vcs_backend().parse_vcs_bundle_file(content)
                         break
                 if url:
                     url = '%s+%s@%s' % (vc_type, url, rev)
             if req_to_install.satisfied_by is not None and not self.upgrade:
                 logger.notify('Requirement already satisfied: %s' % req_to_install)
             elif req_to_install.editable:
-                logger.notify('Checking out %s' % req_to_install)
+                logger.notify('Obtaining %s' % req_to_install)
             else:
                 if req_to_install.url and req_to_install.url.lower().startswith('file:'):
                     logger.notify('Unpacking %s' % display_path(url_to_filename(req_to_install.url)))
         elif (content_type.startswith('text/html')
               and is_svn_page(file_contents(filename))):
             # We don't really care about this
-            Subversion(link.url).unpack(location)
+            Subversion('svn+' + link.url).unpack(location)
         else:
             ## FIXME: handle?
             ## FIXME: magic signatures?
                 dir = os.path.dirname(fn)
                 if not os.path.exists(dir):
                     os.makedirs(dir)
-                if fn.endswith('/'):
+                if fn.endswith('/') or fn.endswith('\\'):
                     # A directory
                     if not os.path.exists(fn):
                         os.makedirs(fn)
 
     def __init__(self, url=None, *args, **kwargs):
         self.url = url
-        self.dirname = '.%s' % self.name
         super(VersionControl, self).__init__(*args, **kwargs)
 
     def _filter(self, line):
         """
         url = self.url.split('+', 1)[1]
         scheme, netloc, path, query, frag = urlparse.urlsplit(url)
+        rev = None
         if '@' in path:
-            path, rev = path.split('@', 1)
-        else:
-            rev = None
+            path, rev = path.rsplit('@', 1)
         url = urlparse.urlunsplit((scheme, netloc, path, query, ''))
         return url, rev
 
+    def parse_vcs_bundle_file(self, content):
+        """
+        Takes the contents of the bundled text file that explains how to revert
+        the stripped off version control data of the given package and returns
+        the URL and revision of it.
+        """
+        raise NotImplementedError
+
     def obtain(self, dest):
+        """
+        Called when installing or updating an editable package, takes the
+        source path of the checkout.
+        """
         raise NotImplementedError
 
     def unpack(self, location):
 
 class Subversion(VersionControl):
     name = 'svn'
+    dirname = '.svn'
     schemes = ('svn', 'svn+ssh')
     bundle_file = 'svn-checkout.txt'
     guide = ('# This was an svn checkout; to make it a checkout again run:\n'
             return url, 'unknown'
         return url, match.group(1)
 
-    def parse_checkout_text(self, text):
-        for line in text.splitlines():
+    def parse_vcs_bundle_file(self, content):
+        for line in content.splitlines():
             if not line.strip() or line.strip().startswith('#'):
                 continue
             match = re.search(r'^-r\s*([^ ])?', line)
             if os.path.exists(location):
                 # Subversion doesn't like to check out over an existing directory
                 # --force fixes this, but was only added in svn 1.5
-                os.rmdir(location)
+                shutil.rmtree(location, onerror=rmtree_errorhandler)
             call_subprocess(
                 ['svn', 'checkout', url, location],
                 filter_stdout=self._filter, show_stdout=False)
 
 class Git(VersionControl):
     name = 'git'
+    dirname = '.git'
     schemes = ('git', 'git+http', 'git+ssh')
     bundle_file = 'git-clone.txt'
     guide = ('# This was a Git repo; to make it a repo again run:\n'
         assert not location.rstrip('/').endswith('.git'), 'Bad directory: %s' % location
         return self.get_url(location), self.get_revision(location)
 
-    def parse_clone_text(self, text):
+    def parse_vcs_bundle_file(self, content):
         url = rev = None
-        for line in text.splitlines():
+        for line in content.splitlines():
             if not line.strip() or line.strip().startswith('#'):
                 continue
             url_match = re.search(r'git\s*remote\s*add\s*origin(.*)\s*-f', line)
             logger.warn('Git URL does not fit normal structure: %s' % repo)
             return '%s@%s#egg=%s-dev' % (repo, current_rev, egg_project_name)
 
+    def get_url_rev(self):
+        """
+        Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'.
+        That's required because although they use SSH they sometimes doesn't
+        work with a ssh:// scheme (e.g. Github). But we need a scheme for
+        parsing. Hence we remove it again afterwards and return it as a stub.
+        """
+        if not '://' in self.url:
+            self.url = self.url.replace('git+', 'git+ssh://')
+            url, rev = super(Git, self).get_url_rev()
+            url = url.replace('ssh://', '')
+            return url, rev
+        return super(Git, self).get_url_rev()
+
 vcs.register(Git)
 
-
 class Mercurial(VersionControl):
     name = 'hg'
+    dirname = '.hg'
     schemes = ('hg', 'hg+http', 'hg+ssh')
     bundle_file = 'hg-clone.txt'
     guide = ('# This was a Mercurial repo; to make it a repo again run:\n'
         assert not location.rstrip('/').endswith('.hg'), 'Bad directory: %s' % location
         return self.get_url(location), self.get_revision(location)
 
-    def parse_clone_text(self, text):
+    def parse_vcs_bundle_file(self, content):
         url = rev = None
-        for line in text.splitlines():
+        for line in content.splitlines():
             if not line.strip() or line.strip().startswith('#'):
                 continue
             url_match = re.search(r'hg\s*pull\s*(.*)\s*', line)
     def get_url(self, location):
         url = call_subprocess(
             ['hg', 'showconfig', 'paths.default'],
-            show_stdout=False, cwd=location)
+            show_stdout=False, cwd=location).strip()
+        if url.startswith('/') or url.startswith('\\'):
+            url = filename_to_url(url)
         return url.strip()
 
     def get_tip_revision(self, location):
 
 class Bazaar(VersionControl):
     name = 'bzr'
+    dirname = '.bzr'
     bundle_file = 'bzr-branch.txt'
     schemes = ('bzr', 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp')
     guide = ('# This was a Bazaar branch; to make it a branch again run:\n'
         assert not location.rstrip('/').endswith('.bzr'), 'Bad directory: %s' % location
         return self.get_url(location), self.get_revision(location)
 
-    def parse_clone_text(self, text):
+    def parse_vcs_bundle_file(self, content):
         url = rev = None
-        for line in text.splitlines():
+        for line in content.splitlines():
             if not line.strip() or line.strip().startswith('#'):
                 continue
             match = re.search(r'^bzr\s*branch\s*-r\s*(\d*)', line)
             if os.path.exists(location):
                 os.rmdir(location)
             call_subprocess(
-                ['bzr', 'branch', url, location],
+                [BZR_CMD, 'branch', url, location],
                 filter_stdout=self._filter, show_stdout=False)
         finally:
             logger.indent -= 2
                 if response == 's':
                     logger.notify('Switching branch %s to %s%s'
                                   % (display_path(dest), url, rev_display))
-                    call_subprocess(['bzr', 'switch', url], cwd=dest)
+                    call_subprocess([BZR_CMD, 'switch', url], cwd=dest)
                 elif response == 'i':
                     # do nothing
                     pass
                 url = 'bzr+' + url
             if update:
                 call_subprocess(
-                    ['bzr', 'pull', '-q'] + rev_options + [url], cwd=dest)
+                    [BZR_CMD, 'pull', '-q'] + rev_options + [url], cwd=dest)
             else:
                 call_subprocess(
-                    ['bzr', 'branch', '-q'] + rev_options + [url, dest])
+                    [BZR_CMD, 'branch', '-q'] + rev_options + [url, dest])
 
     def get_url(self, location):
         urls = call_subprocess(
-            ['bzr', 'info'], show_stdout=False, cwd=location)
+            [BZR_CMD, 'info'], show_stdout=False, cwd=location)
         for line in urls.splitlines():
             line = line.strip()
             for x in ('checkout of branch: ',
 
     def get_revision(self, location):
         revision = call_subprocess(
-            ['bzr', 'revno'], show_stdout=False, cwd=location)
-        return revision.strip()
+            [BZR_CMD, 'revno'], show_stdout=False, cwd=location)
+        return revision.splitlines()[-1]
 
     def get_newest_revision(self, location):
         url = self.get_url(location)
         revision = call_subprocess(
-            ['bzr', 'revno', url], show_stdout=False, cwd=location)
-        return revision.strip()
+            [BZR_CMD, 'revno', url], show_stdout=False, cwd=location)
+        return revision.splitlines()[-1]
 
     def get_tag_revs(self, location):
         tags = call_subprocess(
-            ['bzr', 'tags'], show_stdout=False, cwd=location)
+            [BZR_CMD, 'tags'], show_stdout=False, cwd=location)
         tag_revs = []
         for line in tags.splitlines():
             tags_match = re.search(r'([.\w-]+)\s*(.*)$', line)
 ## Requirement files
 
 _scheme_re = re.compile(r'^(http|https|file):', re.I)
-_drive_re = re.compile(r'/*([a-z])\|', re.I)
+_url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I)
 def get_file_content(url, comes_from=None):
     """Gets the content of a file; it may be a filename, file: URL, or
     http: URL.  Returns (location, content)"""
         if scheme == 'file':
             path = url.split(':', 1)[1]
             path = path.replace('\\', '/')
-            match = _drive_re.match(path)
+            match = _url_slash_drive_re.match(path)
             if match:
                 path = match.group(1) + ':' + path.split('|', 1)[1]
             path = urllib.unquote(path)
     Convert a path to a file: URL.  The path will be made absolute.
     """
     filename = os.path.normcase(os.path.abspath(filename))
+    if _drive_re.match(filename):
+        filename = filename[0] + '|' + filename[2:]
     url = urllib.quote(filename)
-    if _drive_re.match(url):
-        url = url[0] + '|' + url[2:]
     url = url.replace(os.path.sep, '/')
     url = url.lstrip('/')
     return 'file:///' + url

File regen-docs

View file
 #!/bin/sh
 
+CMD="$1"
+if [ "$CMD" = "release" ] ; then
+  python -c 'import setuptools; __file__="setup.py"; execfile(__file__)' register sdist upload
+  CMD="publish"
+fi
+
 mkdir -p docs/_static docs/_build
 sphinx-build -E -b html docs/ docs/_build || exit 1
-if [ "$1" = "publish" ] ; then
+if [ "$CMD" = "publish" ] ; then
   cd docs/_build
   echo "Uploading files..."
   tar czvf - . | ssh flow.openplans.org 'ssh acura.openplans.org "cd /www/pip.openplans.org/; tar xzvf -"'

File setup.cfg

File contents unchanged.

File setup.py

View file
 import os
 
 
-version = '0.3'
+version = '0.3.1'
 
 doc_dir = os.path.join(os.path.dirname(__file__), 'docs')
 index_filename = os.path.join(doc_dir, 'index.txt')
 long_description = """\ 
 The main website for pip is `pip.openplans.org
-<http://pip.openplans.org>`_
-
+<http://pip.openplans.org>`_.  You can also install
+the `in-development version <http://bitbucket.org/ianb/pip/get/tip.gz#egg=pip-dev>_` 
+of pip with ``easy_install pip==dev``.
 """
 long_description = long_description + open(index_filename).read().split('split here', 1)[1]