diff options
62 files changed, 1543 insertions, 776 deletions
diff --git a/AUTHORS.txt b/AUTHORS.txt index b501975dc..5fa57113d 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,6 +1,7 @@ Alex Grönholm Alex Morega Alexandre Conrad +Antti Kaihola Armin Ronacher Brian Rosner Carl Meyer @@ -24,13 +25,23 @@ Kenneth Belitzky Kumar McMillan Luke Macken Masklinn +Marc Abramowitz +Marcus Smith +Matt Maker Nowell Strite Oliver Tonnhofer +Olivier Girardot Patrick Jenkins Paul Nasrat +Paul Oswald +Paul van der Linden Peter Waller +Piet Delport +Qiangning Hong +Rene Dudfield Ronny Pfannschmidt Simon Cross +Stavros Korokithakis Thomas Johansson Vinay Sajip Vitaly Babiy diff --git a/contrib/get-pip.py b/contrib/get-pip.py index 58d90370c..9fd5b3039 100755 --- a/contrib/get-pip.py +++ b/contrib/get-pip.py @@ -1106,6 +1106,7 @@ import zlib import tempfile import shutil + def unpack(sources): temp_dir = tempfile.mkdtemp('-scratchdir', 'unpacker-') for package, content in sources.items(): @@ -1122,7 +1123,7 @@ def unpack(sources): return temp_dir if __name__ == "__main__": - if sys.version_info >= (3,0): + if sys.version_info >= (3, 0): exec("def do_exec(co, loc): exec(co, loc)\n") import pickle sources = sources.encode("ascii") # ensure bytes diff --git a/contrib/packager/__init__.py b/contrib/packager/__init__.py index 580acd204..96068312a 100644 --- a/contrib/packager/__init__.py +++ b/contrib/packager/__init__.py @@ -8,6 +8,7 @@ import base64 import os import fnmatch + def find_toplevel(name): for syspath in sys.path: lib = os.path.join(syspath, name) @@ -18,10 +19,12 @@ def find_toplevel(name): return mod raise LookupError(name) + def pkgname(toplevel, rootpath, path): parts = path.split(os.sep)[len(rootpath.split(os.sep)):] return '.'.join([toplevel] + [os.path.splitext(x)[0] for x in parts]) + def pkg_to_mapping(name): toplevel = find_toplevel(name) if os.path.isfile(toplevel): diff --git a/contrib/packager/template.py b/contrib/packager/template.py index c7f6fdc76..406958936 100644 --- a/contrib/packager/template.py +++ b/contrib/packager/template.py @@ -10,6 +10,7 @@ import zlib import tempfile import shutil + def unpack(sources): temp_dir = tempfile.mkdtemp('-scratchdir', 'unpacker-') for package, content in sources.items(): @@ -25,8 +26,9 @@ def unpack(sources): mod.close() return temp_dir + if __name__ == "__main__": - if sys.version_info >= (3,0): + if sys.version_info >= (3, 0): exec("def do_exec(co, loc): exec(co, loc)\n") import pickle sources = sources.encode("ascii") # ensure bytes diff --git a/docs/conf.py b/docs/conf.py index 040947f89..7dd29dc87 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.2" +release = "1.1" version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation diff --git a/docs/configuration.txt b/docs/configuration.txt index 25a6ad511..35d0a347f 100644 --- a/docs/configuration.txt +++ b/docs/configuration.txt @@ -115,3 +115,27 @@ Just leave an empty space between the passsed values, e.g.:: is the same as calling:: pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com + +Configuration options +--------------------- + +Mirror support +************** + +The `PyPI mirroring infrastructure <http://pypi.python.org/mirrors>`_ as +described in `PEP 381 <http://www.python.org/dev/peps/pep-0381/>`_ can be +used by passing the ``--use-mirrors`` option to the install command. +Alternatively, you can use the other ways to configure pip, e.g.:: + + $ export PIP_USE_MIRRORS=true + +If enabled, pip will automatically query the DNS entry of the mirror index URL +to find the list of mirrors to use. In case you want to override this list, +please use the ``--mirrors`` option of the install command, or add to your pip +configuration file:: + + [install] + use-mirrors = true + mirrors = + http://d.pypi.python.org + http://b.pypi.python.org diff --git a/docs/how-to-contribute.txt b/docs/contributing.txt index be5beb638..c3d0c1356 100644 --- a/docs/how-to-contribute.txt +++ b/docs/contributing.txt @@ -1,6 +1,6 @@ -======================== -How To Contribute to Pip -======================== +================= +How to contribute +================= All kinds of contributions are welcome - code, tests, documentation, @@ -75,6 +75,61 @@ you can learn how to set up a Hudson CI server like that in the +Running the Tests +================= + +Pip uses some system tools - VCS related tools - in its tests, so you need to +intall them (Linux):: + + sudo apt-get install subversion bzr git-core mercurial + +Or downloading and installing `Subversion +<http://subversion.apache.org/packages.html>`_, `Bazaar +<http://wiki.bazaar.canonical.com/Download>`_, `Git +<http://git-scm.com/download>`_ and `Mercurial +<http://mercurial.selenic.com/downloads/>`_ manually. + + +After all requirements (system and python) are installed, +just run the following command:: + + $ python setup.py test + +Running tests directly with Nose +-------------------------------- + +If you want to run only a selection of the tests, you'll need to run them +directly with nose instead. Create a virtualenv, and install required +packages:: + + pip install nose virtualenv scripttest mock + +Run nosetests:: + + nosetests + +Or select just a single test to run:: + + cd tests; nosetests test_upgrade.py:test_uninstall_rollback + + +Troubleshooting +--------------- + +Locale Warnings + There was a problem with locales configuration when running tests in a Hudson + CI Server that broke some tests. The problem was not with pip, but with + `locales` configuration. Hudson was not setting LANG environment variable + correctly, so the solution to fix it was changing default language to + en_US.UTF-8. + The following has been done in a Ubuntu Server 9.10 machine:: + + $ sudo locale-gen en_US en_US.UTF-8 + $ sudo dpkg-reconfigure locales + $ sudo update-locale LANG=en_US.UTF-8 + + + Contributing with Tests ======================= diff --git a/docs/glossary.txt b/docs/glossary.txt new file mode 100644 index 000000000..c9decea5d --- /dev/null +++ b/docs/glossary.txt @@ -0,0 +1,12 @@ +======== +Glossary +======== + +.. glossary:: + + PyPI + The `Python Package Index`_, formerly known as the Cheese Shop, + is a central catalog of Python packages. By default, when + installing packages,`pip` searches for them in PyPI. + + .. _`Python Package Index`: http://pypi.python.org/pypi diff --git a/docs/index.txt b/docs/index.txt index 286bd7cdf..3f117c0ce 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -1,110 +1,49 @@ pip === -pip is a tool for installing and managing Python packages, such as those -found in the `Python Package Index <http://pypi.python.org/pypi>`_. +`pip` is a tool for installing and managing Python packages, such as +those found in the `Python Package Index`_. It's a replacement for +easy_install_. +:: -pip is a replacement for `easy_install -<http://peak.telecommunity.com/DevCenter/EasyInstall>`_. It mostly -uses the same techniques for finding packages, so packages that are -easy_installable should be pip-installable as well. This means that -you can use ``pip install SomePackage`` instead of ``easy_install -SomePackage``. + $ pip install simplejson + [... progress report ...] + Successfully installed simplejson -In order to use pip, you must first install `setuptools -<http://pypi.python.org/pypi/setuptools>`_ or `distribute -<http://pypi.python.org/pypi/distribute>`_. If you use `virtualenv -<http://www.virtualenv.org>`_, a copy of pip will be automatically be -installed in each virtual environment you create. +.. _`Python Package Index`: http://pypi.python.org/pypi +.. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall + +Upgrading a package:: + + $ pip install --upgrade simplejson + [... progress report ...] + Successfully installed simplejson + +Removing a package:: + + $ pip uninstall simplejson + Uninstalling simplejson: + /home/me/env/lib/python2.7/site-packages/simplejson + /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + Proceed (y/n)? y + Successfully uninstalled simplejson .. comment: split here .. toctree:: - :maxdepth: 1 + :maxdepth: 2 - news installing - requirement-format + usage + requirements configuration - how-to-contribute - running-tests + other-tools + contributing + news + glossary .. comment: split here -Usage ------ - -Once you have pip, you can use it like this:: - - $ pip install SomePackage - -SomePackage is some package you'll find on `PyPI -<http://pypi.python.org/pypi/>`_. This installs the package and all -its dependencies. - -pip does other stuff too, with packages, but install is the biggest -one. You can ``pip uninstall`` too. - -You can also install from a URL (that points to a tar or zip file), -install from some version control system (use URLs like -``hg+http://domain/repo`` -- or prefix ``git+``, ``svn+`` etc). pip -knows a bunch of stuff about revisions and stuff, so if you need to do -things like install a very specific revision from a repository pip can -do that too. - -If you've ever used ``python setup.py develop``, you can do something -like that with ``pip install -e ./`` -- this works with packages that -use ``distutils`` too (usually this only works with Setuptools -projects). - -You can use ``pip install --upgrade SomePackage`` to upgrade to a -newer version, or ``pip install SomePackage==1.0.4`` to install a very -specific version. - -Pip Compared To easy_install ----------------------------- - -pip is meant to improve on easy_install. Some of the improvements: - -* All packages are downloaded before installation. Partially-completed - installation doesn't occur as a result. - -* Care is taken to present useful output on the console. - -* The reasons for actions are kept track of. For instance, if a package is - being installed, pip keeps track of why that package was required. - -* Error messages should be useful. - -* The code is relatively concise and cohesive, making it easier to use - programmatically. - -* Packages don't have to be installed as egg archives, they can be installed - flat (while keeping the egg metadata). - -* Native support for other version control systems (Git, Mercurial and Bazaar) - -* Uninstallation of packages. - -* Simple to define fixed sets of requirements and reliably reproduce a - set of packages. - -pip doesn't do everything that easy_install does. Specifically: - -* It cannot install from eggs. It only installs from source. (In the - future it would be good if it could install binaries from Windows ``.exe`` - or ``.msi`` -- binary install on other platforms is not a priority.) - -* It doesn't understand Setuptools extras (like ``package[test]``). This should - be added eventually. - -* It is incompatible with some packages that extensively customize distutils - or setuptools in their ``setup.py`` files. - -pip is complementary with `virtualenv -<http://pypi.python.org/pypi/virtualenv>`__, and it is encouraged that you use -virtualenv to isolate your installation. - Community --------- @@ -113,232 +52,3 @@ Bugs can be filed in the `pip issue tracker <https://github.com/pypa/pip/issues/>`_. Discussion happens on the `virtualenv email group <http://groups.google.com/group/python-virtualenv?hl=en>`_. - -Uninstall ---------- - -pip is able to uninstall most installed packages with ``pip uninstall -package-name``. - -Known exceptions include pure-distutils packages installed with -``python setup.py install`` (such packages leave behind no metadata allowing -determination of what files were installed), and script wrappers installed -by develop-installs (``python setup.py develop``). - -pip also performs an automatic uninstall of an old version of a package -before upgrading to a newer version, so outdated files (and egg-info data) -from conflicting versions aren't left hanging around to cause trouble. The -old version of the package is automatically restored if the new version -fails to download or install. - -.. _`requirements file`: - -Requirements Files ------------------- - -When installing software, and Python packages in particular, it's common that -you get a lot of libraries installed. You just did ``easy_install MyPackage`` -and you get a dozen packages. Each of these packages has its own version. - -Maybe you ran that installation and it works. Great! Will it keep working? -Did you have to provide special options to get it to find everything? Did you -have to install a bunch of other optional pieces? Most of all, will you be able -to do it again? Requirements files give you a way to create an *environment*: -a *set* of packages that work together. - -If you've ever tried to setup an application on a new system, or with slightly -updated pieces, and had it fail, pip requirements are for you. If you -haven't had this problem then you will eventually, so pip requirements are -for you too -- requirements make explicit, repeatable installation of packages. - -So what are requirements files? They are very simple: lists of packages to -install. Instead of running something like ``pip MyApp`` and getting -whatever libraries come along, you can create a requirements file something like:: - - MyApp - Framework==0.9.4 - Library>=0.2 - -Then, regardless of what MyApp lists in ``setup.py``, you'll get a -specific version of Framework (0.9.4) and at least the 0.2 version of -Library. (You might think you could list these specific versions in -MyApp's ``setup.py`` -- but if you do that you'll have to edit MyApp -if you want to try a new version of Framework, or release a new -version of MyApp if you determine that Library 0.3 doesn't work with -your application.) You can also add optional libraries and support -tools that MyApp doesn't strictly require, giving people a set of -recommended libraries. - -You can also include "editable" packages -- packages that are checked out from -Subversion, Git, Mercurial and Bazaar. These are just like using the ``-e`` -option to pip. They look like:: - - -e svn+http://myrepo/svn/MyApp#egg=MyApp - -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. - -Requirement files are mostly *flat*. Maybe ``MyApp`` requires -``Framework``, and ``Framework`` requires ``Library``. I encourage -you to still list all these in a single requirement file; it is the -nature of Python programs that there are implicit bindings *directly* -between MyApp and Library. For instance, Framework might expose one -of Library's objects, and so if Library is updated it might directly -break MyApp. If that happens you can update the requirements file to -force an earlier version of Library, and you can do that without -having to re-release MyApp at all. - -Read the `requirements file format <http://pip.openplans.org/requirement-format.html>`_ to -learn about other features. - -Freezing Requirements ---------------------- - -So you have a working set of packages, and you want to be able to install them -elsewhere. `Requirements files`_ let you install exact versions, but it won't -tell you what all the exact versions are. - -To create a new requirements file from a known working environment, use:: - - $ pip freeze > stable-req.txt - -This will write a listing of *all* installed libraries to ``stable-req.txt`` -with exact versions for every library. You may want to edit the file down after -generating (e.g., to eliminate unnecessary libraries), but it'll give you a -stable starting point for constructing your requirements file. - -You can also give it an existing requirements file, and it will use that as a -sort of template for the new file. So if you do:: - - $ pip freeze -r devel-req.txt > stable-req.txt - -it will keep the packages listed in ``devel-req.txt`` in order and preserve -comments. - -Bundles -------- - -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 -source for your package, and you can have pip install them all together. -Once you have the bundle file further network access won't be necessary. To -build a bundle file, do:: - - $ pip bundle MyApp.pybundle MyApp - -(Using a `requirements file`_ would be wise.) Then someone else can get the -file ``MyApp.pybundle`` and run:: - - $ pip install MyApp.pybundle - -This is *not* a binary format. This only packages source. If you have binary -packages, then the person who installs the files will have to have a compiler, -any necessary headers installed, etc. Binary packages are hard, this is -relatively easy. - -Using pip with virtualenv -------------------------- - -pip is most nutritious when used with `virtualenv -<http://pypi.python.org/pypi/virtualenv>`__. One of the reasons pip -doesn't install "multi-version" eggs is that virtualenv removes much of the need -for it. Because pip is installed by virtualenv, just use -``path/to/my/environment/bin/pip`` to install things into that -specific environment. - -To tell pip to only run if there is a virtualenv currently activated, -and to bail if not, use:: - - export PIP_REQUIRE_VIRTUALENV=true - -To tell pip to automatically use the currently active virtualenv:: - - export PIP_RESPECT_VIRTUALENV=true - -Providing an environment with ``-E`` will be ignored. - -Using pip with virtualenvwrapper ---------------------------------- - -If you are using `virtualenvwrapper -<http://www.doughellmann.com/projects/virtualenvwrapper/>`_, you might -want pip to automatically create its virtualenvs in your -``$WORKON_HOME``. - -You can tell pip to do so by defining ``PIP_VIRTUALENV_BASE`` in your -environment and setting it to the same value as that of -``$WORKON_HOME``. - -Do so by adding the line:: - - export PIP_VIRTUALENV_BASE=$WORKON_HOME - -in your .bashrc under the line starting with ``export WORKON_HOME``. - -Using pip with buildout ------------------------ - -If you are using `zc.buildout -<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. - -Command line completion ------------------------ - -pip comes with support for command line completion in bash and zsh and -allows you tab complete commands and options. To enable it you simply -need copy the required shell script to the your shell startup file -(e.g. ``.profile`` or ``.zprofile``) by running the special ``completion`` -command, e.g. for bash:: - - $ pip completion --bash >> ~/.profile - -And for zsh:: - - $ pip completion --zsh >> ~/.zprofile - -Alternatively, you can use the result of the ``completion`` command -directly with the eval function of you shell, e.g. by adding:: - - eval "`pip completion --bash`" - -to your startup file. - -Searching for packages ----------------------- - -pip can search the `Python Package Index <http://pypi.python.org/pypi>`_ (PyPI) -for packages using the ``pip search`` command. To search, run:: - - $ pip search "query" - -The query will be used to search the names and summaries of all packages -indexed. - -pip searches http://pypi.python.org/pypi by default but alternative indexes -can be searched by using the ``--index`` flag. - -Mirror support --------------- - -The `PyPI mirroring infrastructure <http://pypi.python.org/mirrors>`_ as -described in `PEP 381 <http://www.python.org/dev/peps/pep-0381/>`_ can be -used by passing the ``--use-mirrors`` option to the install command. -Alternatively, you can use the other ways to configure pip, e.g.:: - - $ export PIP_USE_MIRRORS=true - -If enabled, pip will automatically query the DNS entry of the mirror index URL -to find the list of mirrors to use. In case you want to override this list, -please use the ``--mirrors`` option of the install command, or add to your pip -configuration file:: - - [install] - use-mirrors = true - mirrors = - http://d.pypi.python.org - http://b.pypi.python.org diff --git a/docs/installing.txt b/docs/installing.txt index 7c5d9999b..5f86c6a49 100644 --- a/docs/installing.txt +++ b/docs/installing.txt @@ -23,8 +23,7 @@ Prior to installing pip make sure you have either `setuptools <http://pypi.python.org/pypi/distribute>`_ installed. Please consult your operating system's package manager or install it manually:: - $ curl -O http://python-distribute.org/distribute_setup.py - $ python distribute_setup.py + $ curl http://python-distribute.org/distribute_setup.py | python .. warning:: @@ -37,8 +36,7 @@ Using the installer Download `get-pip.py <https://raw.github.com/pypa/pip/master/contrib/get-pip.py>`_ and execute it, using the Python interpreter of your choice:: - $ curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py - $ python get-pip.py + $ curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python This may have to be run as root. diff --git a/docs/news.txt b/docs/news.txt index 9ae479772..3bcdb2a20 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,10 +1,72 @@ -News / Changelog -================ +==== +News +==== -Next release (1.1) schedule +Changelog +========= + +Next release (1.2) schedule --------------------------- -Beta release mid-July 2011, final release early August. +Beta and final releases planned for the second half of 2012. + +1.1 (2012-02-16) +---------------- + +* Fixed issue #326 - don't crash when a package's setup.py emits UTF-8 and + then fails. Thanks Marc Abramowitz. + +* Added ``--target`` option for installing directly to arbitrary directory. + Thanks Stavros Korokithakis. + +* Added support for authentication with Subversion repositories. Thanks + Qiangning Hong. + +* Fixed issue #315 - ``--download`` now downloads dependencies as well. + Thanks Qiangning Hong. + +* Errors from subprocesses will display the current working directory. + Thanks Antti Kaihola. + +* Fixed issue #369 - compatibility with Subversion 1.7. Thanks Qiangning + Hong. Note that setuptools remains incompatible with Subversion 1.7; to + get the benefits of pip's support you must use Distribute rather than + setuptools. + +* Fixed issue #57 - ignore py2app-generated OS X mpkg zip files in finder. + Thanks Rene Dudfield. + +* Fixed issue #182 - log to ~/Library/Logs/ by default on OS X framework + installs. Thanks Dan Callahan for report and patch. + +* Fixed issue #310 - understand version tags without minor version ("py3") + in sdist filenames. Thanks Stuart Andrews for report and Olivier Girardot for + patch. + +* Fixed issue #7 - Pip now supports optionally installing setuptools + "extras" dependencies; e.g. "pip install Paste[openid]". Thanks Matt Maker + and Olivier Girardot. + +* Fixed issue #391 - freeze no longer borks on requirements files with + --index-url or --find-links. Thanks Herbert Pfennig. + +* Fixed issue #288 - handle symlinks properly. Thanks lebedov for the patch. + +* Fixed issue #49 - pip install -U no longer reinstalls the same versions of + packages. Thanks iguananaut for the pull request. + +* Removed ``-E`` option and ``PIP_RESPECT_VIRTUALENV``; both use a + restart-in-venv mechanism that's broken, and neither one is useful since + every virtualenv now has pip inside it. + +* Fixed issue #366 - pip throws IndexError when it calls `scraped_rel_links` + +* Fixed issue #22 - pip search should set and return a userful shell status code + +* Fixed issue #351 and #365 - added global ``--exists-action`` command line + option to easier script file exists conflicts, e.g. from editable + requirements from VCS that have a changed repo URL. + 1.0.2 (2011-07-16) ------------------ @@ -13,8 +75,8 @@ Beta release mid-July 2011, final release early August. * Fixed issue #295 - Reinstall a package when using the ``install -I`` option * Fixed issue #283 - Finds a Git tag pointing to same commit as origin/master * Fixed issue #279 - Use absolute path for path to docs in setup.py -* Fixed issue #320 - Correctly handle exceptions on Python3. -* Fixed issue #314 - Correctly parse ``--editable`` lines in requirements files +* Fixed issue #314 - Correctly handle exceptions on Python3. +* Fixed issue #320 - Correctly parse ``--editable`` lines in requirements files 1.0.1 (2011-04-30) ------------------ diff --git a/docs/other-tools.txt b/docs/other-tools.txt new file mode 100644 index 000000000..a705e8ab2 --- /dev/null +++ b/docs/other-tools.txt @@ -0,0 +1,131 @@ +============================= +Relationship with other tools +============================= + +Pip Compared To easy_install +---------------------------- + +pip is meant to improve on easy_install. Some of the improvements: + +* All packages are downloaded before installation. Partially-completed + installation doesn't occur as a result. + +* Care is taken to present useful output on the console. + +* The reasons for actions are kept track of. For instance, if a package is + being installed, pip keeps track of why that package was required. + +* Error messages should be useful. + +* The code is relatively concise and cohesive, making it easier to use + programmatically. + +* Packages don't have to be installed as egg archives, they can be installed + flat (while keeping the egg metadata). + +* Native support for other version control systems (Git, Mercurial and Bazaar) + +* Uninstallation of packages. + +* Simple to define fixed sets of requirements and reliably reproduce a + set of packages. + +pip doesn't do everything that easy_install does. Specifically: + +* It cannot install from eggs. It only installs from source. (In the + future it would be good if it could install binaries from Windows ``.exe`` + or ``.msi`` -- binary install on other platforms is not a priority.) + +* It doesn't understand Setuptools extras (like ``package[test]``). This should + be added eventually. + +* It is incompatible with some packages that extensively customize distutils + or setuptools in their ``setup.py`` files. + +pip is complementary with `virtualenv +<http://pypi.python.org/pypi/virtualenv>`__, and it is encouraged that you use +virtualenv to isolate your installation. + +Using pip with virtualenv +------------------------- + +pip is most nutritious when used with `virtualenv +<http://pypi.python.org/pypi/virtualenv>`__. One of the reasons pip +doesn't install "multi-version" eggs is that virtualenv removes much of the need +for it. Because pip is installed by virtualenv, just use +``path/to/my/environment/bin/pip`` to install things into that +specific environment. + +To tell pip to only run if there is a virtualenv currently activated, +and to bail if not, use:: + + export PIP_REQUIRE_VIRTUALENV=true + + +Using pip with virtualenvwrapper +--------------------------------- + +If you are using `virtualenvwrapper +<http://www.doughellmann.com/projects/virtualenvwrapper/>`_, you might +want pip to automatically create its virtualenvs in your +``$WORKON_HOME``. + +You can tell pip to do so by defining ``PIP_VIRTUALENV_BASE`` in your +environment and setting it to the same value as that of +``$WORKON_HOME``. + +Do so by adding the line:: + + export PIP_VIRTUALENV_BASE=$WORKON_HOME + +in your .bashrc under the line starting with ``export WORKON_HOME``. + +Using pip with buildout +----------------------- + +If you are using `zc.buildout +<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. + +Using pip with the "user scheme" +-------------------------------- + +With Python 2.6 came the `"user scheme" for installation +<http://docs.python.org/install/index.html#alternate-installation-the-user-scheme>`_, which means that all +Python distributions support an alternative install location that is specific to a user. +The default location for each OS is explained in the python documentation +for the `site.USER_BASE <http://docs.python.org/library/site.html#site.USER_BASE>`_ variable. +This mode of installation can be turned on by +specifying the ``--user`` option to ``pip install``. + +Moreover, the "user scheme" can be customized by setting the +``PYTHONUSERBASE`` environment variable, which updates the value of ``site.USER_BASE``. + +To install "somepackage" into an environment with site.USER_BASE customized to '/myappenv', do the following:: + + export PYTHONUSERBASE=/myappenv + pip install --user somepackage + + +Command line completion +----------------------- + +pip comes with support for command line completion in bash and zsh and +allows you tab complete commands and options. To enable it you simply +need copy the required shell script to the your shell startup file +(e.g. ``.profile`` or ``.zprofile``) by running the special ``completion`` +command, e.g. for bash:: + + $ pip completion --bash >> ~/.profile + +And for zsh:: + + $ pip completion --zsh >> ~/.zprofile + +Alternatively, you can use the result of the ``completion`` command +directly with the eval function of you shell, e.g. by adding:: + + eval "`pip completion --bash`" + +to your startup file. diff --git a/docs/requirement-format.txt b/docs/requirements.txt index d326ea63e..326937da6 100644 --- a/docs/requirement-format.txt +++ b/docs/requirements.txt @@ -1,5 +1,92 @@ -The requirements file format -============================ +.. _`requirements-files`: + +================== +Requirements files +================== + +When installing software, and Python packages in particular, it's common that +you get a lot of libraries installed. You just did ``easy_install MyPackage`` +and you get a dozen packages. Each of these packages has its own version. + +Maybe you ran that installation and it works. Great! Will it keep working? +Did you have to provide special options to get it to find everything? Did you +have to install a bunch of other optional pieces? Most of all, will you be able +to do it again? Requirements files give you a way to create an *environment*: +a *set* of packages that work together. + +If you've ever tried to setup an application on a new system, or with slightly +updated pieces, and had it fail, pip requirements are for you. If you +haven't had this problem then you will eventually, so pip requirements are +for you too -- requirements make explicit, repeatable installation of packages. + +So what are requirements files? They are very simple: lists of packages to +install. Instead of running something like ``pip install MyApp`` and +getting whatever libraries come along, you can create a requirements file +something like:: + + MyApp + Framework==0.9.4 + Library>=0.2 + +If you save this in ``requirements.txt``, then you can ``pip install -r +requirements.txt``. Regardless of what MyApp lists in ``setup.py``, you'll +get a specific version of Framework (0.9.4) and at least the 0.2 version of +Library. (You might think you could list these specific versions in MyApp's +``setup.py`` -- but if you do that you'll have to edit MyApp if you want to +try a new version of Framework, or release a new version of MyApp if you +determine that Library 0.3 doesn't work with your application.) You can also +add optional libraries and support tools that MyApp doesn't strictly +require, giving people a set of recommended libraries. + +You can also include "editable" packages -- packages that are checked out from +Subversion, Git, Mercurial and Bazaar. These are just like using the ``-e`` +option to pip. They look like:: + + -e svn+http://myrepo/svn/MyApp#egg=MyApp + +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. + +Requirement files are mostly *flat*. Maybe ``MyApp`` requires +``Framework``, and ``Framework`` requires ``Library``. I encourage +you to still list all these in a single requirement file; it is the +nature of Python programs that there are implicit bindings *directly* +between MyApp and Library. For instance, Framework might expose one +of Library's objects, and so if Library is updated it might directly +break MyApp. If that happens you can update the requirements file to +force an earlier version of Library, and you can do that without +having to re-release MyApp at all. + +Read the `requirements file format`_ to learn about other features. + +Freezing Requirements +===================== + +So you have a working set of packages, and you want to be able to install them +elsewhere. `Requirements files`_ let you install exact versions, but it won't +tell you what all the exact versions are. + +To create a new requirements file from a known working environment, use:: + + $ pip freeze > stable-req.txt + +This will write a listing of *all* installed libraries to ``stable-req.txt`` +with exact versions for every library. You may want to edit the file down after +generating (e.g., to eliminate unnecessary libraries), but it'll give you a +stable starting point for constructing your requirements file. + +You can also give it an existing requirements file, and it will use that as a +sort of template for the new file. So if you do:: + + $ pip freeze -r devel-req.txt > stable-req.txt + +it will keep the packages listed in ``devel-req.txt`` in order and preserve +comments. + +The _`requirements file format` +=============================== The requirements file is a way to get pip to install specific packages to make up an *environment*. This document describes that format. To @@ -11,9 +98,15 @@ installed. For example:: MyPackage==3.0 -tells pip to install the 3.0 version of MyPackage. +tells pip to install the 3.0 version of MyPackage. + +You can also request `extras`_ in the requirements file:: + + MyPackage==3.0 [PDF] + +.. _extras: http://peak.telecommunity.com/DevCenter/setuptools#declaring-extras-optional-features-with-their-own-dependencies -You can also install a package in an "editable" form. This puts the +Packages may also be installed in an "editable" form. This puts the source code into ``src/distname`` (making the name lower case) and runs ``python setup.py develop`` on the package. To indicate editable, use ``-e``, like:: @@ -76,7 +169,7 @@ Pip currently supports cloning over ``git``, ``git+http`` and ``git+ssh``:: -e git+http://git.myproject.org/MyProject/#egg=MyProject -e git+ssh://git@myproject.org/MyProject/#egg=MyProject -Passing branch names, a commit hash or a tag name is also possible:: +Passing branch names, a commit hash or a tag name is also possible:: -e git://git.myproject.org/MyProject.git@master#egg=MyProject -e git://git.myproject.org/MyProject.git@v1.0#egg=MyProject @@ -104,12 +197,13 @@ Bazaar ~~~~~~ Pip supports Bazaar using the ``bzr+http``, ``bzr+https``, ``bzr+ssh``, -``bzr+sftp`` and ``bzr+ftp`` schemes:: +``bzr+sftp``, ``bzr+ftp`` and ``bzr+lp`` schemes:: -e bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject -e bzr+sftp://user@myproject.org/MyProject/trunk/#egg=MyProject -e bzr+ssh://user@myproject.org/MyProject/trunk/#egg=MyProject -e bzr+ftp://user@myproject.org/MyProject/trunk/#egg=MyProject + -e bzr+lp:MyProject#egg=MyProject Tags or revisions can be installed like this:: diff --git a/docs/running-tests.txt b/docs/running-tests.txt deleted file mode 100644 index bfed34b3b..000000000 --- a/docs/running-tests.txt +++ /dev/null @@ -1,61 +0,0 @@ -================= -Running the Tests -================= - -System Requirements -=================== - -Pip uses some system tools - VCS related tools - in its tests, so you need to -intall them (Linux):: - - sudo apt-get install subversion bzr git-core mercurial - -Or downloading and installing `Subversion -<http://subversion.apache.org/packages.html>`_, `Bazaar -<http://wiki.bazaar.canonical.com/Download>`_, `Git -<http://git-scm.com/download>`_ and `Mercurial -<http://mercurial.selenic.com/downloads/>`_ manually. - - -How To Run Tests -================ - -After all requirements (system and python) are installed, -just run the following command:: - - $ python setup.py test - -Running tests directly with Nose --------------------------------- - -If you want to run only a selection of the tests, you'll need to run them -directly with nose instead. Create a virtualenv, and install required -packages:: - - pip install nose virtualenv scripttest mock - -Run nosetests:: - - nosetests - -Or select just a single test to run:: - - cd tests; nosetests test_upgrade.py:test_uninstall_rollback - - -Troubleshooting -=============== - -Locale Warnings ---------------- - -There was a problem with locales configuration when running tests in a Hudson -CI Server that broke some tests. The problem was not with pip, but with -`locales` configuration. Hudson was not setting LANG environment variable -correctly, so the solution to fix it was changing default language to -en_US.UTF-8. -The following has been done in a Ubuntu Server 9.10 machine:: - - $ sudo locale-gen en_US en_US.UTF-8 - $ sudo dpkg-reconfigure locales - $ sudo update-locale LANG=en_US.UTF-8 diff --git a/docs/usage.txt b/docs/usage.txt new file mode 100644 index 000000000..991d0d54e --- /dev/null +++ b/docs/usage.txt @@ -0,0 +1,160 @@ +===== +Usage +===== + +Install packages +---------------- + +The simplest way to install a package is by specifying its name:: + + $ pip install SomePackage + +`SomePackage` is downloaded from :term:`PyPI`, along with its +dependencies, and installed. + +If `SomePackage` is already installed, and you need a newer version, use ``pip +install --upgrade SomePackage``. You can also request a specific version (``pip +install SomePackage==1.0.4``) and specify `setuptools extras`_ (``pip install +SomePackage[PDF]``). + +You can also install from a particular source distribution file, either +local or remote:: + + $ pip install ./downloads/SomePackage-1.0.4.tar.gz + $ pip install http://my.package.repo/SomePackage-1.0.4.zip + +.. _setuptools extras: http://peak.telecommunity.com/DevCenter/setuptools#declaring-extras-optional-features-with-their-own-dependencies + + +Edit mode +********* + +Packages normally_ install under ``site-packages``, but when you're +making changes, it makes more sense to run the package straight from the +checked-out source tree. "Editable" installs create a ``.pth`` file in +``site-packages`` that extends Python's import path to find the +package:: + + $ pip install -e path/to/SomePackage + +.. _normally: http://docs.python.org/install/index.html#how-installation-works + + +Version control systems +*********************** + +Pip knows how to check out a package from version control. `Subversion`, +`Git`, `Mercurial` and `Bazaar` are supported. The repository will be +checked out in a temporary folder, installed, and cleaned up:: + + $ pip install git+https://github.com/simplejson/simplejson.git + $ pip install svn+svn://svn.zope.org/repos/main/zope.interface/trunk/ + +This can be combined with the `-e` flag, and Pip will perform the +checkout in ``./src/``. You need to supply a name for the checkout +folder by appending a hash to the repository URL:: + + $ pip install -e git+https://github.com/lakshmivyas/hyde.git#egg=hyde + +Note that only basic checking-out of a repo is supported; pip will not +handle advanced VCS-specific features such as submodules or subrepos. + + +Alternate package repositories +****************************** + +pip searches in :term:`PyPI` by default, but this can be overridden using the +``--index-url`` option:: + + $ pip install --index-url http://d.pypi.python.org/simple/ SomePackage + +If you have your own package index with a few additional packages, you may want +to to specify additional index URLs while still also using :term:`PyPI`:: + + $ pip install --extra-index-url http://my.package.repo/ SomePackage + +A "package index" used with ``--index-url`` or ``--extra-index-url`` can be as +simple as a static-web-served directory, with automatic indexes on, with a +subdirectory per package and sdists (tarballs created with ``python setup.py +sdist``) in that directory:: + + mypackage/ + mypackage-0.7.8.tar.gz + mypackage-1.0.1.tar.gz + otherpackage/ + otherpackage-2.3.5.tar.gz + +If the number of packages in the index is small, it's even simpler to skip the +subdirectories: put all of the sdists in a single directory and use pip's +``--find-links`` option with a URL to that directory:: + + mypackage-0.7.8.tar.gz + mypackage-1.0.1.tar.gz + otherpackage-2.3.5.tar.gz + +``--find-links`` also supports local paths, so installation need not require a +network connection. + +Like ``--extra-index-url``, ``--find-links`` is additive by default, it does +not replace or supersede the index. All package sources are checked, and the +latest qualifying version for every requested package is used. If you want only +your ``-find-links`` URL used as package source, you need to pair it with +``--no-index``. + +``--index-url``, ``--extra-index-url`` and ``--find-links`` can all be used +within a :ref:`requirements file <requirements-files>` in addition to on the +command line directly. + + +Uninstall packages +------------------ + +pip is able to uninstall most installed packages with ``pip uninstall +package-name``. + +Known exceptions include pure-distutils packages installed with +``python setup.py install`` (such packages leave behind no metadata allowing +determination of what files were installed), and script wrappers installed +by develop-installs (``python setup.py develop``). + +pip also performs an automatic uninstall of an old version of a package +before upgrading to a newer version, so outdated files (and egg-info data) +from conflicting versions aren't left hanging around to cause trouble. The +old version of the package is automatically restored if the new version +fails to download or install. + + +Searching for packages +---------------------- + +pip can search :term:`PyPI` for packages using the ``pip search`` +command:: + + $ pip search "query" + +The query will be used to search the names and summaries of all +packages. With the ``--index`` option you can search in a different +repository. + + +Bundles +------- + +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 +source for your package, and you can have pip install them all together. +Once you have the bundle file further network access won't be necessary. To +build a bundle file, do:: + + $ pip bundle MyApp.pybundle MyApp + +(Using a :ref:`requirements file <requirements-files>` would be wise.) Then +someone else can get the file ``MyApp.pybundle`` and run:: + + $ pip install MyApp.pybundle + +This is *not* a binary format. This only packages source. If you have binary +packages, then the person who installs the files will have to have a compiler, +any necessary headers installed, etc. Binary packages are hard, this is +relatively easy. diff --git a/pip/__init__.py b/pip/__init__.py index 6622f9c22..9580790a2 100755 --- a/pip/__init__.py +++ b/pip/__init__.py @@ -7,7 +7,7 @@ import sys import re import difflib -from pip.backwardcompat import u, walk_packages, console_to_str +from pip.backwardcompat import walk_packages, console_to_str from pip.basecommand import command_dict, load_command, load_all_commands, command_names from pip.baseparser import parser from pip.exceptions import InstallationError @@ -113,7 +113,8 @@ def main(initial_args=None): parser.error('No command by the name %(script)s %(arg)s\n ' '(maybe you meant "%(script)s %(guess)s")' % error_dict) command = command_dict[command] - return command.main(initial_args, args[1:], options) + return command.main(args[1:], options) + def bootstrap(): """ @@ -251,12 +252,12 @@ def call_subprocess(cmd, show_stdout=True, logger.notify('Complete output from command %s:' % command_desc) logger.notify('\n'.join(all_output) + '\n----------------------------------------') raise InstallationError( - "Command %s failed with error code %s" - % (command_desc, proc.returncode)) + "Command %s failed with error code %s in %s" + % (command_desc, proc.returncode, cwd)) else: logger.warn( - "Command %s had error code %s" - % (command_desc, proc.returncode)) + "Command %s had error code %s in %s" + % (command_desc, proc.returncode, cwd)) if stdout is not None: return ''.join(all_output) diff --git a/pip/backwardcompat.py b/pip/backwardcompat.py index 4e14d1a4f..e33da9896 100644 --- a/pip/backwardcompat.py +++ b/pip/backwardcompat.py @@ -37,7 +37,7 @@ except NameError: console_encoding = sys.__stdout__.encoding if sys.version_info >= (3,): - from io import StringIO + from io import StringIO, BytesIO from functools import reduce from urllib.error import URLError, HTTPError from queue import Queue, Empty @@ -50,14 +50,25 @@ if sys.version_info >= (3,): import xmlrpc.client as xmlrpclib import urllib.parse as urlparse import http.client as httplib + def cmp(a, b): return (a > b) - (a < b) + def b(s): return s.encode('utf-8') + def u(s): return s.decode('utf-8') + def console_to_str(s): - return s.decode(console_encoding) + try: + return s.decode(console_encoding) + except UnicodeDecodeError: + return s.decode('utf_8') + + def fwrite(f, s): + f.buffer.write(b(s)) + bytes = bytes string_types = (str,) raw_input = input @@ -73,17 +84,25 @@ else: import ConfigParser import xmlrpclib import httplib + def b(s): return s + def u(s): return s + def console_to_str(s): return s + + def fwrite(f, s): + f.write(s) + bytes = str string_types = (basestring,) reduce = reduce cmp = cmp raw_input = raw_input + BytesIO = StringIO try: from email.parser import FeedParser @@ -93,6 +112,7 @@ except ImportError: from distutils.sysconfig import get_python_lib, get_python_version + def copytree(src, dst): if sys.version_info < (2, 5): before_last_dir = os.path.dirname(dst) diff --git a/pip/basecommand.py b/pip/basecommand.py index 153ee52be..12bcd6211 100644 --- a/pip/basecommand.py +++ b/pip/basecommand.py @@ -10,9 +10,11 @@ from pip import commands from pip.log import logger from pip.baseparser import parser, ConfigOptionParser, UpdatingDefaultsHelpFormatter from pip.download import urlopen -from pip.exceptions import BadCommand, InstallationError, UninstallationError -from pip.venv import restart_in_venv -from pip.backwardcompat import StringIO, urllib, urllib2, walk_packages +from pip.exceptions import (BadCommand, InstallationError, UninstallationError, + CommandError) +from pip.backwardcompat import StringIO, walk_packages +from pip.status_codes import SUCCESS, ERROR, UNKNOWN_ERROR, VIRTUALENV_NOT_FOUND + __all__ = ['command_dict', 'Command', 'load_all_commands', 'load_command', 'command_names'] @@ -45,10 +47,11 @@ class Command(object): def merge_options(self, initial_options, options): # Make sure we have all global options carried over - for attr in ['log', 'venv', 'proxy', 'venv_base', 'require_venv', - 'respect_venv', 'log_explicit_levels', 'log_file', - 'timeout', 'default_vcs', 'skip_requirements_regex', - 'no_input']: + for attr in ['log', 'proxy', 'require_venv', + 'log_explicit_levels', 'log_file', + 'timeout', 'default_vcs', + 'skip_requirements_regex', + 'no_input', 'exists_action']: setattr(options, attr, getattr(initial_options, attr) or getattr(options, attr)) options.quiet += initial_options.quiet options.verbose += initial_options.verbose @@ -56,7 +59,7 @@ class Command(object): def setup_logging(self): pass - def main(self, complete_args, args, initial_options): + def main(self, args, initial_options): options, args = self.parser.parse_args(args) self.merge_options(initial_options, options) @@ -73,44 +76,18 @@ class Command(object): self.setup_logging() - if options.require_venv and not options.venv: + if options.no_input: + os.environ['PIP_NO_INPUT'] = '1' + + if options.exists_action: + os.environ['PIP_EXISTS_ACTION'] = ''.join(options.exists_action) + + if options.require_venv: # If a venv is required check if it can really be found if not os.environ.get('VIRTUAL_ENV'): logger.fatal('Could not find an activated virtualenv (required).') - sys.exit(3) - # Automatically install in currently activated venv if required - options.respect_venv = True - - if args and args[-1] == '___VENV_RESTART___': - ## FIXME: We don't do anything this this value yet: - args = args[:-2] - options.venv = None - else: - # If given the option to respect the activated environment - # check if no venv is given as a command line parameter - if options.respect_venv and os.environ.get('VIRTUAL_ENV'): - if options.venv and os.path.exists(options.venv): - # Make sure command line venv and environmental are the same - if (os.path.realpath(os.path.expanduser(options.venv)) != - os.path.realpath(os.environ.get('VIRTUAL_ENV'))): - logger.fatal("Given virtualenv (%s) doesn't match " - "currently activated virtualenv (%s)." - % (options.venv, os.environ.get('VIRTUAL_ENV'))) - sys.exit(3) - else: - options.venv = os.environ.get('VIRTUAL_ENV') - logger.info('Using already activated environment %s' % options.venv) - if options.venv: - logger.info('Running in environment %s' % options.venv) - site_packages=False - if options.site_packages: - site_packages=True - restart_in_venv(options.venv, options.venv_base, site_packages, - complete_args) - # restart_in_venv should actually never return, but for clarity... - return - - ## FIXME: not sure if this sure come before or after venv restart + sys.exit(VIRTUALENV_NOT_FOUND) + if options.log: log_fp = open_logfile(options.log, 'a') logger.consumers.append((logger.DEBUG, log_fp)) @@ -121,30 +98,43 @@ class Command(object): urlopen.setup(proxystr=options.proxy, prompting=not options.no_input) - exit = 0 + exit = SUCCESS + store_log = False try: - self.run(options, args) + status = self.run(options, args) + # FIXME: all commands should return an exit status + # and when it is done, isinstance is not needed anymore + if isinstance(status, int): + exit = status except (InstallationError, UninstallationError): e = sys.exc_info()[1] logger.fatal(str(e)) logger.info('Exception information:\n%s' % format_exc()) - exit = 1 + store_log = True + exit = ERROR except BadCommand: e = sys.exc_info()[1] logger.fatal(str(e)) logger.info('Exception information:\n%s' % format_exc()) - exit = 1 + store_log = True + exit = ERROR + except CommandError: + e = sys.exc_info()[1] + logger.fatal('ERROR: %s' % e) + logger.info('Exception information:\n%s' % format_exc()) + exit = ERROR except KeyboardInterrupt: logger.fatal('Operation cancelled by user') logger.info('Exception information:\n%s' % format_exc()) - exit = 1 + store_log = True + exit = ERROR except: logger.fatal('Exception:\n%s' % format_exc()) - exit = 2 - + store_log = True + exit = UNKNOWN_ERROR if log_fp is not None: log_fp.close() - if exit: + if store_log: log_fn = options.log_file text = '\n'.join(complete_log) logger.fatal('Storing complete log in %s' % log_fn) @@ -154,8 +144,6 @@ class Command(object): return exit - - def format_exc(exc_info=None): if exc_info is None: exc_info = sys.exc_info() diff --git a/pip/baseparser.py b/pip/baseparser.py index 5e4898530..b3864f3da 100644 --- a/pip/baseparser.py +++ b/pip/baseparser.py @@ -46,14 +46,11 @@ class ConfigOptionParser(optparse.OptionParser): config = {} # 1. config files for section in ('global', self.name): - config.update(dict(self.get_config_section(section))) + config.update(self.normalize_keys(self.get_config_section(section))) # 2. environmental variables - config.update(dict(self.get_environ_vars())) + config.update(self.normalize_keys(self.get_environ_vars())) # Then set the options with those values for key, val in config.items(): - key = key.replace('_', '-') - if not key.startswith('--'): - key = '--%s' % key # only prefer long opts option = self.get_option(key) if option is not None: # ignore empty values @@ -75,6 +72,18 @@ class ConfigOptionParser(optparse.OptionParser): defaults[option.dest] = val return defaults + def normalize_keys(self, items): + """Return a config dictionary with normalized keys regardless of + whether the keys were specified in environment variables or in config + files""" + normalized = {} + for key, val in items: + key = key.replace('_', '-') + if not key.startswith('--'): + key = '--%s' % key # only prefer long opts + normalized[key] = val + return normalized + def get_config_section(self, name): """Get a section of a configuration""" if self.config.has_section(name): @@ -123,41 +132,12 @@ parser.add_option( action='store_true', help='Show help') parser.add_option( - '-E', '--environment', - dest='venv', - metavar='DIR', - help='virtualenv environment to run pip in (either give the ' - 'interpreter or the environment base directory)') -parser.add_option( - '-s', '--enable-site-packages', - dest='site_packages', - action='store_true', - help='Include site-packages in virtualenv if one is to be ' - 'created. Ignored if --environment is not used or ' - 'the virtualenv already exists.') -parser.add_option( - # Defines a default root directory for virtualenvs, relative - # virtualenvs names/paths are considered relative to it. - '--virtualenv-base', - dest='venv_base', - type='str', - default='', - help=optparse.SUPPRESS_HELP) -parser.add_option( # Run only if inside a virtualenv, bail if not. '--require-virtualenv', '--require-venv', dest='require_venv', action='store_true', default=False, help=optparse.SUPPRESS_HELP) -parser.add_option( - # Use automatically an activated virtualenv instead of installing - # globally. -E will be ignored if used. - '--respect-virtualenv', '--respect-venv', - dest='respect_venv', - action='store_true', - default=False, - help=optparse.SUPPRESS_HELP) parser.add_option( '-v', '--verbose', @@ -229,4 +209,18 @@ parser.add_option( default='', help=optparse.SUPPRESS_HELP) +parser.add_option( + # Option when path already exist + '--exists-action', + dest='exists_action', + type='choice', + 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 " + "another action if a certain option is not " + "available, choices: " + "(s)witch, (i)gnore, (w)ipe, (b)ackup") + parser.disable_interspersed_args() diff --git a/pip/commands/bundle.py b/pip/commands/bundle.py index fb0f75704..f782f1bc3 100644 --- a/pip/commands/bundle.py +++ b/pip/commands/bundle.py @@ -13,14 +13,19 @@ class BundleCommand(InstallCommand): def __init__(self): super(BundleCommand, self).__init__() + # bundle uses different default source and build dirs + build_opt = self.parser.get_option("--build") + build_opt.default = backup_dir(build_prefix, '-bundle') + src_opt = self.parser.get_option("--src") + src_opt.default = backup_dir(src_prefix, '-bundle') + self.parser.set_defaults(**{ + src_opt.dest: src_opt.default, + build_opt.dest: build_opt.default, + }) def run(self, options, args): if not args: raise InstallationError('You must give a bundle filename') - if not options.build_dir: - options.build_dir = backup_dir(build_prefix, '-bundle') - if not options.src_dir: - options.src_dir = backup_dir(src_prefix, '-bundle') # We have to get everything when creating a bundle: options.ignore_installed = True logger.notify('Putting temporary build files in %s and source/develop files in %s' diff --git a/pip/commands/freeze.py b/pip/commands/freeze.py index 01b5df934..03ac80f55 100644 --- a/pip/commands/freeze.py +++ b/pip/commands/freeze.py @@ -85,7 +85,9 @@ class FreezeCommand(Command): elif (line.startswith('-r') or line.startswith('--requirement') or line.startswith('-Z') or line.startswith('--always-unzip') or line.startswith('-f') or line.startswith('-i') - or line.startswith('--extra-index-url')): + or line.startswith('--extra-index-url') + or line.startswith('--find-links') + or line.startswith('--index-url')): f.write(line) continue else: diff --git a/pip/commands/help.py b/pip/commands/help.py index b5b31e702..4d504c521 100644 --- a/pip/commands/help.py +++ b/pip/commands/help.py @@ -1,5 +1,7 @@ -from pip.basecommand import Command, command_dict, load_all_commands -from pip.exceptions import InstallationError +from pip.basecommand import (Command, command_dict, + load_all_commands, SUCCESS, + ERROR) +from pip.exceptions import CommandError from pip.baseparser import parser @@ -14,10 +16,10 @@ class HelpCommand(Command): ## FIXME: handle errors better here command = args[0] if command not in command_dict: - raise InstallationError('No command with the name: %s' % command) + raise CommandError('No command with the name: %s' % command) command = command_dict[command] command.parser.print_help() - return + return SUCCESS parser.print_help() print('\nCommands available:') commands = list(set(command_dict.values())) @@ -26,6 +28,6 @@ class HelpCommand(Command): if command.hidden: continue print(' %s: %s' % (command.name, command.summary)) - + return SUCCESS HelpCommand() diff --git a/pip/commands/install.py b/pip/commands/install.py index 861c332bf..925d57feb 100644 --- a/pip/commands/install.py +++ b/pip/commands/install.py @@ -1,11 +1,14 @@ -import os, sys +import os +import sys +import tempfile +import shutil from pip.req import InstallRequirement, RequirementSet from pip.req import parse_requirements from pip.log import logger from pip.locations import build_prefix, src_prefix from pip.basecommand import Command from pip.index import PackageFinder -from pip.exceptions import InstallationError +from pip.exceptions import InstallationError, CommandError class InstallCommand(Command): @@ -79,8 +82,14 @@ class InstallCommand(Command): '-b', '--build', '--build-dir', '--build-directory', dest='build_dir', metavar='DIR', + default=build_prefix, + help='Unpack packages into DIR (default %default) and build from there') + self.parser.add_option( + '-t', '--target', + dest='target_dir', + metavar='DIR', default=None, - help='Unpack packages into DIR (default %s) and build from there' % build_prefix) + help='Install packages into DIR.') self.parser.add_option( '-d', '--download', '--download-dir', '--download-directory', dest='download_dir', @@ -97,8 +106,8 @@ class InstallCommand(Command): '--src', '--source', '--source-dir', '--source-directory', dest='src_dir', metavar='DIR', - default=None, - help='Check out --editable packages into DIR (default %s)' % src_prefix) + default=src_prefix, + help='Check out --editable packages into DIR (default %default)') self.parser.add_option( '-U', '--upgrade', @@ -106,6 +115,12 @@ class InstallCommand(Command): action='store_true', help='Upgrade all packages to the newest available version') self.parser.add_option( + '--force-reinstall', + dest='force_reinstall', + action='store_true', + help='When upgrading, reinstall all packages even if they are ' + 'already up-to-date.') + self.parser.add_option( '-I', '--ignore-installed', dest='ignore_installed', action='store_true', @@ -162,10 +177,6 @@ class InstallCommand(Command): mirrors=options.mirrors) def run(self, options, args): - if not options.build_dir: - options.build_dir = build_prefix - if not options.src_dir: - options.src_dir = src_prefix if options.download_dir: options.no_install = True options.ignore_installed = True @@ -174,6 +185,13 @@ class InstallCommand(Command): install_options = options.install_options or [] if options.use_user_site: install_options.append('--user') + if options.target_dir: + options.ignore_installed = True + temp_target_dir = tempfile.mkdtemp() + options.target_dir = os.path.abspath(options.target_dir) + if os.path.exists(options.target_dir) and not os.path.isdir(options.target_dir): + raise CommandError("Target path exists but is not a directory, will not continue.") + install_options.append('--home=' + temp_target_dir) global_options = options.global_options or [] index_urls = [options.index_url] + options.extra_index_urls if options.no_index: @@ -189,7 +207,8 @@ class InstallCommand(Command): download_cache=options.download_cache, upgrade=options.upgrade, ignore_installed=options.ignore_installed, - ignore_dependencies=options.ignore_dependencies) + ignore_dependencies=options.ignore_dependencies, + force_reinstall=options.force_reinstall) for name in args: requirement_set.add_requirement( InstallRequirement.from_line(name, None)) @@ -199,14 +218,17 @@ class InstallCommand(Command): for filename in options.requirements: for req in parse_requirements(filename, finder=finder, options=options): requirement_set.add_requirement(req) - if not requirement_set.has_requirements: + opts = {'name': self.name} if options.find_links: - raise InstallationError('You must give at least one ' - 'requirement to %s (maybe you meant "pip install %s"?)' - % (self.name, " ".join(options.find_links))) - raise InstallationError('You must give at least one requirement ' - 'to %(name)s (see "pip help %(name)s")' % dict(name=self.name)) + msg = ('You must give at least one requirement to %(name)s ' + '(maybe you meant "pip %(name)s %(links)s"?)' % + dict(opts, links=' '.join(options.find_links))) + else: + msg = ('You must give at least one requirement ' + 'to %(name)s (see "pip help %(name)s")' % opts) + logger.warn(msg) + return if (options.use_user_site and sys.version_info < (2, 6)): @@ -239,8 +261,18 @@ class InstallCommand(Command): requirement_set.create_bundle(self.bundle_filename) logger.notify('Created bundle in %s' % self.bundle_filename) # Clean up - if not options.no_install: + if not options.no_install or options.download_dir: requirement_set.cleanup_files(bundle=self.bundle) + 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/") + for item in os.listdir(lib_dir): + shutil.move( + os.path.join(lib_dir, item), + os.path.join(options.target_dir, item) + ) + shutil.rmtree(temp_target_dir) return requirement_set diff --git a/pip/commands/search.py b/pip/commands/search.py index 1a6bf9c52..9f287e594 100644 --- a/pip/commands/search.py +++ b/pip/commands/search.py @@ -2,10 +2,12 @@ import sys import textwrap import pkg_resources import pip.download -from pip.basecommand import Command +from pip.basecommand import Command, SUCCESS from pip.util import get_terminal_size from pip.log import logger from pip.backwardcompat import xmlrpclib, reduce, cmp +from pip.exceptions import CommandError +from pip.status_codes import NO_MATCHES_FOUND from distutils.version import StrictVersion, LooseVersion @@ -25,8 +27,7 @@ class SearchCommand(Command): def run(self, options, args): if not args: - logger.warn('ERROR: Missing required argument (search query).') - return + raise CommandError('Missing required argument (search query).') query = args index_url = options.index @@ -38,6 +39,9 @@ class SearchCommand(Command): terminal_width = get_terminal_size()[0] print_results(hits, terminal_width=terminal_width) + if pypi_hits: + return SUCCESS + return NO_MATCHES_FOUND def search(self, query, index_url): pypi = xmlrpclib.ServerProxy(index_url, pip.download.xmlrpclib_transport) @@ -106,7 +110,14 @@ def compare_versions(version1, version2): return cmp(StrictVersion(version1), StrictVersion(version2)) # in case of abnormal version number, fall back to LooseVersion except ValueError: + pass + try: return cmp(LooseVersion(version1), LooseVersion(version2)) + except TypeError: + # certain LooseVersion comparions raise due to unorderable types, + # fallback to string comparison + return cmp([str(v) for v in LooseVersion(version1).version], + [str(v) for v in LooseVersion(version2).version]) def highest_version(versions): diff --git a/pip/commands/uninstall.py b/pip/commands/uninstall.py index 7effd844e..9f2b89121 100644 --- a/pip/commands/uninstall.py +++ b/pip/commands/uninstall.py @@ -2,6 +2,7 @@ from pip.req import InstallRequirement, RequirementSet, parse_requirements from pip.basecommand import Command from pip.exceptions import InstallationError + class UninstallCommand(Command): name = 'uninstall' usage = '%prog [OPTIONS] PACKAGE_NAMES ...' diff --git a/pip/download.py b/pip/download.py index 87d6ad97a..a31e5d670 100644 --- a/pip/download.py +++ b/pip/download.py @@ -9,9 +9,9 @@ import tempfile from pip.backwardcompat import (md5, copytree, xmlrpclib, urllib, urllib2, urlparse, string_types, HTTPError) from pip.exceptions import InstallationError -from pip.util import (splitext, rmtree, - format_size, display_path, backup_dir, ask, - unpack_file, create_download_cache_folder, cache_download) +from pip.util import (splitext, rmtree, format_size, display_path, + backup_dir, ask, ask_path_exists, unpack_file, + create_download_cache_folder, cache_download) from pip.vcs import vcs from pip.log import logger @@ -64,6 +64,7 @@ def get_file_content(url, comes_from=None): _scheme_re = re.compile(r'^(http|https|file):', re.I) _url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I) + class URLOpener(object): """ pip's own URL helper that adds HTTP auth and proxy support @@ -131,7 +132,7 @@ class URLOpener(object): self.prompting = prompting proxy = self.get_proxy(proxystr) if proxy: - proxy_support = urllib2.ProxyHandler({"http": proxy, "ftp": proxy}) + proxy_support = urllib2.ProxyHandler({"http": proxy, "ftp": proxy, "https": proxy}) opener = urllib2.build_opener(proxy_support, urllib2.CacheFTPHandler) urllib2.install_opener(opener) @@ -387,8 +388,9 @@ def _copy_file(filename, location, content_type, link): copy = True download_location = os.path.join(location, link.filename) if os.path.exists(download_location): - response = ask('The file %s exists. (i)gnore, (w)ipe, (b)ackup ' - % display_path(download_location), ('i', 'w', 'b')) + response = ask_path_exists( + 'The file %s exists. (i)gnore, (w)ipe, (b)ackup ' % + display_path(download_location), ('i', 'w', 'b')) if response == 'i': copy = False elif response == 'w': @@ -405,7 +407,7 @@ def _copy_file(filename, location, content_type, link): logger.notify('Saved %s' % display_path(download_location)) -def unpack_http_url(link, location, download_cache, only_download): +def unpack_http_url(link, location, download_cache, download_dir=None): temp_dir = tempfile.mkdtemp('-unpack', 'pip-') target_url = link.url.split('#', 1)[0] target_file = None @@ -449,10 +451,9 @@ def unpack_http_url(link, location, download_cache, only_download): download_hash = _download_url(resp, link, temp_location) if link.md5_hash: _check_md5(download_hash, link) - if only_download: - _copy_file(temp_location, location, content_type, link) - else: - unpack_file(temp_location, location, content_type, link) + if download_dir: + _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: diff --git a/pip/exceptions.py b/pip/exceptions.py index 1ad1a616d..22f554a76 100644 --- a/pip/exceptions.py +++ b/pip/exceptions.py @@ -13,5 +13,15 @@ class DistributionNotFound(InstallationError): """Raised when a distribution cannot be found to satisfy a requirement""" +class BestVersionAlreadyInstalled(Exception): + """Raised when the most up-to-date version of a package is already + installed. + """ + + class BadCommand(Exception): """Raised when virtualenv or a command is not found""" + + +class CommandError(Exception): + """Raised when there is an error in command-line arguments""" diff --git a/pip/index.py b/pip/index.py index 1b8a52b4f..8e53e44b7 100644 --- a/pip/index.py +++ b/pip/index.py @@ -3,18 +3,23 @@ import sys import os import re +import gzip import mimetypes -import threading +try: + import threading +except ImportError: + import dummy_threading as threading import posixpath import pkg_resources import random import socket import string +import zlib from pip.log import logger from pip.util import Inf from pip.util import normalize_name, splitext -from pip.exceptions import DistributionNotFound -from pip.backwardcompat import (WindowsError, +from pip.exceptions import DistributionNotFound, BestVersionAlreadyInstalled +from pip.backwardcompat import (WindowsError, BytesIO, Queue, httplib, urlparse, URLError, HTTPError, u, product, url2pathname) @@ -170,6 +175,7 @@ class PackageFinder(object): if applicable_versions[0][1] is Inf: 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])) @@ -182,7 +188,7 @@ class PackageFinder(object): # 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')) - return 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]))) @@ -245,7 +251,7 @@ class PackageFinder(object): _egg_fragment_re = re.compile(r'#egg=([^&]*)') _egg_info_re = re.compile(r'([a-z0-9_.]+)-([a-z0-9_.-]+)', re.I) - _py_version_re = re.compile(r'-py([123]\.[0-9])$') + _py_version_re = re.compile(r'-py([123]\.?[0-9]?)$') def _sort_links(self, links): "Returns elements of links in order, non-egg links first, egg links second, while eliminating duplicates" @@ -291,6 +297,11 @@ class PackageFinder(object): logger.debug('Skipping link %s; unknown archive format: %s' % (link, ext)) self.logged_links.add(link) return [] + if "macosx10" in link.path and ext == '.zip': + if link not in self.logged_links: + logger.debug('Skipping link %s; macosx10 one' % (link)) + self.logged_links.add(link) + return [] version = self._egg_info_matches(egg_info, search_name, link) if version is None: logger.debug('Skipping link %s; wrong project name (not %s)' % (link, search_name)) @@ -442,7 +453,15 @@ class HTMLPage(object): real_url = geturl(resp) headers = resp.info() - inst = cls(u(resp.read()), real_url, headers) + contents = resp.read() + encoding = headers.get('Content-Encoding', None) + #XXX need to handle exceptions and add testing for this + if encoding is not None: + if encoding == 'gzip': + contents = gzip.GzipFile(fileobj=BytesIO(contents)).read() + if encoding == 'deflate': + contents = zlib.decompress(contents) + inst = cls(u(contents), real_url, headers) except (HTTPError, URLError, socket.timeout, socket.error, OSError, WindowsError): e = sys.exc_info()[1] desc = str(e) @@ -539,7 +558,7 @@ class HTMLPage(object): href_match = self._href_re.search(self.content, pos=match.end()) if not href_match: continue - url = match.group(1) or match.group(2) or match.group(3) + url = href_match.group(1) or href_match.group(2) or href_match.group(3) if not url: continue url = self.clean_link(urlparse.urljoin(self.base_url, url)) diff --git a/pip/locations.py b/pip/locations.py index b439bd375..34c6dbbe6 100644 --- a/pip/locations.py +++ b/pip/locations.py @@ -46,5 +46,7 @@ else: default_config_file = os.path.join(default_storage_dir, 'pip.conf') default_log_file = os.path.join(default_storage_dir, 'pip.log') # Forcing to use /usr/local/bin for standard Mac OS X framework installs + # Also log to ~/Library/Logs/ for use with the Console.app log viewer if sys.platform[:6] == 'darwin' and sys.prefix[:16] == '/System/Library/': bin_py = '/usr/local/bin' + default_log_file = os.path.join(user_dir, 'Library/Logs/pip.log') diff --git a/pip/log.py b/pip/log.py index 56196b787..63541a1c1 100644 --- a/pip/log.py +++ b/pip/log.py @@ -4,6 +4,8 @@ import sys import logging +import pip.backwardcompat + class Logger(object): @@ -71,7 +73,8 @@ class Logger(object): ## FIXME: should this be a name, not a level number? rendered = '%02i %s' % (level, rendered) if hasattr(consumer, 'write'): - consumer.write(rendered+'\n') + rendered += '\n' + pip.backwardcompat.fwrite(consumer, rendered) else: consumer(rendered) diff --git a/pip/req.py b/pip/req.py index 7110abc93..ff423dfa2 100644 --- a/pip/req.py +++ b/pip/req.py @@ -6,11 +6,12 @@ import zipfile import pkg_resources import tempfile from pip.locations import bin_py, running_under_virtualenv -from pip.exceptions import InstallationError, UninstallationError +from pip.exceptions import (InstallationError, UninstallationError, + BestVersionAlreadyInstalled) from pip.vcs import vcs from pip.log import logger from pip.util import display_path, rmtree -from pip.util import ask, backup_dir +from pip.util import ask, ask_path_exists, backup_dir from pip.util import is_installable_dir, is_local, dist_is_local from pip.util import renames, normalize_path, egg_link_path from pip.util import make_path_relative @@ -34,8 +35,10 @@ class InstallRequirement(object): def __init__(self, req, comes_from, source_dir=None, editable=False, url=None, update=True): + self.extras = () if isinstance(req, string_types): req = pkg_resources.Requirement.parse(req) + self.extras = req.extras self.req = req self.comes_from = comes_from self.source_dir = source_dir @@ -91,15 +94,15 @@ class InstallRequirement(object): # 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 + 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))) + # 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 + req = name return cls(req, comes_from, url=url) @@ -327,11 +330,12 @@ exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec')) def requirements(self, extras=()): in_extra = None for line in self.egg_info_lines('requires.txt'): - match = self._requirements_section_re.match(line) + match = self._requirements_section_re.match(line.lower()) if match: in_extra = match.group(1) continue if in_extra and in_extra not in extras: + logger.debug('skipping extra %s' % in_extra) # Skip requirement for an extra we aren't requiring continue yield line @@ -502,8 +506,9 @@ exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec')) archive_name = '%s-%s.zip' % (self.name, self.installed_version) archive_path = os.path.join(build_dir, archive_name) if os.path.exists(archive_path): - response = ask('The file %s exists. (i)gnore, (w)ipe, (b)ackup ' - % display_path(archive_path), ('i', 'w', 'b')) + response = ask_path_exists( + 'The file %s exists. (i)gnore, (w)ipe, (b)ackup ' % + display_path(archive_path), ('i', 'w', 'b')) if response == 'i': create_archive = False elif response == 'w': @@ -768,7 +773,7 @@ class Requirements(object): return self._dict[key] def __repr__(self): - values = [ '%s: %s' % (repr(k), repr(self[k])) for k in self.keys() ] + values = ['%s: %s' % (repr(k), repr(self[k])) for k in self.keys()] return 'Requirements({%s})' % ', '.join(values) @@ -776,13 +781,14 @@ class RequirementSet(object): def __init__(self, build_dir, src_dir, download_dir, download_cache=None, upgrade=False, ignore_installed=False, - ignore_dependencies=False): + ignore_dependencies=False, force_reinstall=False): self.build_dir = build_dir self.src_dir = src_dir self.download_dir = download_dir self.download_cache = download_cache self.upgrade = upgrade self.ignore_installed = ignore_installed + self.force_reinstall = force_reinstall self.requirements = Requirements() # Mapping of alias: real_name self.requirement_aliases = {} @@ -903,18 +909,35 @@ class RequirementSet(object): else: req_to_install = reqs.pop(0) install = True + best_installed = False if not self.ignore_installed and not req_to_install.editable: 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 - req_to_install.satisfied_by = None + if not self.force_reinstall: + try: + url = finder.find_requirement( + req_to_install, self.upgrade) + except BestVersionAlreadyInstalled: + best_installed = True + install = False + else: + # Avoid the need to call find_requirement again + req_to_install.url = url.url + + if not best_installed: + req_to_install.conflicts_with = req_to_install.satisfied_by + req_to_install.satisfied_by = None else: install = False if req_to_install.satisfied_by: - logger.notify('Requirement already satisfied ' - '(use --upgrade to upgrade): %s' - % req_to_install) + if best_installed: + logger.notify('Requirement already up-to-date: %s' + % req_to_install) + else: + logger.notify('Requirement already satisfied ' + '(use --upgrade to upgrade): %s' + % req_to_install) if req_to_install.editable: logger.notify('Obtaining %s' % req_to_install) elif install: @@ -948,6 +971,7 @@ class RequirementSet(object): 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 if not os.path.exists(os.path.join(location, 'setup.py')): ## FIXME: this won't upgrade when there's an existing package unpacked in `location` if req_to_install.url is None: @@ -970,7 +994,6 @@ class RequirementSet(object): unpack = False if unpack: is_bundle = req_to_install.is_bundle - url = None if is_bundle: req_to_install.move_bundle_files(self.build_dir, self.src_dir) for subreq in req_to_install.bundle_requirements(): @@ -978,8 +1001,8 @@ class RequirementSet(object): self.add_requirement(subreq) elif self.is_download: req_to_install.source_dir = location + req_to_install.run_egg_info() if url and url.scheme in vcs.all_schemes: - req_to_install.run_egg_info() req_to_install.archive(self.download_dir) else: req_to_install.source_dir = location @@ -1002,12 +1025,13 @@ class RequirementSet(object): req_to_install.satisfied_by = None else: install = False - if not is_bundle and not self.is_download: + if not is_bundle: ## FIXME: shouldn't be globally added: finder.add_dependency_links(req_to_install.dependency_links) - ## FIXME: add extras in here: + if (req_to_install.extras): + logger.notify("Installing extra requirements: %r" % ','.join(req_to_install.extras)) if not self.ignore_dependencies: - for req in req_to_install.requirements(): + for req in req_to_install.requirements(req_to_install.extras): try: name = pkg_resources.Requirement.parse(req).project_name except ValueError: @@ -1023,8 +1047,11 @@ 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: + self.reqs_to_cleanup.append(req_to_install) else: self.reqs_to_cleanup.append(req_to_install) + if install: self.successfully_downloaded.append(req_to_install) if bundle and (req_to_install.url and req_to_install.url.startswith('file:///')): @@ -1044,6 +1071,7 @@ class RequirementSet(object): remove_dir.append(self.build_dir) # The source dir of a bundle can always be removed. + # FIXME: not if it pre-existed the bundle! if bundle: remove_dir.append(self.src_dir) @@ -1068,20 +1096,25 @@ class RequirementSet(object): def unpack_url(self, link, location, only_download=False): if only_download: - location = self.download_dir + loc = self.download_dir + else: + loc = location if is_vcs_url(link): - return unpack_vcs_link(link, location, only_download) + return unpack_vcs_link(link, loc, only_download) elif is_file_url(link): - return unpack_file_url(link, location) + return unpack_file_url(link, loc) else: if self.download_cache: self.download_cache = os.path.expanduser(self.download_cache) - return unpack_http_url(link, location, self.download_cache, only_download) + retval = unpack_http_url(link, location, self.download_cache, self.download_dir) + if only_download: + _write_delete_marker_message(os.path.join(location, PIP_DELETE_MARKER_FILENAME)) + return retval def install(self, install_options, global_options=()): """Install everything in this set (after having downloaded and unpacked the packages)""" to_install = [r for r in self.requirements.values() - if self.upgrade or not r.satisfied_by] + if not r.satisfied_by] if to_install: logger.notify('Installing collected packages: %s' % ', '.join([req.name for req in to_install])) @@ -1222,7 +1255,7 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None): req_url = line[len('--requirement'):].strip().strip('=') if _scheme_re.search(filename): # Relative to a URL - req_url = urlparse.urljoin(req_url, filename) + req_url = urlparse.urljoin(filename, req_url) elif not _scheme_re.search(req_url): req_url = os.path.join(os.path.dirname(filename), req_url) for item in parse_requirements(req_url, finder, comes_from=filename, options=options): diff --git a/pip/status_codes.py b/pip/status_codes.py new file mode 100644 index 000000000..b6208e964 --- /dev/null +++ b/pip/status_codes.py @@ -0,0 +1,5 @@ +SUCCESS = 0 +ERROR = 1 +UNKNOWN_ERROR = 2 +VIRTUALENV_NOT_FOUND = 3 +NO_MATCHES_FOUND = 23 diff --git a/pip/util.py b/pip/util.py index 5d0ed4f88..e5ad6df17 100644 --- a/pip/util.py +++ b/pip/util.py @@ -98,10 +98,18 @@ def find_command(cmd, paths=None, pathext=None): 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' ]) + default_pathext = os.pathsep.join(['.COM', '.EXE', '.BAT', '.CMD']) pathext = os.environ.get('PATHEXT', default_pathext) return pathext + +def ask_path_exists(message, options): + for action in os.environ.get('PIP_EXISTS_ACTION', ''): + if action in options: + return action + return ask(message, options) + + def ask(message, options): """Ask the message interactively, with the given possible responses""" while 1: @@ -424,6 +432,17 @@ def untar_file(filename, location): if member.isdir(): if not os.path.exists(path): os.makedirs(path) + elif member.issym(): + try: + tar._extract_member(member, path) + except: + e = sys.exc_info()[1] + # Some corrupt tar files seem to produce this + # (specifically bad symlinks) + logger.warn( + 'In the tar file %s the member %s is invalid: %s' + % (filename, member.name, e)) + continue else: try: fp = tar.extractfile(member) diff --git a/pip/vcs/__init__.py b/pip/vcs/__init__.py index 33c9c7c5d..a2137e96a 100644 --- a/pip/vcs/__init__.py +++ b/pip/vcs/__init__.py @@ -5,7 +5,8 @@ import shutil from pip.backwardcompat import urlparse, urllib from pip.log import logger -from pip.util import display_path, backup_dir, find_command, ask, rmtree +from pip.util import (display_path, backup_dir, find_command, + ask, rmtree, ask_path_exists) __all__ = ['vcs', 'get_src_requirement'] @@ -182,27 +183,34 @@ class VersionControl(object): if os.path.exists(os.path.join(dest, self.dirname)): existing_url = self.get_url(dest) if self.compare_urls(existing_url, url): - logger.info('%s in %s exists, and has correct URL (%s)' - % (self.repo_name.title(), display_path(dest), url)) - logger.notify('Updating %s %s%s' - % (display_path(dest), self.repo_name, rev_display)) + logger.info('%s in %s exists, and has correct URL (%s)' % + (self.repo_name.title(), display_path(dest), + url)) + logger.notify('Updating %s %s%s' % + (display_path(dest), self.repo_name, + rev_display)) self.update(dest, rev_options) else: - logger.warn('%s %s in %s exists with URL %s' - % (self.name, self.repo_name, display_path(dest), existing_url)) - prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ', ('s', 'i', 'w', 'b')) + logger.warn('%s %s in %s exists with URL %s' % + (self.name, self.repo_name, + display_path(dest), existing_url)) + prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ', + ('s', 'i', 'w', 'b')) else: - logger.warn('Directory %s already exists, and is not a %s %s.' - % (dest, self.name, self.repo_name)) + logger.warn('Directory %s already exists, ' + 'and is not a %s %s.' % + (dest, self.name, self.repo_name)) prompt = ('(i)gnore, (w)ipe, (b)ackup ', ('i', 'w', 'b')) if prompt: - logger.warn('The plan is to install the %s repository %s' - % (self.name, url)) - response = ask('What to do? %s' % prompt[0], prompt[1]) + logger.warn('The plan is to install the %s repository %s' % + (self.name, url)) + response = ask_path_exists('What to do? %s' % prompt[0], + prompt[1]) if response == 's': - logger.notify('Switching %s %s to %s%s' - % (self.repo_name, display_path(dest), url, rev_display)) + logger.notify('Switching %s %s to %s%s' % + (self.repo_name, display_path(dest), url, + rev_display)) self.switch(dest, url, rev_options) elif response == 'i': # do nothing diff --git a/pip/vcs/bazaar.py b/pip/vcs/bazaar.py index 9f5389835..5d5277771 100644 --- a/pip/vcs/bazaar.py +++ b/pip/vcs/bazaar.py @@ -2,6 +2,7 @@ import os import tempfile import re from pip import call_subprocess +from pip.backwardcompat import urlparse from pip.log import logger from pip.util import rmtree, display_path from pip.vcs import vcs, VersionControl @@ -13,10 +14,15 @@ class Bazaar(VersionControl): dirname = '.bzr' repo_name = 'branch' bundle_file = 'bzr-branch.txt' - schemes = ('bzr', 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp', 'bzr+ftp') + schemes = ('bzr', 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp', 'bzr+ftp', 'bzr+lp') guide = ('# This was a Bazaar branch; to make it a branch again run:\n' 'bzr branch -r %(rev)s %(url)s .\n') + def __init__(self, url=None, *args, **kwargs): + super(Bazaar, self).__init__(url, *args, **kwargs) + urlparse.non_hierarchical.extend(['lp']) + urlparse.uses_fragment.extend(['lp']) + def parse_vcs_bundle_file(self, content): url = rev = None for line in content.splitlines(): diff --git a/pip/vcs/git.py b/pip/vcs/git.py index c159d7744..ecaf19f50 100644 --- a/pip/vcs/git.py +++ b/pip/vcs/git.py @@ -8,6 +8,7 @@ from pip.backwardcompat import url2pathname, urlparse urlsplit = urlparse.urlsplit urlunsplit = urlparse.urlunsplit + class Git(VersionControl): name = 'git' dirname = '.git' diff --git a/pip/vcs/subversion.py b/pip/vcs/subversion.py index 79c31ec0e..f54eee664 100644 --- a/pip/vcs/subversion.py +++ b/pip/vcs/subversion.py @@ -1,6 +1,7 @@ import os import re -from pip import call_subprocess +from pip.backwardcompat import urlparse +from pip import call_subprocess, InstallationError from pip.index import Link from pip.util import rmtree, display_path from pip.log import logger @@ -10,6 +11,8 @@ _svn_xml_url_re = re.compile('url="([^"]+)"') _svn_rev_re = re.compile('committed-rev="(\d+)"') _svn_url_re = re.compile(r'URL: (.+)') _svn_revision_re = re.compile(r'Revision: (.+)') +_svn_info_xml_rev_re = re.compile(r'\s*revision="(\d+)"') +_svn_info_xml_url_re = re.compile(r'<url>(.*)</url>') class Subversion(VersionControl): @@ -54,6 +57,7 @@ class Subversion(VersionControl): def export(self, location): """Export the svn repository at the url to the destination location""" url, rev = self.get_url_rev() + rev_options = get_rev_options(url, rev) logger.notify('Exporting svn repository %s to %s' % (url, location)) logger.indent += 2 try: @@ -62,7 +66,7 @@ class Subversion(VersionControl): # --force fixes this, but was only added in svn 1.5 rmtree(location) call_subprocess( - [self.cmd, 'export', url, location], + [self.cmd, 'export'] + rev_options + [url, location], filter_stdout=self._filter, show_stdout=False) finally: logger.indent -= 2 @@ -77,11 +81,10 @@ class Subversion(VersionControl): def obtain(self, dest): url, rev = self.get_url_rev() + rev_options = get_rev_options(url, rev) if rev: - rev_options = ['-r', rev] rev_display = ' (to revision %s)' % rev else: - rev_options = [] rev_display = '' if self.check_destination(dest, url, rev_options, rev_display): logger.notify('Checking out %s%s to %s' @@ -119,33 +122,12 @@ class Subversion(VersionControl): if not os.path.exists(entries_fn): ## FIXME: should we warn? continue - f = open(entries_fn) - data = f.read() - f.close() - - if data.startswith('8') or data.startswith('9') or data.startswith('10'): - data = list(map(str.splitlines, data.split('\n\x0c\n'))) - del data[0][0] # get rid of the '8' - dirurl = data[0][3] - revs = [int(d[9]) for d in data if len(d)>9 and d[9]]+[0] - if revs: - localrev = max(revs) - else: - localrev = 0 - elif data.startswith('<?xml'): - dirurl = _svn_xml_url_re.search(data).group(1) # get repository URL - revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)]+[0] - if revs: - localrev = max(revs) - else: - localrev = 0 - else: - logger.warn("Unrecognized .svn/entries format; skipping %s", base) - dirs[:] = [] - continue + + dirurl, localrev = self._get_svn_url_rev(base) + if base == location: base_url = dirurl+'/' # save the root url - elif not dirurl.startswith(base_url): + elif not dirurl or not dirurl.startswith(base_url): dirs[:] = [] continue # not part of the same svn tree, skip it revision = max(revision, localrev) @@ -170,22 +152,39 @@ class Subversion(VersionControl): logger.warn("Could not find setup.py for directory %s (tried all parent directories)" % orig_location) return None + + return self._get_svn_url_rev(location)[0] + + def _get_svn_url_rev(self, location): f = open(os.path.join(location, self.dirname, 'entries')) data = f.read() f.close() if data.startswith('8') or data.startswith('9') or data.startswith('10'): data = list(map(str.splitlines, data.split('\n\x0c\n'))) del data[0][0] # get rid of the '8' - return data[0][3] + url = data[0][3] + revs = [int(d[9]) for d in data if len(d)>9 and d[9]]+[0] elif data.startswith('<?xml'): match = _svn_xml_url_re.search(data) if not match: raise ValueError('Badly formatted data: %r' % data) - return match.group(1) # get repository URL + url = match.group(1) # get repository URL + revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)]+[0] else: - logger.warn("Unrecognized .svn/entries format in %s" % location) - # Or raise exception? - return None + try: + # subversion >= 1.7 + xml = call_subprocess([self.cmd, 'info', '--xml', location], show_stdout=False) + url = _svn_info_xml_url_re.search(xml).group(1) + revs = [int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml)] + except InstallationError: + url, revs = None, [] + + if revs: + rev = max(revs) + else: + rev = 0 + + return url, rev def get_tag_revs(self, svn_tag_url): stdout = call_subprocess( @@ -241,4 +240,33 @@ class Subversion(VersionControl): full_egg_name = '%s-dev_r%s' % (egg_project_name, rev) return 'svn+%s@%s#egg=%s' % (repo, rev, full_egg_name) + +def get_rev_options(url, rev): + if rev: + rev_options = ['-r', rev] + else: + rev_options = [] + + r = urlparse.urlsplit(url) + if hasattr(r, 'username'): + # >= Python-2.5 + username, password = r.username, r.password + else: + netloc = r[1] + if '@' in netloc: + auth = netloc.split('@')[0] + if ':' in auth: + username, password = auth.split(':', 1) + else: + username, password = auth, None + else: + username, password = None, None + + if username: + rev_options += ['--username', username] + if password: + rev_options += ['--password', password] + return rev_options + + vcs.register(Subversion) diff --git a/pip/venv.py b/pip/venv.py deleted file mode 100644 index 88de4f6de..000000000 --- a/pip/venv.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Tools for working with virtualenv environments""" - -import os -import sys -import subprocess -from pip.exceptions import BadCommand -from pip.log import logger - - -def restart_in_venv(venv, base, site_packages, args): - """ - Restart this script using the interpreter in the given virtual environment - """ - if base and not os.path.isabs(venv) and not venv.startswith('~'): - base = os.path.expanduser(base) - # ensure we have an abs basepath at this point: - # a relative one makes no sense (or does it?) - if os.path.isabs(base): - venv = os.path.join(base, venv) - - if venv.startswith('~'): - venv = os.path.expanduser(venv) - - if not os.path.exists(venv): - try: - import virtualenv - except ImportError: - print('The virtual environment does not exist: %s' % venv) - print('and virtualenv is not installed, so a new environment cannot be created') - sys.exit(3) - print('Creating new virtualenv environment in %s' % venv) - virtualenv.logger = logger - logger.indent += 2 - virtualenv.create_environment(venv, site_packages=site_packages) - if sys.platform == 'win32': - python = os.path.join(venv, 'Scripts', 'python.exe') - # check for bin directory which is used in buildouts - if not os.path.exists(python): - python = os.path.join(venv, 'bin', 'python.exe') - else: - python = os.path.join(venv, 'bin', 'python') - if not os.path.exists(python): - python = venv - if not os.path.exists(python): - raise BadCommand('Cannot find virtual environment interpreter at %s' % python) - base = os.path.dirname(os.path.dirname(python)) - file = os.path.join(os.path.dirname(__file__), 'runner.py') - if file.endswith('.pyc'): - file = file[:-1] - proc = subprocess.Popen( - [python, file] + args + [base, '___VENV_RESTART___']) - proc.wait() - sys.exit(proc.returncode) @@ -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.2" +version = "1.1" doc_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "docs") index_filename = os.path.join(doc_dir, "index.txt") @@ -51,5 +51,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.7', 'scripttest>=1.1.1', 'mock'], zip_safe=False) diff --git a/tests/packages/BrokenEmitsUTF8/broken.py b/tests/packages/BrokenEmitsUTF8/broken.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/packages/BrokenEmitsUTF8/broken.py diff --git a/tests/packages/BrokenEmitsUTF8/setup.py b/tests/packages/BrokenEmitsUTF8/setup.py new file mode 100644 index 000000000..989cc2a0b --- /dev/null +++ b/tests/packages/BrokenEmitsUTF8/setup.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from distutils.core import setup +import sys + +class FakeError(Exception): + pass + +if sys.argv[1] == 'install': + if hasattr(sys.stdout, 'buffer'): + sys.stdout.buffer.write('\nThis package prints out UTF-8 stuff like:\n'.encode('utf-8')) + sys.stdout.buffer.write('* return type of ‘main’ is not ‘int’\n'.encode('utf-8')) + sys.stdout.buffer.write('* Björk Guðmundsdóttir [ˈpjœr̥k ˈkvʏðmʏntsˌtoʊhtɪr]'.encode('utf-8')) + else: + pass + sys.stdout.write('\nThis package prints out UTF-8 stuff like:\n') + sys.stdout.write('* return type of \xe2\x80\x98main\xe2\x80\x99 is not \xe2\x80\x98int\xe2\x80\x99\n') + sys.stdout.write('* Bj\xc3\xb6rk Gu\xc3\xb0mundsd\xc3\xb3ttir [\xcb\x88pj\xc5\x93r\xcc\xa5k \xcb\x88kv\xca\x8f\xc3\xb0m\xca\x8fnts\xcb\x8cto\xca\x8aht\xc9\xaar]\n') + + raise FakeError('this package designed to fail on install') + +setup(name='broken', + version='0.2broken', + py_modules=['broken'], + ) diff --git a/tests/packages/pkgwithmpkg-1.0-py2.7-macosx10.7.mpkg.zip b/tests/packages/pkgwithmpkg-1.0-py2.7-macosx10.7.mpkg.zip new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/packages/pkgwithmpkg-1.0-py2.7-macosx10.7.mpkg.zip diff --git a/tests/packages/pkgwithmpkg-1.0.tar.gz b/tests/packages/pkgwithmpkg-1.0.tar.gz new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/packages/pkgwithmpkg-1.0.tar.gz diff --git a/tests/path.py b/tests/path.py index d564ac607..fc724487c 100644 --- a/tests/path.py +++ b/tests/path.py @@ -15,6 +15,7 @@ _base = os.path.supports_unicode_filenames and unicode or str from pip.util import rmtree + class Path(_base): """ Models a path in an object oriented way. """ @@ -32,7 +33,7 @@ class Path(_base): """ path_obj / 'bc.d' """ """ path_obj / path_obj2 """ return Path(self, path) - + __truediv__ = __div__ def __rdiv__(self, path): diff --git a/tests/test_basic.py b/tests/test_basic.py index aa1129745..8bdce9772 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -7,7 +7,7 @@ from os.path import abspath, join, curdir, pardir from nose import SkipTest from nose.tools import assert_raises -from mock import Mock, patch +from mock import patch from pip.util import rmtree, find_command from pip.exceptions import BadCommand @@ -17,6 +17,7 @@ from tests.test_pip import (here, reset_env, run_pip, pyversion, mkdir, from tests.local_repos import local_checkout from tests.path import Path + def test_correct_pip_version(): """ Check we are running proper version of pip in run_pip. @@ -223,10 +224,10 @@ def test_install_editable_from_git(): reset_env() args = ['install'] args.extend(['-e', - '%s#egg=django-feedutil' % - local_checkout('git+http://github.com/jezdez/django-feedutil.git')]) + '%s#egg=pip-test-package' % + local_checkout('git+http://github.com/pypa/pip-test-package.git')]) result = run_pip(*args, **{"expect_error": True}) - result.assert_installed('django-feedutil', with_files=['.git']) + result.assert_installed('pip-test-package', with_files=['.git']) def test_install_editable_from_hg(): @@ -377,7 +378,7 @@ def test_install_subversion_usersite_editable_with_setuptools_fails(): # We don't try to use setuptools for 3.X. elif sys.version_info >= (3,): raise SkipTest() - env = reset_env() + env = reset_env(use_distribute=False) no_site_packages = env.lib_path/'no-global-site-packages.txt' if os.path.isfile(no_site_packages): no_site_packages.rm() # this re-enables user_site @@ -388,6 +389,7 @@ def test_install_subversion_usersite_editable_with_setuptools_fails(): expect_error=True) assert '--user --editable not supported with setuptools, use distribute' in result.stdout + def test_install_pardir(): """ Test installing parent directory ('..'). @@ -419,6 +421,7 @@ def test_install_with_pax_header(): run_from = abspath(join(here, 'packages')) run_pip('install', 'paxpkg.tar.bz2', cwd=run_from) + def test_install_using_install_option_and_editable(): """ Test installing a tool using -e and --install-option @@ -502,6 +505,7 @@ def test_install_folder_using_relative_path(): egg_folder = env.site_packages / 'mock-100.1-py%s.egg-info' % pyversion assert egg_folder in result.files_created, str(result) + def test_install_package_which_contains_dev_in_name(): """ Test installing package from pypi which contains 'dev' in name @@ -513,6 +517,17 @@ def test_install_package_which_contains_dev_in_name(): assert devserver_folder in result.files_created, str(result.stdout) assert egg_info_folder in result.files_created, str(result) + +def test_install_package_with_target(): + """ + Test installing a package using pip install --target + """ + env = reset_env() + target_dir = env.scratch_path/'target' + result = run_pip('install', '-t', target_dir, "initools==0.1") + assert Path('scratch')/'target'/'initools' in result.files_created, str(result) + + def test_find_command_folder_in_path(): """ If a folder named e.g. 'git' is in PATH, and find_command is looking for @@ -520,13 +535,16 @@ def test_find_command_folder_in_path(): looking. """ env = reset_env() - mkdir('path_one'); path_one = env.scratch_path/'path_one' + mkdir('path_one') + path_one = env.scratch_path/'path_one' mkdir(path_one/'foo') - mkdir('path_two'); path_two = env.scratch_path/'path_two' + mkdir('path_two') + path_two = env.scratch_path/'path_two' write_file(path_two/'foo', '# nothing') 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 @@ -534,16 +552,18 @@ def test_does_not_find_command_because_there_is_no_path(): 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`") + 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('os.pathsep', ':') @patch('pip.util.get_pathext') @patch('os.path.isfile') def test_find_command_trys_all_pathext(mock_isfile, getpath_mock): @@ -552,22 +572,18 @@ def test_find_command_trys_all_pathext(mock_isfile, getpath_mock): 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 ] + paths = [os.path.join('path_one', f) for f in ['foo.com', 'foo.exe', 'foo']] + expected = [((p,),) for p in paths] + + 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" - 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('os.pathsep', ':') @patch('pip.util.get_pathext') @patch('os.path.isfile') def test_find_command_trys_supplied_pathext(mock_isfile, getpath_mock): @@ -575,19 +591,13 @@ 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 ] + 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 + 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" diff --git a/tests/test_cleanup.py b/tests/test_cleanup.py index 8dbc45129..15a050833 100644 --- a/tests/test_cleanup.py +++ b/tests/test_cleanup.py @@ -1,11 +1,11 @@ import os import textwrap from os.path import abspath, exists, join -from tests.test_pip import (here, reset_env, run_pip, write_file, mkdir, - pyversion) +from tests.test_pip import (here, reset_env, run_pip, write_file, mkdir) from tests.local_repos import local_checkout from tests.path import Path + def test_cleanup_after_install_from_pypi(): """ Test clean up after installing a package from PyPI. @@ -49,6 +49,7 @@ def test_cleanup_after_install_from_local_directory(): assert not exists(build), "unexpected build/ dir exists: %s" % build assert not exists(src), "unexpected src/ dir exist: %s" % src + def test_cleanup_after_create_bundle(): """ Test clean up after making a bundle. Make sure (build|src)-bundle/ dirs are removed but not src/. @@ -58,8 +59,8 @@ def test_cleanup_after_create_bundle(): # Install an editable to create a src/ dir. args = ['install'] args.extend(['-e', - '%s#egg=django-feedutil' % - local_checkout('git+http://github.com/jezdez/django-feedutil.git')]) + '%s#egg=pip-test-package' % + local_checkout('git+http://github.com/pypa/pip-test-package.git')]) run_pip(*args) build = env.venv_path/"build" src = env.venv_path/"src" diff --git a/tests/test_config.py b/tests/test_config.py index 027442e5d..477f868f2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -33,6 +33,41 @@ def test_command_line_options_override_env_vars(): assert "Getting page http://download.zope.org/ppix" in result.stdout +def test_env_vars_override_config_file(): + """ + Test that environmental variables override settings in config files. + + """ + fd, config_file = tempfile.mkstemp('-pip.cfg', 'test-') + try: + _test_env_vars_override_config_file(config_file) + finally: + # `os.close` is a workaround for a bug in subprocess + # http://bugs.python.org/issue3210 + os.close(fd) + os.remove(config_file) + + +def _test_env_vars_override_config_file(config_file): + environ = clear_environ(os.environ.copy()) + environ['PIP_CONFIG_FILE'] = config_file # set this to make pip load it + reset_env(environ) + # It's important that we test this particular config value ('no-index') + # because their is/was a bug which only shows up in cases in which + # 'config-item' and 'config_item' hash to the same value modulo the size + # of the config dictionary. + write_file(config_file, textwrap.dedent("""\ + [global] + no-index = 1 + """)) + result = run_pip('install', '-vvv', 'INITools', expect_error=True) + assert "DistributionNotFound: No distributions at all found for INITools" in result.stdout + environ['PIP_NO_INDEX'] = '0' + reset_env(environ) + result = run_pip('install', '-vvv', 'INITools', expect_error=True) + assert "Successfully installed INITools" in result.stdout + + def test_command_line_append_flags(): """ Test command line flags that append to defaults set by environmental variables. diff --git a/tests/test_download.py b/tests/test_download.py index b52318af5..5d4923ccd 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -1,3 +1,5 @@ +from pip.backwardcompat import any + import textwrap from tests.test_pip import reset_env, run_pip, write_file from tests.path import Path @@ -26,3 +28,16 @@ def test_single_download_from_requirements_file(): 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 + + +def test_download_should_download_dependencies(): + """ + It should download dependencies (in the scratch path) + """ + + env = reset_env() + result = run_pip('install', 'Paste[openid]==1.7.5.1', '-d', '.', expect_error=True) + assert Path('scratch')/ 'Paste-1.7.5.1.tar.gz' in result.files_created + 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 diff --git a/tests/test_extras.py b/tests/test_extras.py new file mode 100644 index 000000000..163893e87 --- /dev/null +++ b/tests/test_extras.py @@ -0,0 +1,27 @@ +from os.path import join + +from tests.test_pip import reset_env, run_pip + + +def test_simple_extras_install_from_pypi(): + """ + Test installing a package from PyPI using extras dependency Paste[openid]. + """ + e = reset_env() + result = run_pip('install', 'Paste[openid]==1.7.5.1', expect_stderr=True) + initools_folder = e.site_packages / 'openid' + assert initools_folder in result.files_created, result.files_created + + +def test_no_extras_uninstall(): + """ + No extras dependency gets uninstalled when the root package is uninstalled + """ + env = reset_env() + result = run_pip('install', 'Paste[openid]==1.7.5.1', expect_stderr=True) + assert join(env.site_packages, 'paste') in result.files_created, sorted(result.files_created.keys()) + assert join(env.site_packages, 'openid') in result.files_created, sorted(result.files_created.keys()) + result2 = run_pip('uninstall', 'Paste', '-y') + # openid should not be uninstalled + initools_folder = env.site_packages / 'openid' + assert not initools_folder in result2.files_deleted, result.files_deleted diff --git a/tests/test_finder.py b/tests/test_finder.py new file mode 100644 index 000000000..4c930ed81 --- /dev/null +++ b/tests/test_finder.py @@ -0,0 +1,18 @@ +from pip.backwardcompat import urllib + +from pip.req import InstallRequirement +from pip.index import PackageFinder + +from tests.path import Path +from tests.test_pip import here + +find_links = 'file://' + urllib.quote(str(Path(here).abspath/'packages').replace('\\', '/')) + + +def test_no_mpkg(): + """Finder skips zipfiles with "macosx10" in the name.""" + finder = PackageFinder([find_links], []) + req = InstallRequirement.from_line("pkgwithmpkg") + found = finder.find_requirement(req, False) + + assert found.url.endswith("pkgwithmpkg-1.0.tar.gz"), found diff --git a/tests/test_freeze.py b/tests/test_freeze.py index c89ae8b15..4412c1435 100644 --- a/tests/test_freeze.py +++ b/tests/test_freeze.py @@ -32,6 +32,7 @@ def _check_output(result, expected): return '\n========== %s ==========\n' % msg assert checker.check_output(expected, actual, ELLIPSIS), banner('EXPECTED')+expected+banner('ACTUAL')+actual+banner(6*'=') + def test_freeze_basic(): """ Some tests of freeze, first we have to install some stuff. Note that @@ -57,6 +58,7 @@ def test_freeze_basic(): <BLANKLINE>""") _check_output(result, expected) + def test_freeze_svn(): """Now lets try it with an svn checkout""" env = reset_env() @@ -64,12 +66,12 @@ def test_freeze_svn(): local_repo('svn+http://svn.colorstudy.com/INITools/trunk'), 'initools-trunk') result = env.run('python', 'setup.py', 'develop', - cwd=env.scratch_path/ 'initools-trunk') + cwd=env.scratch_path/ 'initools-trunk', expect_stderr=True) result = run_pip('freeze', expect_stderr=True) expected = textwrap.dedent("""\ Script result: ...pip freeze -- stdout: -------------------- - -e %s@10#egg=INITools-0.3.1dev_r10-py...-dev_r10 + -e %s@10#egg=INITools-0.3.1dev...-dev_r10 ...""" % local_checkout('svn+http://svn.colorstudy.com/INITools/trunk')) _check_output(result, expected) @@ -80,28 +82,28 @@ def test_freeze_git_clone(): """ env = reset_env() - result = env.run('git', 'clone', local_repo('git+http://github.com/jezdez/django-pagination.git'), 'django-pagination') - result = env.run('git', 'checkout', '1df6507872d73ee387eb375428eafbfc253dfcd8', - cwd=env.scratch_path/'django-pagination', expect_stderr=True) + result = env.run('git', 'clone', local_repo('git+http://github.com/pypa/pip-test-package.git'), 'pip-test-package') + result = env.run('git', 'checkout', '7d654e66c8fa7149c165ddeffa5b56bc06619458', + cwd=env.scratch_path / 'pip-test-package', expect_stderr=True) result = env.run('python', 'setup.py', 'develop', - cwd=env.scratch_path / 'django-pagination') + cwd=env.scratch_path / 'pip-test-package') result = run_pip('freeze', expect_stderr=True) expected = textwrap.dedent("""\ Script result: ...pip freeze -- stdout: -------------------- - -e %s@...#egg=django_pagination-... - ...""" % local_checkout('git+http://github.com/jezdez/django-pagination.git')) + -e %s@...#egg=pip_test_package-... + ...""" % local_checkout('git+http://github.com/pypa/pip-test-package.git')) _check_output(result, expected) result = run_pip('freeze', '-f', - '%s#egg=django_pagination' % local_checkout('git+http://github.com/jezdez/django-pagination.git'), + '%s#egg=pip_test_package' % local_checkout('git+http://github.com/pypa/pip-test-package.git'), expect_stderr=True) expected = textwrap.dedent("""\ - Script result: pip freeze -f %(repo)s#egg=django_pagination + Script result: pip freeze -f %(repo)s#egg=pip_test_package -- stdout: -------------------- - -f %(repo)s#egg=django_pagination - -e %(repo)s@...#egg=django_pagination-dev - ...""" % {'repo': local_checkout('git+http://github.com/jezdez/django-pagination.git')}) + -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) @@ -201,3 +203,39 @@ def test_freeze_with_local_option(): INITools==0.2 <BLANKLINE>""") _check_output(result, expected) + + +def test_freeze_with_requirement_option(): + """ + Test that new requirements are created correctly with --requirement hints + + """ + reset_env() + ignores = textwrap.dedent("""\ + # Unchanged requirements below this line + -r ignore.txt + --requirement ignore.txt + -Z ignore + --always-unzip ignore + -f http://ignore + -i http://ignore + --extra-index-url http://ignore + --find-links http://ignore + --index-url http://ignore + """) + write_file('hint.txt', textwrap.dedent("""\ + INITools==0.1 + NoExist==4.2 + """) + ignores) + result = run_pip('install', 'initools==0.2') + result = run_pip('install', 'MarkupSafe') + result = run_pip('freeze', '--requirement', 'hint.txt', expect_stderr=True) + expected = textwrap.dedent("""\ + Script result: pip freeze --requirement hint.txt + -- stderr: -------------------- + Requirement file contains NoExist==4.2, but that package is not installed + + -- stdout: -------------------- + INITools==0.2 + """) + ignores + "## The following requirements were added by pip --freeze:..." + _check_output(result, expected) diff --git a/tests/test_help.py b/tests/test_help.py new file mode 100644 index 000000000..e638963e7 --- /dev/null +++ b/tests/test_help.py @@ -0,0 +1,66 @@ +from pip.exceptions import CommandError +from pip.commands.help import (HelpCommand, + SUCCESS, + ERROR,) +from mock import Mock +from nose.tools import assert_raises +from tests.test_pip import run_pip, reset_env + + +def test_run_method_should_return_sucess_when_finds_command_name(): + """ + Test HelpCommand.run for existing command + """ + options_mock = Mock() + args = ('freeze',) + help_cmd = HelpCommand() + status = help_cmd.run(options_mock, args) + assert status == SUCCESS + + +def test_run_method_should_return_sucess_when_command_name_not_specified(): + """ + Test HelpCommand.run when there are no args + """ + options_mock = Mock() + args = () + help_cmd = HelpCommand() + status = help_cmd.run(options_mock, args) + assert status == SUCCESS + + +def test_run_method_should_raise_command_error_when_command_does_not_exist(): + """ + Test HelpCommand.run for non-existing command + """ + options_mock = Mock() + args = ('mycommand',) + help_cmd = HelpCommand() + assert_raises(CommandError, help_cmd.run, options_mock, args) + + +def test_help_command_should_exit_status_ok_when_command_exists(): + """ + Test `help` command for existing command + """ + reset_env() + result = run_pip('help', 'freeze') + assert result.returncode == SUCCESS + + +def test_help_command_should_exit_status_ok_when_no_command_is_specified(): + """ + Test `help` command for no command + """ + reset_env() + result = run_pip('help') + assert result.returncode == SUCCESS + + +def test_help_command_should_exit_status_error_when_command_does_not_exist(): + """ + Test `help` command for non-existing command + """ + reset_env() + result = run_pip('help', 'mycommand', expect_error=True) + assert result.returncode == ERROR diff --git a/tests/test_index.py b/tests/test_index.py index e5385c452..6f9d216dc 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,4 +1,5 @@ -from pip.index import package_to_requirement +from pip.index import package_to_requirement, HTMLPage + def test_package_name_should_be_converted_to_requirement(): """ @@ -6,4 +7,22 @@ def test_package_name_should_be_converted_to_requirement(): """ assert package_to_requirement('Foo-1.2') == 'Foo==1.2' assert package_to_requirement('Foo-dev') == 'Foo==dev' - assert package_to_requirement('Foo') == 'Foo'
\ No newline at end of file + assert package_to_requirement('Foo') == 'Foo' + + +def test_html_page_should_be_able_to_scrap_rel_links(): + """ + Test scraping page looking for url in href + """ + page = HTMLPage(""" + <!-- The <th> elements below are a terrible terrible hack for setuptools --> + <li> + <strong>Home Page:</strong> + <!-- <th>Home Page --> + <a href="http://supervisord.org/">http://supervisord.org/</a> + </li>""", "supervisor") + + links = list(page.scraped_rel_links()) + assert len(links) == 1 + assert links[0].url == 'http://supervisord.org/' + diff --git a/tests/test_pip.py b/tests/test_pip.py index ec3ebc9b1..17e8f6616 100644 --- a/tests/test_pip.py +++ b/tests/test_pip.py @@ -49,15 +49,11 @@ sys.path = [src_folder] + sys.path def create_virtualenv(where, distribute=False): - save_argv = sys.argv - - try: - import virtualenv - distribute_opt = distribute and ['--distribute'] or [] - sys.argv = ['virtualenv', '--quiet'] + distribute_opt + ['--no-site-packages', '--unzip-setuptools', where] - virtualenv.main() - finally: - sys.argv = save_argv + import virtualenv + if sys.version_info[0] > 2: + distribute = True + virtualenv.create_environment( + where, use_distribute=distribute, unzip_setuptools=True) return virtualenv.path_locations(where) @@ -293,9 +289,10 @@ class TestPipEnvironment(TestFileEnvironment): if use_distribute is None: use_distribute = os.environ.get('PIP_TEST_USE_DISTRIBUTE', False) + self.use_distribute = use_distribute # Create a virtualenv and remember where it's putting things. - virtualenv_paths = create_virtualenv(self.venv_path, distribute=use_distribute) + virtualenv_paths = create_virtualenv(self.venv_path, distribute=self.use_distribute) assert self.venv_path == virtualenv_paths[0] # sanity check @@ -329,7 +326,7 @@ class TestPipEnvironment(TestFileEnvironment): " rather than expected %r" % (pythonbin, self.bin_path/'python')) # make sure we have current setuptools to avoid svn incompatibilities - if not use_distribute: + if not self.use_distribute: install_setuptools(self) # Uninstall whatever version of pip came with the virtualenv. @@ -421,6 +418,8 @@ class FastTestPipEnvironment(TestPipEnvironment): # put the test-scratch virtualenv's bin dir first on the PATH self.environ['PATH'] = Path.pathsep.join((self.bin_path, self.environ['PATH'])) + self.use_distribute = os.environ.get('PIP_TEST_USE_DISTRIBUTE', False) + if self.root_path.exists: rmtree(self.root_path) if self.backup_path.exists: @@ -429,10 +428,8 @@ class FastTestPipEnvironment(TestPipEnvironment): demand_dirs(self.venv_path) demand_dirs(self.scratch_path) - use_distribute = os.environ.get('PIP_TEST_USE_DISTRIBUTE', False) - # Create a virtualenv and remember where it's putting things. - create_virtualenv(self.venv_path, distribute=use_distribute) + create_virtualenv(self.venv_path, distribute=self.use_distribute) demand_dirs(self.user_site_path) @@ -449,7 +446,7 @@ class FastTestPipEnvironment(TestPipEnvironment): " rather than expected %r" % (pythonbin, self.bin_path/'python')) # make sure we have current setuptools to avoid svn incompatibilities - if not use_distribute: + if not self.use_distribute: install_setuptools(self) # Uninstall whatever version of pip came with the virtualenv. @@ -468,6 +465,7 @@ class FastTestPipEnvironment(TestPipEnvironment): def __del__(self): pass # shutil.rmtree(str(self.root_path), ignore_errors=True) + def run_pip(*args, **kw): result = env.run('pip', *args, **kw) ignore = [] diff --git a/tests/test_requirements.py b/tests/test_requirements.py index d40f5e872..59e1347c3 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -6,6 +6,7 @@ from tests.test_pip import reset_env, run_pip, write_file, pyversion, here, path from tests.local_repos import local_checkout from tests.path import Path + def test_requirements_file(): """ Test installing from a requirements file. @@ -25,6 +26,7 @@ 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.. @@ -39,6 +41,7 @@ def test_relative_requirements_file(): 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(): """ Test installing from multiple nested requirements files. @@ -65,7 +68,7 @@ def test_respect_order_in_requirements_file(): write_file('frameworks-req.txt', textwrap.dedent("""\ bidict ordereddict - mock + initools """)) result = run_pip('install', '-r', env.scratch_path / 'frameworks-req.txt') downloaded = [line for line in result.stdout.split('\n') @@ -75,8 +78,8 @@ def test_respect_order_in_requirements_file(): 'be "bidict" but was "%s"' % downloaded[0] assert 'ordereddict' in downloaded[1], 'Second download should ' \ 'be "ordereddict" but was "%s"' % downloaded[1] - assert 'mock' in downloaded[2], 'Third download should ' \ - 'be "mock" but was "%s"' % downloaded[2] + assert 'initools' in downloaded[2], 'Third download should ' \ + 'be "initools" but was "%s"' % downloaded[2] def test_requirements_data_structure_keeps_order(): @@ -88,6 +91,7 @@ def test_requirements_data_structure_keeps_order(): assert ['pip', 'nose', 'coverage'] == list(requirements.values()) assert ['pip', 'nose', 'coverage'] == list(requirements.keys()) + def test_requirements_data_structure_implements__repr__(): requirements = Requirements() requirements['pip'] = 'pip' @@ -95,6 +99,7 @@ def test_requirements_data_structure_implements__repr__(): assert "Requirements({'pip': 'pip', 'nose': 'nose'})" == repr(requirements) + def test_requirements_data_structure_implements__contains__(): requirements = Requirements() requirements['pip'] = 'pip' diff --git a/tests/test_search.py b/tests/test_search.py index 7a4a9ef12..53aad5349 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -2,7 +2,8 @@ import pip.download from pip.commands.search import (compare_versions, highest_version, transform_hits, - SearchCommand,) + SearchCommand) +from pip.status_codes import NO_MATCHES_FOUND, SUCCESS from pip.backwardcompat import xmlrpclib, b from mock import Mock from tests.test_pip import run_pip, reset_env, pyversion @@ -23,6 +24,7 @@ def test_version_compare(): assert compare_versions('1.0', '1.1') == -1 assert compare_versions('1.1', '1.0') == 1 assert compare_versions('1.1a1', '1.1') == -1 + assert compare_versions('1.1.1', '1.1a') == -1 assert highest_version(['1.0', '2.0', '0.1']) == '2.0' assert highest_version(['1.0a1', '1.0']) == '1.0' @@ -65,6 +67,7 @@ def test_searching_through_Search_class(): """ Verify if ``pip.vcs.Search`` uses tests xmlrpclib.Transport class """ + original_xmlrpclib_transport = pip.download.xmlrpclib_transport pip.download.xmlrpclib_transport = fake_transport = Mock() query = 'mylittlequerythatdoesnotexists' dumped_xmlrpc_request = b(xmlrpclib.dumps(({'name': query, 'summary': query}, 'or'), 'search')) @@ -72,5 +75,57 @@ def test_searching_through_Search_class(): fake_transport.request.return_value = (expected,) pypi_searcher = SearchCommand() result = pypi_searcher.search(query, 'http://pypi.python.org/pypi') - assert expected == result, result - fake_transport.request.assert_called_with('pypi.python.org', '/pypi', dumped_xmlrpc_request, verbose=VERBOSE_FALSE) + try: + assert expected == result, result + fake_transport.request.assert_called_with('pypi.python.org', '/pypi', dumped_xmlrpc_request, verbose=VERBOSE_FALSE) + finally: + pip.download.xmlrpclib_transport = original_xmlrpclib_transport + + +def test_search_missing_argument(): + """ + Test missing required argument for search + """ + env = reset_env(use_distribute=True) + result = run_pip('search', expect_error=True) + assert 'ERROR: Missing required argument (search query).' in result.stdout + + +def test_run_method_should_return_sucess_when_find_packages(): + """ + Test SearchCommand.run for found package + """ + options_mock = Mock() + options_mock.index = 'http://pypi.python.org/pypi' + search_cmd = SearchCommand() + status = search_cmd.run(options_mock, ('pip',)) + assert status == SUCCESS + + +def test_run_method_should_return_no_matches_found_when_does_not_find_packages(): + """ + Test SearchCommand.run for no matches + """ + options_mock = Mock() + options_mock.index = 'http://pypi.python.org/pypi' + search_cmd = SearchCommand() + status = search_cmd.run(options_mock, ('non-existant-package',)) + assert status == NO_MATCHES_FOUND, status + + +def test_search_should_exit_status_code_zero_when_find_packages(): + """ + Test search exit status code for package found + """ + env = reset_env(use_distribute=True) + result = run_pip('search', 'pip') + assert result.returncode == SUCCESS + + +def test_search_exit_status_code_when_finds_no_package(): + """ + Test search exit status code for no matches + """ + env = reset_env(use_distribute=True) + result = run_pip('search', 'non-existant-package', expect_error=True) + assert result.returncode == NO_MATCHES_FOUND diff --git a/tests/test_unicode.py b/tests/test_unicode.py new file mode 100644 index 000000000..d9196e750 --- /dev/null +++ b/tests/test_unicode.py @@ -0,0 +1,25 @@ +import os +from tests.test_pip import here, reset_env, run_pip + + +def test_install_package_that_emits_unicode(): + """ + Install a package with a setup.py that emits UTF-8 output and then fails. + This works fine in Python 2, but fails in Python 3 with: + + Traceback (most recent call last): + ... + File "/Users/marc/python/virtualenvs/py3.1-phpserialize/lib/python3.2/site-packages/pip-1.0.2-py3.2.egg/pip/__init__.py", line 230, in call_subprocess + line = console_to_str(stdout.readline()) + File "/Users/marc/python/virtualenvs/py3.1-phpserialize/lib/python3.2/site-packages/pip-1.0.2-py3.2.egg/pip/backwardcompat.py", line 60, in console_to_str + return s.decode(console_encoding) + UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 17: ordinal not in range(128) + + Refs https://github.com/pypa/pip/issues/326 + """ + + 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 + assert 'UnicodeDecodeError' not in result.stdout diff --git a/tests/test_uninstall.py b/tests/test_uninstall.py index 1b35bffc3..c88c7a681 100644 --- a/tests/test_uninstall.py +++ b/tests/test_uninstall.py @@ -7,6 +7,7 @@ from tests.local_repos import local_repo, local_checkout from pip.util import rmtree + def test_simple_uninstall(): """ Test simple install and uninstall. diff --git a/tests/test_upgrade.py b/tests/test_upgrade.py index 4d2145bb8..c6b8d686a 100644 --- a/tests/test_upgrade.py +++ b/tests/test_upgrade.py @@ -41,6 +41,34 @@ def test_upgrade_if_requested(): assert env.site_packages/'INITools-0.1-py%s.egg-info' % pyversion not in result.files_created +def test_upgrade_with_newest_already_installed(): + """ + If the newest version of a package is already installed, the package should + not be reinstalled and the user should be informed. + """ + + 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 + + +def test_upgrade_force_reinstall_newest(): + """ + Force reinstallation of a package even if it is already at its newest + version if --force-reinstall is supplied. + """ + + env = reset_env() + result = run_pip('install', 'INITools') + assert env.site_packages/ 'initools' in result.files_created, sorted(result.files_created.keys()) + result2 = run_pip('install', '--upgrade', '--force-reinstall', 'INITools') + assert result2.files_updated, 'upgrade to INITools 0.3 failed' + result3 = run_pip('uninstall', 'initools', '-y', expect_error=True) + assert_all_changes(result, result3, [env.venv/'build', 'cache']) + + def test_uninstall_before_upgrade(): """ Automatic uninstall-before-upgrade. @@ -54,6 +82,7 @@ def test_uninstall_before_upgrade(): result3 = run_pip('uninstall', 'initools', '-y', expect_error=True) assert_all_changes(result, result3, [env.venv/'build', 'cache']) + def test_uninstall_before_upgrade_from_url(): """ Automatic uninstall-before-upgrade from URL. @@ -67,6 +96,7 @@ def test_uninstall_before_upgrade_from_url(): result3 = run_pip('uninstall', 'initools', '-y', expect_error=True) assert_all_changes(result, result3, [env.venv/'build', 'cache']) + def test_upgrade_to_same_version_from_url(): """ When installing from a URL the same version that is already installed, no @@ -81,6 +111,7 @@ def test_upgrade_to_same_version_from_url(): result3 = run_pip('uninstall', 'initools', '-y', expect_error=True) assert_all_changes(result, result3, [env.venv/'build', 'cache']) + def test_upgrade_from_reqs_file(): """ Upgrade from a requirements file. @@ -134,6 +165,7 @@ def test_editable_git_upgrade(): version2 = env.run('version_pkg') assert 'some different version' in version2.stdout + def test_should_not_install_always_from_cache(): """ If there is an old cached package, pip should download the newer version @@ -146,6 +178,7 @@ def test_should_not_install_always_from_cache(): assert env.site_packages/'INITools-0.2-py%s.egg-info' % pyversion not in result.files_created assert env.site_packages/'INITools-0.1-py%s.egg-info' % pyversion in result.files_created + def test_install_with_ignoreinstalled_requested(): """ It installs package if ignore installed is set. @@ -153,6 +186,7 @@ def test_install_with_ignoreinstalled_requested(): """ env = reset_env() run_pip('install', 'INITools==0.1', expect_error=True) - result = run_pip('install', '-I' , 'INITools', expect_error=True) + result = run_pip('install', '-I', 'INITools', expect_error=True) assert result.files_created, 'pip install -I did not install' assert env.site_packages/'INITools-0.1-py%s.egg-info' % pyversion not in result.files_created + diff --git a/tests/test_vcs_backends.py b/tests/test_vcs_backends.py index fcdd00dac..9561254fa 100644 --- a/tests/test_vcs_backends.py +++ b/tests/test_vcs_backends.py @@ -2,16 +2,17 @@ from tests.test_pip import (reset_env, run_pip, _create_test_package, _change_test_package_version) from tests.local_repos import local_checkout + def test_install_editable_from_git_with_https(): """ Test cloning from Git with https. """ reset_env() result = run_pip('install', '-e', - '%s#egg=django-feedutil' % - local_checkout('git+https://github.com/jezdez/django-feedutil.git'), + '%s#egg=pip-test-package' % + local_checkout('git+https://github.com/pypa/pip-test-package.git'), expect_error=True) - result.assert_installed('django-feedutil', with_files=['.git']) + result.assert_installed('pip-test-package', with_files=['.git']) def test_git_with_sha1_revisions(): @@ -58,15 +59,15 @@ def test_git_with_tag_name_and_update(): Test cloning a git repository and updating to a different version. """ reset_env() - result = run_pip('install', '-e', '%s#egg=django-staticfiles' % - local_checkout('git+http://github.com/jezdez/django-staticfiles.git'), + result = run_pip('install', '-e', '%s#egg=pip-test-package' % + local_checkout('git+http://github.com/pypa/pip-test-package.git'), expect_error=True) - result.assert_installed('django-staticfiles', with_files=['.git']) + result.assert_installed('pip-test-package', with_files=['.git']) result = run_pip('install', '--global-option=--version', '-e', - '%s@0.3.1#egg=django-staticfiles' % - local_checkout('git+http://github.com/jezdez/django-staticfiles.git'), + '%s@0.1.1#egg=pip-test-package' % + local_checkout('git+http://github.com/pypa/pip-test-package.git'), expect_error=True) - assert '0.3.1\n' in result.stdout + assert '0.1.1\n' in result.stdout def test_git_branch_should_not_be_changed(): @@ -75,12 +76,13 @@ def test_git_branch_should_not_be_changed(): related to issue #32 and #161 """ env = reset_env() - run_pip('install', '-e', '%s#egg=django-staticfiles' % - local_checkout('git+http://github.com/jezdez/django-staticfiles.git'), + run_pip('install', '-e', '%s#egg=pip-test-package' % + local_checkout('git+http://github.com/pypa/pip-test-package.git'), expect_error=True) - source_dir = env.venv_path/'src'/'django-staticfiles' + source_dir = env.venv_path/'src'/'pip-test-package' result = env.run('git', 'branch', cwd=source_dir) - assert '* master' in result.stdout + assert '* master' in result.stdout, result.stdout + def test_git_with_non_editable_unpacking(): """ @@ -88,9 +90,10 @@ def test_git_with_non_editable_unpacking(): """ reset_env() result = run_pip('install', '--global-option=--version', local_checkout( - 'git+http://github.com/jezdez/django-staticfiles.git@0.3.1#egg=django-staticfiles' + 'git+http://github.com/pypa/pip-test-package.git@0.1.1#egg=pip-test-package' ), expect_error=True) - assert '0.3.1\n' in result.stdout + assert '0.1.1\n' in result.stdout + def test_git_with_editable_where_egg_contains_dev_string(): """ @@ -101,6 +104,7 @@ def test_git_with_editable_where_egg_contains_dev_string(): local_checkout('git+git://github.com/dcramer/django-devserver.git')) result.assert_installed('django-devserver', with_files=['.git']) + def test_git_with_non_editable_where_egg_contains_dev_string(): """ Test cloning a git repository from a non-editable url which contains "dev" string diff --git a/tests/test_vcs_bazaar.py b/tests/test_vcs_bazaar.py new file mode 100644 index 000000000..4e43fe5f9 --- /dev/null +++ b/tests/test_vcs_bazaar.py @@ -0,0 +1,29 @@ +from tests.test_pip import pyversion +from pip.vcs.bazaar import Bazaar + +if pyversion >= '3': + VERBOSE_FALSE = False +else: + VERBOSE_FALSE = 0 + + +def test_bazaar_simple_urls(): + """ + Test bzr url support. + + SSH and launchpad have special handling. + """ + http_bzr_repo = Bazaar(url='bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject') + https_bzr_repo = Bazaar(url='bzr+https://bzr.myproject.org/MyProject/trunk/#egg=MyProject') + ssh_bzr_repo = Bazaar(url='bzr+ssh://bzr.myproject.org/MyProject/trunk/#egg=MyProject') + ftp_bzr_repo = Bazaar(url='bzr+ftp://bzr.myproject.org/MyProject/trunk/#egg=MyProject') + sftp_bzr_repo = Bazaar(url='bzr+sftp://bzr.myproject.org/MyProject/trunk/#egg=MyProject') + launchpad_bzr_repo = Bazaar(url='bzr+lp:MyLaunchpadProject#egg=MyLaunchpadProject') + + assert http_bzr_repo.get_url_rev() == ('http://bzr.myproject.org/MyProject/trunk/', None) + assert https_bzr_repo.get_url_rev() == ('https://bzr.myproject.org/MyProject/trunk/', None) + assert ssh_bzr_repo.get_url_rev() == ('bzr+ssh://bzr.myproject.org/MyProject/trunk/', None) + assert ftp_bzr_repo.get_url_rev() == ('ftp://bzr.myproject.org/MyProject/trunk/', None) + assert sftp_bzr_repo.get_url_rev() == ('sftp://bzr.myproject.org/MyProject/trunk/', None) + assert launchpad_bzr_repo.get_url_rev() == ('lp:MyLaunchpadProject', None) + diff --git a/tests/test_vcs_subversion.py b/tests/test_vcs_subversion.py new file mode 100644 index 000000000..212220113 --- /dev/null +++ b/tests/test_vcs_subversion.py @@ -0,0 +1,21 @@ +from mock import patch +from pip.vcs.subversion import Subversion +from tests.test_pip import reset_env + +@patch('pip.vcs.subversion.call_subprocess') +def test_obtain_should_recognize_auth_info_in_url(call_subprocess_mock): + env = reset_env() + svn = Subversion(url='svn+http://username:password@svn.example.com/') + svn.obtain(env.scratch_path/'test') + call_subprocess_mock.assert_called_with([ + svn.cmd, 'checkout', '-q', '--username', 'username', '--password', 'password', + 'http://username:password@svn.example.com/', env.scratch_path/'test']) + +@patch('pip.vcs.subversion.call_subprocess') +def test_export_should_recognize_auth_info_in_url(call_subprocess_mock): + env = reset_env() + svn = Subversion(url='svn+http://username:password@svn.example.com/') + svn.export(env.scratch_path/'test') + assert call_subprocess_mock.call_args[0] == ([ + svn.cmd, 'export', '--username', 'username', '--password', 'password', + 'http://username:password@svn.example.com/', env.scratch_path/'test'],) |