diff -Nru pgxnclient-1.0.3/AUTHORS pgxnclient-1.2.1/AUTHORS --- pgxnclient-1.0.3/AUTHORS 1970-01-01 00:00:00.000000000 +0000 +++ pgxnclient-1.2.1/AUTHORS 2013-05-12 15:06:13.000000000 +0000 @@ -0,0 +1,27 @@ +Who has contributed to the PGXN client? +======================================= + +Daniele Varrazzo + + He rushed to implement a client before David could do it in Perl! + +David Wheeler + + He is the PGXN mastermind: a lot of helpful design discussions. + +Peter Eisentraut + + First implementation of tarball support. Auto-sudo is not a good idea, I + got it. + +Hitoshi Harada + + Tricky installation corner cases. + +Andrey Popp + + Make selection. Helped the program not to suck on BSD! + +Also thank you everybody for the useful discussions on the PGXN mailing list, +bug reports, proofreading the docs and the general support to the project. + diff -Nru pgxnclient-1.0.3/CHANGES pgxnclient-1.2.1/CHANGES --- pgxnclient-1.0.3/CHANGES 2012-05-19 20:33:27.000000000 +0000 +++ pgxnclient-1.2.1/CHANGES 2013-05-12 15:06:20.000000000 +0000 @@ -3,11 +3,40 @@ PGXN Client changes log ----------------------- +pgxnclient 1.2.1 +================ + +- Fixed traceback on error when a dir doesn't contain META.json (ticket #19). +- Handle version numbers both with and without hyphen (ticket #22). + + +pgxnclient 1.2 +============== + +- Packages can be downloaded, installed, loaded specifying an URL + (ticket #15). +- Added support for ``.tar`` files (ticket #17). +- Use ``gmake`` in favour of ``make`` for platforms where the two are + distinct, such as BSD (ticket #14). +- Added ``--make`` option to select the make executable (ticket #16). + + +pgxnclient 1.1 +============== + +- Dropped support for Python 2.4. +- ``sudo`` is not invoked automatically: the ``--sudo`` option must be + specified if the user has not permission to write into PostgreSQL's libdir + (ticket #13). The ``--sudo`` option can also be invoked without argument. +- Make sure the same ``pg_config`` is used both by the current user and by + sudo. + + pgxnclient 1.0.3 ================ - Can deal with extensions whose ``Makefile`` is created by ``configure`` - and with makefile not in the package root. Patch provided by Hithoshi + and with makefile not in the package root. Patch provided by Hitoshi Harada (ticket #12). diff -Nru pgxnclient-1.0.3/COPYING pgxnclient-1.2.1/COPYING --- pgxnclient-1.0.3/COPYING 2011-04-24 12:06:16.000000000 +0000 +++ pgxnclient-1.2.1/COPYING 2013-05-12 15:06:13.000000000 +0000 @@ -1,4 +1,4 @@ -Copyright (c) 2011, Daniele Varrazzo +Copyright (c) 2011-2012, Daniele Varrazzo All rights reserved. Redistribution and use in source and binary forms, with or without diff -Nru pgxnclient-1.0.3/MANIFEST.in pgxnclient-1.2.1/MANIFEST.in --- pgxnclient-1.0.3/MANIFEST.in 2011-11-29 22:51:08.000000000 +0000 +++ pgxnclient-1.2.1/MANIFEST.in 2013-05-12 15:06:13.000000000 +0000 @@ -1,4 +1,4 @@ -include CHANGES COPYING MANIFEST.in README.rst setup.py Makefile +include AUTHORS CHANGES COPYING MANIFEST.in README.rst setup.py Makefile include bin/pgxn bin/pgxnclient recursive-include pgxnclient *.py recursive-include testdata * diff -Nru pgxnclient-1.0.3/Makefile pgxnclient-1.2.1/Makefile --- pgxnclient-1.0.3/Makefile 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/Makefile 2013-05-12 15:06:13.000000000 +0000 @@ -1,6 +1,6 @@ # pgxnclient Makefile # -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/PKG-INFO pgxnclient-1.2.1/PKG-INFO --- pgxnclient-1.0.3/PKG-INFO 2012-05-19 20:37:51.000000000 +0000 +++ pgxnclient-1.2.1/PKG-INFO 2013-05-12 15:08:59.000000000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: pgxnclient -Version: 1.0.3 +Version: 1.2.1 Summary: A command line tool to interact with the PostgreSQL Extension Network. Home-page: http://pgxnclient.projects.postgresql.org/ Author: Daniele Varrazzo @@ -15,5 +15,10 @@ Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 Classifier: Topic :: Database diff -Nru pgxnclient-1.0.3/bin/pgxn pgxnclient-1.2.1/bin/pgxn --- pgxnclient-1.0.3/bin/pgxn 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/bin/pgxn 2013-05-12 15:06:13.000000000 +0000 @@ -13,7 +13,7 @@ ``PATH`` directories. """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/bin/pgxnclient pgxnclient-1.2.1/bin/pgxnclient --- pgxnclient-1.0.3/bin/pgxnclient 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/bin/pgxnclient 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/debian/changelog pgxnclient-1.2.1/debian/changelog --- pgxnclient-1.0.3/debian/changelog 2012-05-22 10:12:20.000000000 +0000 +++ pgxnclient-1.2.1/debian/changelog 2013-06-24 08:40:59.000000000 +0000 @@ -1,3 +1,27 @@ +pgxnclient (1.2.1-2) unstable; urgency=low + + * B-D on libpq-dev to make pg_config available. Spotted by Felix Geyer. + Closes: #713054. + + -- Christoph Berg Mon, 24 Jun 2013 10:40:28 +0200 + +pgxnclient (1.2.1-1) unstable; urgency=low + + * New upstream release. + * Add B-D python-simplejson, python-unittest2 for the testsuite (needed on + squeeze). + * Point Vcs-Browser at a real webpage instead of some repository. + + -- Christoph Berg Thu, 20 Jun 2013 12:25:53 +0200 + +pgxnclient (1.2-1) experimental; urgency=low + + * New upstream release. + * Install AUTHORS file. + * Point watch file directly at the download directory. + + -- Christoph Berg Sat, 06 Oct 2012 10:50:15 +0200 + pgxnclient (1.0.3-1) unstable; urgency=low * New upstream release. diff -Nru pgxnclient-1.0.3/debian/control pgxnclient-1.2.1/debian/control --- pgxnclient-1.0.3/debian/control 2012-05-15 17:06:22.000000000 +0000 +++ pgxnclient-1.2.1/debian/control 2013-06-24 08:40:25.000000000 +0000 @@ -2,11 +2,12 @@ Section: database Priority: extra Maintainer: Christoph Berg -Build-Depends: debhelper (>= 8), python-all, python-mock, python-setuptools +Build-Depends: debhelper (>= 8), python-all, python-mock, python-setuptools, + python-simplejson, python-unittest2, libpq-dev Standards-Version: 3.9.3 Homepage: http://pgxnclient.projects.postgresql.org/ Vcs-Git: git://git.debian.org/pkg-postgresql/pgxnclient.git -Vcs-Browser: http://anonscm.debian.org/git/pkg-postgresql/pgxnclient.git +Vcs-Browser: http://anonscm.debian.org/gitweb/?p=pkg-postgresql/pgxnclient.git Package: pgxnclient Architecture: all diff -Nru pgxnclient-1.0.3/debian/docs pgxnclient-1.2.1/debian/docs --- pgxnclient-1.0.3/debian/docs 2012-04-04 19:44:08.000000000 +0000 +++ pgxnclient-1.2.1/debian/docs 2012-11-26 13:38:16.000000000 +0000 @@ -1 +1,2 @@ +AUTHORS README.rst diff -Nru pgxnclient-1.0.3/debian/rules pgxnclient-1.2.1/debian/rules --- pgxnclient-1.0.3/debian/rules 2012-04-04 20:28:24.000000000 +0000 +++ pgxnclient-1.2.1/debian/rules 2013-06-20 10:22:15.000000000 +0000 @@ -3,5 +3,9 @@ override_dh_install: python setup.py install --prefix=/usr --root=$(CURDIR)/debian/pgxnclient --install-layout=deb +override_dh_auto_clean: + dh_auto_clean + find pgxnclient -name '*.pyc' | xargs -r rm + %: dh $@ --with python2 diff -Nru pgxnclient-1.0.3/debian/watch pgxnclient-1.2.1/debian/watch --- pgxnclient-1.0.3/debian/watch 2012-05-15 16:56:03.000000000 +0000 +++ pgxnclient-1.2.1/debian/watch 2012-11-26 13:38:16.000000000 +0000 @@ -1,2 +1,3 @@ version=3 -http://pypi.python.org/pypi/pgxnclient/ http://pypi.python.org/packages/source/p/pgxnclient/pgxnclient-(.*).tar.gz.* +#http://pypi.python.org/pypi/pgxnclient/ http://pypi.python.org/packages/source/p/pgxnclient/pgxnclient-(.*).tar.gz.* +http://pypi.python.org/packages/source/p/pgxnclient/pgxnclient-(.*).tar.gz.* diff -Nru pgxnclient-1.0.3/docs/Makefile pgxnclient-1.2.1/docs/Makefile --- pgxnclient-1.0.3/docs/Makefile 2012-03-13 19:38:42.000000000 +0000 +++ pgxnclient-1.2.1/docs/Makefile 2013-05-12 15:06:13.000000000 +0000 @@ -2,7 +2,7 @@ # # Use 'make env', then 'make html' to build the HTML documentation # -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo PYTHON := python$(PYTHON_VERSION) PYTHON_VERSION ?= $(shell $(PYTHON) -c 'import sys; print ("%d.%d" % sys.version_info[:2])') diff -Nru pgxnclient-1.0.3/docs/changes.rst pgxnclient-1.2.1/docs/changes.rst --- pgxnclient-1.0.3/docs/changes.rst 2012-05-19 20:33:27.000000000 +0000 +++ pgxnclient-1.2.1/docs/changes.rst 2013-05-12 15:06:20.000000000 +0000 @@ -3,11 +3,40 @@ PGXN Client changes log ----------------------- +pgxnclient 1.2.1 +================ + +- Fixed traceback on error when a dir doesn't contain META.json (ticket #19). +- Handle version numbers both with and without hyphen (ticket #22). + + +pgxnclient 1.2 +============== + +- Packages can be downloaded, installed, loaded specifying an URL + (ticket #15). +- Added support for ``.tar`` files (ticket #17). +- Use ``gmake`` in favour of ``make`` for platforms where the two are + distinct, such as BSD (ticket #14). +- Added ``--make`` option to select the make executable (ticket #16). + + +pgxnclient 1.1 +============== + +- Dropped support for Python 2.4. +- ``sudo`` is not invoked automatically: the ``--sudo`` option must be + specified if the user has not permission to write into PostgreSQL's libdir + (ticket #13). The ``--sudo`` option can also be invoked without argument. +- Make sure the same ``pg_config`` is used both by the current user and by + sudo. + + pgxnclient 1.0.3 ================ - Can deal with extensions whose ``Makefile`` is created by ``configure`` - and with makefile not in the package root. Patch provided by Hithoshi + and with makefile not in the package root. Patch provided by Hitoshi Harada (ticket #12). diff -Nru pgxnclient-1.0.3/docs/conf.py pgxnclient-1.2.1/docs/conf.py --- pgxnclient-1.0.3/docs/conf.py 2012-03-13 19:38:42.000000000 +0000 +++ pgxnclient-1.2.1/docs/conf.py 2013-05-12 15:06:13.000000000 +0000 @@ -41,7 +41,7 @@ # General information about the project. project = u'PGXN Client' -copyright = u'2011, Daniele Varrazzo' +copyright = u'2011-2012, Daniele Varrazzo' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff -Nru pgxnclient-1.0.3/docs/index.rst pgxnclient-1.2.1/docs/index.rst --- pgxnclient-1.0.3/docs/index.rst 2012-03-13 19:38:42.000000000 +0000 +++ pgxnclient-1.2.1/docs/index.rst 2013-05-12 15:06:13.000000000 +0000 @@ -20,16 +20,16 @@ The client interacts with the PGXN web service and a ``Makefile`` provided by the extension. The best results are achieved with makefiles using the PostgreSQL `Extension Building Infrastructure`__; however the client tries to -degrade gracefully in presence of any package hosted on PGXN. +degrade gracefully in presence of any package hosted on PGXN and any package +available outside the extension network. .. _semver: http://pgxn.org/dist/semver -.. __: http://www.postgresql.org/docs/9.1/static/extend-pgxs.html +.. __: http://www.postgresql.org/docs/current/static/extend-pgxs.html - Home page: http://pgxnclient.projects.postgresql.org/ - Downloads: http://pypi.python.org/pypi/pgxnclient/ - Discussion group: http://groups.google.com/group/pgxn-users/ - Source repository: https://github.com/dvarrazzo/pgxnclient/ -- PgFoundry project: http://pgfoundry.org/projects/pgxnclient/ Contents: diff -Nru pgxnclient-1.0.3/docs/install.rst pgxnclient-1.2.1/docs/install.rst --- pgxnclient-1.0.3/docs/install.rst 2012-05-19 20:33:27.000000000 +0000 +++ pgxnclient-1.2.1/docs/install.rst 2013-05-12 15:06:13.000000000 +0000 @@ -4,7 +4,7 @@ Prerequisites ------------- -The program is implemented in Python. Versions from Python 2.4 onwards are +The program is implemented in Python. Versions from Python 2.5 onwards are supported, including Python 3.0 and successive. PostgreSQL client-side development tools are required to build and install diff -Nru pgxnclient-1.0.3/docs/usage.rst pgxnclient-1.2.1/docs/usage.rst --- pgxnclient-1.0.3/docs/usage.rst 2012-03-13 04:00:05.000000000 +0000 +++ pgxnclient-1.2.1/docs/usage.rst 2013-05-12 15:06:13.000000000 +0000 @@ -60,10 +60,17 @@ ``--stable``, ``--testing`` and ``--unstable`` to specify the minimum release status accepted. The default is "stable". -A few commands also allow specifying a local ``.zip`` package or a local -directory containing a distribution: in this case the specification should -contain at least a path separator to disambiguate it from a distribution name, -for instance ``pgxn install ./foo.zip``. +A few commands also allow specifying a local archive or local directory +containing a distribution: in this case the specification should contain at +least a path separator to disambiguate it from a distribution name (for +instance ``pgxn install ./foo.zip``) or it should be specified as an URL with +``file://`` schema. + +A few commands also allow specifying a remote package with a URL. Currently +the schemas ``http://`` and ``https://`` are supported. + +Currently the client supports ``.zip`` and ``.tar`` archives (eventually with +*gzip* and *bz2* compression). .. _install: @@ -79,35 +86,53 @@ :class: pgxn-install pgxn install [--help] [--stable | --testing | --unstable] - [--pg_config *PATH*] [--sudo *PROG* | --nosudo] + [--pg_config *PROG*] [--make *PROG*] + [--sudo [*PROG*] | --nosudo] *SPEC* The program takes a `package specification`_ identifying the distribution to work with. The download phase is skipped if the distribution specification -refers to a local directory or package. +refers to a local directory or package. The package may be specified with an +URL. Note that the built extension is not loaded in any database: use the command `load`_ for this purpose. -The command will run the ``./configure`` script if available in the package, +The command will run the ``configure`` script if available in the package, then will perform ``make all`` and ``make install``. It is assumed that the ``Makefile`` provided by the distribution uses PGXS_ to build the extension, but this is not enforced: you may provide any Makefile as long as the expected commands are implemented. -.. _PGXS: http://www.postgresql.org/docs/9.1/static/extend-pgxs.html - -The install phase usually requires root privileges in order to install a build -library and other files in the PostgreSQL directories: by default -:program:`sudo` will be invoked for the purpose. An alternative program can be -specified with the option :samp:`--sudo {PROG}`; ``--nosudo`` can be used to -avoid running any program. +.. _PGXS: http://www.postgresql.org/docs/current/static/extend-pgxs.html If there are many PostgreSQL installations on the system, the extension will be built and installed against the instance whose :program:`pg_config` is first found on the :envvar:`PATH`. A different instance can be specified using the option :samp:`--pg_config {PATH}`. +The PGXS_ build system relies on a presence of `GNU Make`__: in many systems +it is installed as :program:`gmake` or :program:`make` executable. The program +will use the first of them on the path. You can specify an alternative program +using ``--make`` option. + +.. __: http://www.gnu.org/software/make/ + +If the extension is being installed into a system PostgreSQL installation, the +install phase will likely require root privileges to be performed. In this +case either run the command under :program:`sudo` or specify the ``--sudo`` +option: in the latter case :program:`sudo` will only be invoked during the +"install" phase. An optional program :samp:`{PROG}` to elevate the user +privileges can be specified as ``--sudo`` option; if none is specified, +:program:`sudo` will be used. + +.. note:: + + If ``--sudo`` is the last option and no :samp:`{PROG}` is specified, a + ``--`` separator may be required to disambiguate the :samp:`{SPEC}`:: + + pgxn install --sudo -- foobar + .. _check: @@ -122,12 +147,14 @@ :class: pgxn-check pgxn check [--help] [--stable | --testing | --unstable] - [--pg_config *PATH*] [-d *DBNAME*] [-h *HOST*] [-p *PORT*] [-U *NAME*] + [--pg_config *PROG*] [--make *PROG*] + [-d *DBNAME*] [-h *HOST*] [-p *PORT*] [-U *NAME*] *SPEC* The command takes a `package specification`_ identifying the distribution to -work with, which can also be a local file or directory. The distribution is -unpacked if required and the ``installcheck`` make target is run. +work with, which can also be a local file or directory or an URL. The +distribution is unpacked if required and the ``installcheck`` make target is +run. .. note:: The command doesn't run ``make all`` before ``installcheck``: if any file @@ -150,6 +177,8 @@ difference that the variable :envvar:`PGDATABASE` doesn't influence the database name. +See the install_ command for details about the command arguments. + .. warning:: At the time of writing, :program:`pg_regress` on Debian and derivatives is affected by `bug #554166`__ which makes *HOST* selection impossible. @@ -170,7 +199,8 @@ :class: pgxn-uninstall pgxn uninstall [--help] [--stable | --testing | --unstable] - [--pg_config *PATH*] [--sudo *PROG* | --nosudo] + [--pg_config *PROG*] [--make *PROG*] + [--sudo [*PROG*] | --nosudo] *SPEC* The command does the opposite of the install_ command, removing a @@ -203,11 +233,11 @@ *SPEC* [*EXT* [*EXT* ...]] The distribution is specified according to the `package specification`_ and -can refer to a local directory or file. No consistency check is performed -between the packages specified in the ``install`` and ``load`` command: the -specifications should refer to compatible packages. The specified distribution -is only used to read the metadata: only installed files are actually used to -issue database commands. +can refer to a local directory or file or to an URL. No consistency check is +performed between the packages specified in the ``install`` and ``load`` +command: the specifications should refer to compatible packages. The specified +distribution is only used to read the metadata: only installed files are +actually used to issue database commands. The database to install into can be specified using options ``-d``/``--dbname``, ``-h``/``--host``, ``-p``/``--port``, @@ -226,8 +256,8 @@ EXTENSION`_ command, otherwise it will be loaded as a loose set of objects. For more information see the `extensions documentation`__. -.. _CREATE EXTENSION: http://www.postgresql.org/docs/9.1/static/sql-createextension.html -.. __: http://www.postgresql.org/docs/9.1/static/extend-extensions.html +.. _CREATE EXTENSION: http://www.postgresql.org/docs/current/static/sql-createextension.html +.. __: http://www.postgresql.org/docs/current/static/extend-extensions.html The command is based on the `'provides' section`_ of the distribution's ``META.json``: if a SQL file is specified, that file will be used to load the @@ -240,7 +270,7 @@ If the distribution provides more than one extension, the extensions are loaded in the order in which they are specified in the ``provides`` section of -the ``META.json`` file. It is also possilbe to load only a few of the +the ``META.json`` file. It is also possible to load only a few of the extensions provided, specifying them after *SPEC*: the extensions will be loaded in the order specified. @@ -288,11 +318,11 @@ If the distribution specifies more than one extension, they are unloaded in reverse order respect to the order in which they are specified in the -``META.json`` file. It is also possilbe to unload only a few of the +``META.json`` file. It is also possible to unload only a few of the extensions provided, specifying them after *SPEC*: the extensions will be unloaded in the order specified. -.. _DROP EXTENSION: http://www.postgresql.org/docs/9.1/static/sql-dropextension.html +.. _DROP EXTENSION: http://www.postgresql.org/docs/current/static/sql-dropextension.html See the load_ command for details about the command arguments. @@ -313,11 +343,12 @@ [--target *PATH*] *SPEC* -The distribution is specified according to the `package specification`_. The -file is saved in the current directory with name usually -:samp:`{distribution}-{version}.zip`. If a file with the same name exists, a -suffix ``-1``, ``-2`` etc. is added to the name, before the extension. A -different directory or name can be specified using the ``--target`` option. +The distribution is specified according to the `package specification`_ and +can be represented by an URL. The file is saved in the current directory with +name usually :samp:`{distribution}-{version}.zip`. If a file with the same +name exists, a suffix ``-1``, ``-2`` etc. is added to the name, before the +extension. A different directory or name can be specified using the +``--target`` option. .. _pgxn-search: @@ -398,9 +429,9 @@ [--details | --meta | --readme | --versions] *SPEC* -The distribution is specified according to the `package specification`_. -The command output is a list of values obtained by the distribution's -``META.json`` file, for example: +The distribution is specified according to the `package specification`_. It +cannot be a local dir or file nor an URL. The command output is a list of +values obtained by the distribution's ``META.json`` file, for example: .. code-block:: console diff -Nru pgxnclient-1.0.3/pgxnclient/__init__.py pgxnclient-1.2.1/pgxnclient/__init__.py --- pgxnclient-1.0.3/pgxnclient/__init__.py 2012-05-19 20:33:27.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/__init__.py 2013-05-12 15:06:20.000000000 +0000 @@ -2,11 +2,11 @@ pgxnclient -- main package """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client -__version__ = '1.0.3' +__version__ = '1.2.1' # Paths where to find the command executables. # If relative, it's from the `pgxnclient` package directory. diff -Nru pgxnclient-1.0.3/pgxnclient/api.py pgxnclient-1.2.1/pgxnclient/api.py --- pgxnclient-1.0.3/pgxnclient/api.py 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/api.py 2013-05-12 15:06:20.000000000 +0000 @@ -2,15 +2,17 @@ pgxnclient -- client API stub """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client +from __future__ import with_statement + from urllib import urlencode +from pgxnclient import network from pgxnclient.utils import load_json from pgxnclient.errors import NetworkError, NotFound, ResourceNotFound -from pgxnclient.network import get_file from pgxnclient.utils.uri import expand_template @@ -20,27 +22,29 @@ def dist(self, dist, version=''): try: - return load_json(self.call( - version and 'meta' or 'dist', - {'dist': dist, 'version': version})) + with self.call(version and 'meta' or 'dist', + {'dist': dist, 'version': version}) as f: + return load_json(f) except ResourceNotFound: raise NotFound("distribution '%s' not found" % dist) def ext(self, ext): try: - return load_json(self.call('extension', {'extension': ext})) + with self.call('extension', {'extension': ext}) as f: + return load_json(f) except ResourceNotFound: raise NotFound("extension '%s' not found" % ext) def meta(self, dist, version, as_json=True): - f = self.call('meta', {'dist': dist, 'version': version}) - if as_json: - return load_json(f) - else: - return f.read().decode('utf-8') + with self.call('meta', {'dist': dist, 'version': version}) as f: + if as_json: + return load_json(f) + else: + return f.read().decode('utf-8') def readme(self, dist, version): - return self.call('readme', {'dist': dist, 'version': version}).read() + with self.call('readme', {'dist': dist, 'version': version}) as f: + return f.read() def download(self, dist, version): dist = dist.lower() @@ -48,7 +52,8 @@ return self.call('download', {'dist': dist, 'version': version}) def mirrors(self): - return load_json(self.call('mirrors')) + with self.call('mirrors') as f: + return load_json(f) def search(self, where, query): """Search into PGXN. @@ -60,18 +65,32 @@ # convert the query list into a string q = ' '.join([' ' in s and ('"%s"' % s) or s for s in query]) - return load_json(self.call('search', {'in': where}, - query={'q': q})) + with self.call('search', {'in': where}, query={'q': q}) as f: + return load_json(f) def stats(self, arg): - return load_json(self.call('stats', {'stats': arg})) + with self.call('stats', {'stats': arg}) as f: + return load_json(f) def user(self, username): - return load_json(self.call('user', {'user': username})) + with self.call('user', {'user': username}) as f: + return load_json(f) def call(self, meth, args=None, query=None): url = self.get_url(meth, args, query) - return get_file(url) + try: + return network.get_file(url) + except ResourceNotFound: + # check if it is one of the broken URLs as reported in + # https://groups.google.com/group/pgxn-users/browse_thread/thread/e41fbc202680c92c + version = args and args.get('version') + if not (version and version.trail): + raise + + args = args.copy() + args['version'] = str(version).replace('-', '', 1) + url = self.get_url(meth, args, query) + return network.get_file(url) def get_url(self, meth, args=None, query=None): tmpl = self.get_template(meth) @@ -91,7 +110,8 @@ if self._api_index is None: url = self.mirror.rstrip('/') + '/index.json' try: - self._api_index = load_json(get_file(url)) + with network.get_file(url) as f: + self._api_index = load_json(f) except ResourceNotFound: raise NetworkError("API index not found at '%s'" % url) diff -Nru pgxnclient-1.0.3/pgxnclient/archive.py pgxnclient-1.2.1/pgxnclient/archive.py --- pgxnclient-1.0.3/pgxnclient/archive.py 1970-01-01 00:00:00.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/archive.py 2013-05-12 15:06:13.000000000 +0000 @@ -0,0 +1,99 @@ +""" +pgxnclient -- archives handling +""" + +# Copyright (C) 2011-2012 Daniele Varrazzo + +# This file is part of the PGXN client + +import os + +from pgxnclient.i18n import _ +from pgxnclient.utils import load_jsons +from pgxnclient.errors import PgxnClientException + +def from_spec(spec): + """Return an `Archive` instance to handle the file requested by *spec* + """ + assert spec.is_file() + return from_file(spec.filename) + +def from_file(filename): + """Return an `Archive` instance to handle the file *filename* + """ + from pgxnclient.zip import ZipArchive + from pgxnclient.tar import TarArchive + + for cls in (ZipArchive, TarArchive): + a = cls(filename) + if a.can_open(): + return a + + raise PgxnClientException( + _("can't open archive '%s': file type not recognized") + % filename) + + +class Archive(object): + """Base class to handle archives.""" + def __init__(self, filename): + self.filename = filename + + def can_open(self): + """Return `!True` if the `!filename` can be opened by the obect.""" + raise NotImplementedError + + def open(self): + """Open the archive for usage. + + Raise PgxnClientException if the archive can't be open. + """ + raise NotImplementedError + + def close(self): + """Close the archive after usage.""" + raise NotImplementedError + + def list_files(self): + """Return an iterable with the list of file names in the archive.""" + raise NotImplementedError + + def read(self, fn): + """Return a file's data from the archive.""" + raise NotImplementedError + + def unpack(self, destdir): + raise NotImplementedError + + def get_meta(self): + filename = self.filename + + self.open() + try: + # Return the first file with the expected name + for fn in self.list_files(): + if fn.endswith('META.json'): + return load_jsons(self.read(fn).decode('utf8')) + else: + raise PgxnClientException( + _("file 'META.json' not found in archive '%s'") % filename) + finally: + self.close() + + def _find_work_directory(self, destdir): + """ + Choose the directory where to work. + + Because we are mostly a wrapper for pgxs, let's look for a makefile. + The tar should contain a single base directory, so return the first + dir we found containing a Makefile, alternatively just return the + unpacked dir + """ + for dir in os.listdir(destdir): + for fn in ('Makefile', 'makefile', 'GNUmakefile', 'configure'): + if os.path.exists(os.path.join(destdir, dir, fn)): + return os.path.join(destdir, dir) + + return destdir + + diff -Nru pgxnclient-1.0.3/pgxnclient/cli.py pgxnclient-1.2.1/pgxnclient/cli.py --- pgxnclient-1.0.3/pgxnclient/cli.py 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/cli.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,7 +2,7 @@ pgxnclient -- command line entry point """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client @@ -122,3 +122,6 @@ return fn +if __name__ == '__main__': + script() + diff -Nru pgxnclient-1.0.3/pgxnclient/commands/__init__.py pgxnclient-1.2.1/pgxnclient/commands/__init__.py --- pgxnclient-1.0.3/pgxnclient/commands/__init__.py 2012-05-19 20:33:27.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/commands/__init__.py 2013-05-12 15:06:13.000000000 +0000 @@ -6,23 +6,27 @@ modules. """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client +from __future__ import with_statement + import os import sys import logging from subprocess import Popen, PIPE -from pgxnclient.utils import load_json -from pgxnclient.utils import argparse +from pgxnclient.utils import load_json, argparse, find_executable from pgxnclient import __version__ +from pgxnclient import network from pgxnclient import Spec, SemVer +from pgxnclient import archive from pgxnclient.api import Api from pgxnclient.i18n import _, gettext from pgxnclient.errors import NotFound, PgxnClientException, ProcessError, ResourceNotFound, UserAbort +from pgxnclient.utils.temp import temp_dir logger = logging.getLogger('pgxnclient.commands') @@ -222,7 +226,6 @@ from pgxnclient.errors import BadSpecError -from pgxnclient.utils.zip import get_meta_from_zip class WithSpec(Command): """Mixin to implement commands taking a package specification. @@ -264,7 +267,7 @@ return subp - def get_spec(self, _can_be_local=False): + def get_spec(self, _can_be_local=False, _can_be_url=False): """ Return the package specification requested. @@ -282,6 +285,10 @@ raise PgxnClientException( _("you cannot use a local resource with this command")) + if not _can_be_url and spec.is_url(): + raise PgxnClientException( + _("you cannot use an url with this command")) + return spec def get_best_version(self, data, spec, quiet=False): @@ -358,7 +365,7 @@ Return the object obtained parsing the JSON. """ - if not spec.is_local(): + if spec.is_name(): # Get the metadata from the API try: data = self.api.dist(spec.name) @@ -377,14 +384,24 @@ logger.debug("reading %s", fn) if not os.path.exists(fn): raise PgxnClientException( - _("file 'META.json' not found in '%s'") % dir) + _("file 'META.json' not found in '%s'") % spec.dirname) - return load_json(open(fn)) + with open(fn) as f: + return load_json(f) elif spec.is_file(): - # Get the metadata from a zip file - return get_meta_from_zip(spec.filename) + arc = archive.from_spec(spec) + return arc.get_meta() + elif spec.is_url(): + with network.get_file(spec.url) as fin: + with temp_dir() as dir: + fn = network.download(fin, dir) + arc = archive.from_file(fn) + return arc.get_meta() + + else: + assert False class WithSpecLocal(WithSpec): """ @@ -403,35 +420,30 @@ return subp - def get_spec(self): - return super(WithSpecLocal, self).get_spec(_can_be_local=True) - + def get_spec(self, **kwargs): + kwargs['_can_be_local'] = True + return super(WithSpecLocal, self).get_spec(**kwargs) -import shutil -import tempfile -from pgxnclient.utils.zip import unpack -class WithUnpacking(object): +class WithSpecUrl(WithSpec): """ - Mixin to implement commands that may deal with zip files. + Mixin to implement commands that can also refer to a URL. """ - def call_with_temp_dir(self, f, *args, **kwargs): - """ - Call a function in the context of a temporary directory. - Create the temp directory and pass its name as first argument to *f*. - Other arguments and keywords are passed to *f* too. Upon exit delete - the directory. - """ - dir = tempfile.mkdtemp() - try: - return f(dir, *args, **kwargs) - finally: - shutil.rmtree(dir) - - def unpack(self, zipname, destdir): - """Unpack the zip file *zipname* into *destdir*.""" - return unpack(zipname, destdir) + @classmethod + def customize_parser(self, parser, subparsers, epilog=None, **kwargs): + epilog = _(""" +SPEC may also be an url specifying a protocol such as 'http://' or 'https://'. +""") + (epilog or "") + + subp = super(WithSpecUrl, self).customize_parser( + parser, subparsers, epilog=epilog, **kwargs) + + return subp + + def get_spec(self, **kwargs): + kwargs['_can_be_url'] = True + return super(WithSpecUrl, self).get_spec(**kwargs) class WithPgConfig(object): @@ -446,8 +458,8 @@ subp = super(WithPgConfig, self).customize_parser( parser, subparsers, **kwargs) - subp.add_argument('--pg_config', metavar="PATH", default='pg_config', - help = _("path to the pg_config executable to find the database" + subp.add_argument('--pg_config', metavar="PROG", default='pg_config', + help = _("the pg_config executable to find the database" " [default: %(default)s]")) return subp @@ -460,7 +472,7 @@ return _cache[what] logger.debug("running pg_config --%s", what) - cmdline = [self.opts.pg_config, "--%s" % what] + cmdline = [self.get_pg_config(), "--%s" % what] p = self.popen(cmdline, stdout=PIPE) out, err = p.communicate() if p.returncode: @@ -471,13 +483,41 @@ rv = _cache[what] = out return rv + def get_pg_config(self): + """ + Return the absolute path of the pg_config binary. + """ + pg_config = self.opts.pg_config + if os.path.split(pg_config)[0]: + pg_config = os.path.abspath(pg_config) + else: + pg_config = find_executable(pg_config) + if not pg_config: + raise PgxnClientException(_("pg_config executable not found")) + return pg_config + import shlex -class WithMake(WithPgConfig, WithUnpacking): +class WithMake(WithPgConfig): """ Mixin to implement commands that should invoke :program:`make`. """ + @classmethod + def customize_parser(self, parser, subparsers, **kwargs): + """ + Add the ``--make`` option to the options parser. + """ + subp = super(WithMake, self).customize_parser( + parser, subparsers, **kwargs) + + subp.add_argument('--make', metavar="PROG", + default=self._find_default_make(), + help = _("the 'make' executable to use to build the extension " + "[default: %(default)s]")) + + return subp + def run_make(self, cmd, dir, env=None, sudo=None): """Invoke make with the selected command. @@ -500,13 +540,7 @@ if sudo: cmdline.extend(shlex.split(sudo)) - # convert to absolute path for makefile, or else it may miss it - # if the cwd is changed during execution - pg_config = self.opts.pg_config - if os.path.split(pg_config)[0]: - pg_config = os.path.abspath(pg_config) - - cmdline.extend(['make', 'PG_CONFIG=%s' % pg_config]) + cmdline.extend([self.get_make(), 'PG_CONFIG=%s' % self.get_pg_config()]) if isinstance(cmd, basestring): cmdline.append(cmd) @@ -520,6 +554,48 @@ raise ProcessError(_("command returned %s: %s") % (p.returncode, ' '.join(cmdline))) + def get_make(self, _cache=[]): + """ + Return the path of the make binary. + """ + # the cache is not for performance but to return a consistent value + # even if the cwd is changed + if _cache: + return _cache[0] + + make = self.opts.make + + if os.path.split(make)[0]: + # At least a relative dir specified. + if not os.path.exists(make): + raise PgxnClientException(_("make executable not found: %s") + % make) + + # Convert to abs path to be robust in case the dir is changed. + make = os.path.abspath(make) + + else: + # we don't find make here and convert to abs path because it's a + # security hole: make may be run under sudo and in this case we + # don't want root to execute a make hacked in an user local dir + if not find_executable(make): + raise PgxnClientException(_("make executable not found: %s") + % make) + + _cache.append(make) + return make + + @classmethod + def _find_default_make(self): + for make in ('gmake', 'make'): + path = find_executable(make) + if path: + return make + + # if nothing was found, fall back on 'gmake'. If it was missing we + # will give an error when attempting to use it + return 'gmake' + class WithSudo(object): """ @@ -531,11 +607,12 @@ parser, subparsers, **kwargs) g = subp.add_mutually_exclusive_group() - g.add_argument('--sudo', metavar="PROG", default='sudo', + g.add_argument('--sudo', metavar="PROG", const='sudo', nargs="?", help = _("run PROG to elevate privileges when required" - " [default: %(default)s]")) + " [default: %(const)s]")) g.add_argument('--nosudo', dest='sudo', action='store_false', - help = _("never elevate privileges")) + help = _("never elevate privileges " + "(no more needed: for backward compatibility)")) return subp diff -Nru pgxnclient-1.0.3/pgxnclient/commands/help.py pgxnclient-1.2.1/pgxnclient/commands/help.py --- pgxnclient-1.0.3/pgxnclient/commands/help.py 2012-03-13 04:00:05.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/commands/help.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,7 +2,7 @@ pgxnclient -- help commands implementation """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/commands/info.py pgxnclient-1.2.1/pgxnclient/commands/info.py --- pgxnclient-1.0.3/pgxnclient/commands/info.py 2012-03-13 04:00:05.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/commands/info.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,7 +2,7 @@ pgxnclient -- informative commands implementation """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/commands/install.py pgxnclient-1.2.1/pgxnclient/commands/install.py --- pgxnclient-1.0.3/pgxnclient/commands/install.py 2012-05-19 20:33:27.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/commands/install.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,31 +2,35 @@ pgxnclient -- installation/loading commands implementation """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client +from __future__ import with_statement + import os import re -import sys import shutil import difflib import logging +import tempfile from subprocess import PIPE from pgxnclient import SemVer +from pgxnclient import archive +from pgxnclient import network from pgxnclient.i18n import _, N_ -from pgxnclient.utils import sha1 -from pgxnclient.errors import BadChecksum, PgxnClientException -from pgxnclient.network import download +from pgxnclient.utils import sha1, b +from pgxnclient.errors import BadChecksum, PgxnClientException, InsufficientPrivileges from pgxnclient.commands import Command, WithDatabase, WithMake, WithPgConfig -from pgxnclient.commands import WithSpec, WithSpecLocal, WithSudo +from pgxnclient.commands import WithSpecUrl, WithSpecLocal, WithSudo +from pgxnclient.utils.temp import temp_dir from pgxnclient.utils.strings import Identifier logger = logging.getLogger('pgxnclient.commands') -class Download(WithSpec, Command): +class Download(WithSpecUrl, Command): name = 'download' description = N_("download a distribution from the network") @@ -41,6 +45,11 @@ def run(self): spec = self.get_spec() + assert not spec.is_local() + + if spec.is_url(): + return self._run_url(spec) + data = self.get_meta(spec) try: @@ -49,12 +58,18 @@ raise PgxnClientException( "sha1 missing from the distribution meta") - fin = self.api.download(data['name'], SemVer(data['version'])) - fn = self._get_local_file_name(fin.url) - fn = download(fin, fn, rename=True) + with self.api.download(data['name'], SemVer(data['version'])) as fin: + fn = network.download(fin, self.opts.target) + self.verify_checksum(fn, chk) return fn + def _run_url(self, spec): + with network.get_file(spec.url) as fin: + fn = network.download(fin, self.opts.target) + + return fn + def verify_checksum(self, fn, chk): """Verify that a downloaded file has the expected sha1.""" sha = sha1() @@ -75,34 +90,27 @@ fn, sha, chk) raise BadChecksum(_("bad sha1 in downloaded file")) - def _get_local_file_name(self, url): - from urlparse import urlsplit - if os.path.isdir(self.opts.target): - basename = urlsplit(url)[2].rsplit('/', 1)[-1] - fn = os.path.join(self.opts.target, basename) - else: - fn = self.opts.target - - return os.path.abspath(fn) - -class InstallUninstall(WithMake, WithSpecLocal, Command): +class InstallUninstall(WithMake, WithSpecUrl, WithSpecLocal, Command): """ Base class to implement the ``install`` and ``uninstall`` commands. """ def run(self): - return self.call_with_temp_dir(self._run) + with temp_dir() as dir: + return self._run(dir) def _run(self, dir): spec = self.get_spec() if spec.is_dir(): pdir = os.path.abspath(spec.dirname) elif spec.is_file(): - pdir = self.unpack(spec.filename, dir) - else: # download + pdir = archive.from_file(spec.filename).unpack(dir) + elif not spec.is_local(): self.opts.target = dir fn = Download(self.opts).run() - pdir = self.unpack(fn, dir) + pdir = archive.from_file(fn).unpack(dir) + else: + assert False self.maybe_run_configure(pdir) @@ -126,7 +134,49 @@ _("configure failed with return code %s") % p.returncode) -class Install(WithSudo, InstallUninstall): +class SudoInstallUninstall(WithSudo, InstallUninstall): + """ + Installation commands base class supporting sudo operations. + """ + def run(self): + if not self.is_libdir_writable() and not self.opts.sudo: + dir = self.call_pg_config('libdir') + raise InsufficientPrivileges(_( + "PostgreSQL library directory (%s) not writable: " + "you should run the program as superuser, or specify " + "a 'sudo' program") % dir) + + return super(SudoInstallUninstall, self).run() + + def get_sudo_prog(self): + if self.is_libdir_writable(): + return None # not needed + + return self.opts.sudo + + def is_libdir_writable(self): + """ + Check if the Postgres installation directory is writable. + + If it is, we will assume that sudo is not required to + install/uninstall the library, so the sudo program will not be invoked + or its specification will not be required. + """ + dir = self.call_pg_config('libdir') + logger.debug("testing if %s is writable", dir) + try: + f = tempfile.TemporaryFile(prefix="pgxn-", suffix=".test", dir=dir) + f.write(b('test')) + f.close() + except (IOError, OSError): + rv = False + else: + rv = True + + return rv + + +class Install(SudoInstallUninstall): name = 'install' description = N_("download, build and install a distribution") @@ -135,16 +185,16 @@ self.run_make('all', dir=pdir) logger.info(_("installing extension")) - self.run_make('install', dir=pdir, sudo=self.opts.sudo) + self.run_make('install', dir=pdir, sudo=self.get_sudo_prog()) -class Uninstall(WithSudo, InstallUninstall): +class Uninstall(SudoInstallUninstall): name = 'uninstall' description = N_("remove a distribution from the system") def _inun(self, pdir): logger.info(_("removing extension")) - self.run_make('uninstall', dir=pdir, sudo=self.opts.sudo) + self.run_make('uninstall', dir=pdir, sudo=self.get_sudo_prog()) class Check(WithDatabase, InstallUninstall): @@ -176,7 +226,7 @@ raise -class LoadUnload(WithPgConfig, WithDatabase, WithSpecLocal, Command): +class LoadUnload(WithPgConfig, WithDatabase, WithSpecUrl, WithSpecLocal, Command): """ Base class to implement the ``load`` and ``unload`` commands. """ @@ -237,9 +287,9 @@ # load via pipe to enable psql commands in the file if not data: logger.debug("loading sql from %s", filename) - fin = open(filename, 'r') - p = self.popen(cmdline, stdin=fin) - p.communicate() + with open(filename, 'r') as fin: + p = self.popen(cmdline, stdin=fin) + p.communicate() else: if len(data) > 105: tdata = data[:100] + "..." @@ -247,6 +297,9 @@ tdata = data logger.debug('running sql command: "%s"', tdata) p = self.popen(cmdline, stdin=PIPE) + # for Python 3: just assume default encoding will do + if isinstance(data, unicode): + data = data.encode() p.communicate(data) if p.returncode: @@ -357,7 +410,7 @@ # not mangled by the PGXN upload script yet. name = dist['name'] for ext in self.opts.extensions: - if ext <> name: + if ext != name: raise PgxnClientException( "can't find extension '%s' in the distribution '%s'" % (name, spec)) @@ -368,9 +421,7 @@ if not self.opts.extensions: # All the extensions, in the order specified - if len(dist['provides']) > 1 and sys.version_info < (2, 5): - logger.warn(_("can't guarantee extensions load order " - "with Python < 2.5")) + # (assume we got an orddict from json) for name, data in dist['provides'].items(): rv.append((name, data.get('file'))) else: diff -Nru pgxnclient-1.0.3/pgxnclient/errors.py pgxnclient-1.2.1/pgxnclient/errors.py --- pgxnclient-1.0.3/pgxnclient/errors.py 2012-03-13 04:00:05.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/errors.py 2013-05-12 15:06:13.000000000 +0000 @@ -6,7 +6,7 @@ """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client @@ -25,6 +25,9 @@ class ProcessError(PgxnClientException): """An error raised calling an external program.""" +class InsufficientPrivileges(PgxnClientException): + """Operation will fail because the user is too lame.""" + class NotFound(PgxnException): """Something requested by the user not found on PGXN""" diff -Nru pgxnclient-1.0.3/pgxnclient/i18n.py pgxnclient-1.2.1/pgxnclient/i18n.py --- pgxnclient-1.0.3/pgxnclient/i18n.py 2011-05-01 23:51:11.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/i18n.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,7 +2,7 @@ pgxnclient -- internationalization support """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-check pgxnclient-1.2.1/pgxnclient/libexec/pgxn-check --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-check 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-check 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-download pgxnclient-1.2.1/pgxnclient/libexec/pgxn-download --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-download 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-download 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-help pgxnclient-1.2.1/pgxnclient/libexec/pgxn-help --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-help 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-help 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-info pgxnclient-1.2.1/pgxnclient/libexec/pgxn-info --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-info 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-info 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-install pgxnclient-1.2.1/pgxnclient/libexec/pgxn-install --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-install 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-install 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-load pgxnclient-1.2.1/pgxnclient/libexec/pgxn-load --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-load 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-load 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-mirror pgxnclient-1.2.1/pgxnclient/libexec/pgxn-mirror --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-mirror 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-mirror 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-search pgxnclient-1.2.1/pgxnclient/libexec/pgxn-search --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-search 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-search 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-uninstall pgxnclient-1.2.1/pgxnclient/libexec/pgxn-uninstall --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-uninstall 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-uninstall 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/libexec/pgxn-unload pgxnclient-1.2.1/pgxnclient/libexec/pgxn-unload --- pgxnclient-1.0.3/pgxnclient/libexec/pgxn-unload 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/libexec/pgxn-unload 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- command line interface """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/network.py pgxnclient-1.2.1/pgxnclient/network.py --- pgxnclient-1.0.3/pgxnclient/network.py 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/network.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,13 +2,15 @@ pgxnclient -- network interaction """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client import os import urllib2 +from urlparse import urlsplit from itertools import count +from contextlib import closing from pgxnclient import __version__ from pgxnclient.i18n import _ @@ -22,7 +24,7 @@ opener.addheaders = [('User-agent', 'pgxnclient/%s' % __version__)] logger.debug('opening url: %s', url) try: - return opener.open(url) + return closing(opener.open(url)) except urllib2.HTTPError, e: if e.code == 404: raise ResourceNotFound(_("resource not found: '%s'") % e.url) @@ -38,16 +40,33 @@ except urllib2.URLError, e: raise NetworkError(_("network error: %s") % e.reason) +def get_local_file_name(target, url): + """Return a good name for a local file. + + If *target* is a dir, make a name out of the url. Otherwise return target + itself. Always return an absolute path. + """ + if os.path.isdir(target): + basename = urlsplit(url)[2].rsplit('/', 1)[-1] + fn = os.path.join(target, basename) + else: + fn = target + + return os.path.abspath(fn) + def download(f, fn, rename=True): """Download a file locally. - :param f: open file read - :param fn: name of the file to write + :param f: open file to read + :param fn: name of the file to write. If a dir, save into it. :param rename: if true and a file *fn* exist, rename the downloaded file adding a prefix ``-1``, ``-2``... before the extension. Return the name of the file saved. """ + if os.path.isdir(fn): + fn = get_local_file_name(fn, f.url) + if rename: if os.path.exists(fn): base, ext = os.path.splitext(fn) diff -Nru pgxnclient-1.0.3/pgxnclient/spec.py pgxnclient-1.2.1/pgxnclient/spec.py --- pgxnclient-1.0.3/pgxnclient/spec.py 2011-11-27 23:10:17.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/spec.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,13 +2,14 @@ pgxnclient -- specification object """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client import os import re +import urllib import operator as _op from pgxnclient.i18n import _ @@ -33,14 +34,18 @@ 'stable': STABLE, } def __init__(self, name=None, op=None, ver=None, - dirname=None, filename=None): + dirname=None, filename=None, url=None): self.name = name and name.lower() self.op = op self.ver = ver - # point to local files + # point to local files or specific resources self.dirname = dirname self.filename = filename + self.url = url + + def is_name(self): + return self.name is not None def is_dir(self): return self.dirname is not None @@ -48,11 +53,14 @@ def is_file(self): return self.filename is not None + def is_url(self): + return self.url is not None + def is_local(self): return self.is_dir() or self.is_file() def __str__(self): - name = self.name or self.filename or self.dirname or "???" + name = self.name or self.filename or self.dirname or self.url or "???" if self.op is None: return name else: @@ -64,14 +72,28 @@ Raise BadSpecError if couldn't parse. """ - if os.sep in spec: + # check if it's a network resource + if spec.startswith('http://') or spec.startswith('https://'): + return Spec(url=spec) + + # check if it's a local resource + if spec.startswith('file://'): + try_file = urllib.unquote_plus(spec[len('file://'):]) + elif os.sep in spec: + try_file = spec + else: + try_file = None + + if try_file: # This is a local thing, let's see what - if os.path.isdir(spec): - return Spec(dirname=spec) - elif os.path.exists(spec): - return Spec(filename=spec) + if os.path.isdir(try_file): + return Spec(dirname=try_file) + elif os.path.exists(try_file): + return Spec(filename=try_file) else: - raise ResourceNotFound(_("cannot find '%s'") % spec) + raise ResourceNotFound(_("cannot find '%s'") % try_file) + + # so we think it's a PGXN spec # split operator/version and name m = re.match(r'(.+?)(?:(==|=|>=|>|<=|<)(.*))?$', spec) diff -Nru pgxnclient-1.0.3/pgxnclient/tar.py pgxnclient-1.2.1/pgxnclient/tar.py --- pgxnclient-1.0.3/pgxnclient/tar.py 1970-01-01 00:00:00.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/tar.py 2013-05-12 15:06:13.000000000 +0000 @@ -0,0 +1,69 @@ +""" +pgxnclient -- tar file utilities +""" + +# Copyright (C) 2011-2012 Daniele Varrazzo + +# This file is part of the PGXN client + +import os +import tarfile + +from pgxnclient.i18n import _ +from pgxnclient.errors import PgxnClientException +from pgxnclient.archive import Archive + +import logging +logger = logging.getLogger('pgxnclient.tar') + + +class TarArchive(Archive): + """Handle .tar archives""" + _file = None + + def can_open(self): + return tarfile.is_tarfile(self.filename) + + def open(self): + assert not self._file, "archive already open" + try: + self._file = tarfile.open(self.filename, 'r') + except Exception, e: + raise PgxnClientException( + _("cannot open archive '%s': %s") % (self.filename, e)) + + def close(self): + if self._file is not None: + self._file.close() + self._file = None + + def list_files(self): + assert self._file, "archive not open" + return self._file.getnames() + + def read(self, fn): + assert self._file, "archive not open" + return self._file.extractfile(fn).read() + + def unpack(self, destdir): + tarname = self.filename + logger.info(_("unpacking: %s"), tarname) + destdir = os.path.abspath(destdir) + self.open() + try: + for fn in self.list_files(): + fname = os.path.abspath(os.path.join(destdir, fn)) + if not fname.startswith(destdir): + raise PgxnClientException( + _("archive file '%s' trying to escape!") % fname) + + self._file.extractall(path=destdir) + finally: + self.close() + + return self._find_work_directory(destdir) + + +def unpack(filename, destdir): + return TarArchive(filename).unpack(destdir) + diff -Nru pgxnclient-1.0.3/pgxnclient/tests/__init__.py pgxnclient-1.2.1/pgxnclient/tests/__init__.py --- pgxnclient-1.0.3/pgxnclient/tests/__init__.py 2011-09-02 23:55:11.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/tests/__init__.py 2013-05-12 15:06:13.000000000 +0000 @@ -8,7 +8,7 @@ not installed but only avaliable in the sdist. """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client @@ -22,6 +22,13 @@ import unittest +# fix unittest maintainers stubborness: see Python issue #9424 +if unittest.TestCase.assert_ is not unittest.TestCase.assertTrue: + # Vaffanculo, Wolf + unittest.TestCase.assert_ = unittest.TestCase.assertTrue + unittest.TestCase.assertEquals = unittest.TestCase.assertEqual + + if __name__ == '__main__': unittest.main() diff -Nru pgxnclient-1.0.3/pgxnclient/tests/test_archives.py pgxnclient-1.2.1/pgxnclient/tests/test_archives.py --- pgxnclient-1.0.3/pgxnclient/tests/test_archives.py 1970-01-01 00:00:00.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/tests/test_archives.py 2013-05-12 15:06:13.000000000 +0000 @@ -0,0 +1,67 @@ +from pgxnclient import tar +from pgxnclient import zip +from pgxnclient import archive + +from pgxnclient.tests import unittest +from pgxnclient.errors import PgxnClientException +from pgxnclient.tests.testutils import get_test_filename + +class TestArchive(unittest.TestCase): + def test_from_file_zip(self): + fn = get_test_filename('foobar-0.42.1.zip') + a = archive.from_file(fn) + self.assert_(isinstance(a, zip.ZipArchive)) + self.assertEqual(a.filename, fn) + + def test_from_file_tar(self): + fn = get_test_filename('foobar-0.42.1.tar.gz') + a = archive.from_file(fn) + self.assert_(isinstance(a, tar.TarArchive)) + self.assertEqual(a.filename, fn) + + def test_from_file_unknown(self): + fn = get_test_filename('META-manyext.json') + self.assertRaises(PgxnClientException(archive.from_file, fn)) + + +class TestZipArchive(unittest.TestCase): + def test_can_open(self): + fn = get_test_filename('foobar-0.42.1.zip') + a = zip.ZipArchive(fn) + self.assert_(a.can_open()) + a.open() + a.close() + + def test_can_open_noext(self): + fn = get_test_filename('zip.ext') + a = zip.ZipArchive(fn) + self.assert_(a.can_open()) + a.open() + a.close() + + def test_cannot_open(self): + fn = get_test_filename('foobar-0.42.1.tar.gz') + a = zip.ZipArchive(fn) + self.assert_(not a.can_open()) + + +class TestTarArchive(unittest.TestCase): + def test_can_open(self): + fn = get_test_filename('foobar-0.42.1.tar.gz') + a = tar.TarArchive(fn) + self.assert_(a.can_open()) + a.open() + a.close() + + def test_can_open_noext(self): + fn = get_test_filename('tar.ext') + a = tar.TarArchive(fn) + self.assert_(a.can_open()) + a.open() + a.close() + + def test_cannot_open(self): + fn = get_test_filename('foobar-0.42.1.zip') + a = tar.TarArchive(fn) + self.assert_(not a.can_open()) + diff -Nru pgxnclient-1.0.3/pgxnclient/tests/test_commands.py pgxnclient-1.2.1/pgxnclient/tests/test_commands.py --- pgxnclient-1.0.3/pgxnclient/tests/test_commands.py 2012-03-13 04:00:05.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/tests/test_commands.py 2013-05-12 15:06:13.000000000 +0000 @@ -6,7 +6,7 @@ from urllib import quote from pgxnclient.utils import b -from pgxnclient.errors import PgxnClientException, ResourceNotFound +from pgxnclient.errors import PgxnClientException, ResourceNotFound, InsufficientPrivileges from pgxnclient.tests import unittest from pgxnclient.tests.testutils import ifunlink, get_test_filename @@ -15,6 +15,13 @@ self._f = open(*args) self.url = None + def __enter__(self): + self._f.__enter__() + return self + + def __exit__(self, type, value, traceback): + self._f.__exit__(type, value, traceback) + def __getattr__(self, attr): return getattr(self._f, attr) @@ -27,11 +34,17 @@ f.url = url return f +def fake_pg_config(**map): + def f(what): + return map[what] + + return f + class InfoTestCase(unittest.TestCase): def _get_output(self, cmdline): @patch('sys.stdout') - @patch('pgxnclient.api.get_file') + @patch('pgxnclient.network.get_file') def do(mock, stdout): mock.side_effect = fake_get_file from pgxnclient.cli import main @@ -68,6 +81,35 @@ foobar 0.42.0 stable """)) + def test_mirrors_list(self): + output = self._get_output(['mirror']) + self.assertEqual(output, b("""\ +http://pgxn.depesz.com/ +http://www.postgres-support.ch/pgxn/ +http://pgxn.justatheory.com/ +http://pgxn.darkixion.com/ +http://mirrors.cat.pdx.edu/pgxn/ +http://pgxn.dalibo.org/ +http://pgxn.cxsoftware.org/ +http://api.pgxn.org/ +""")) + + def test_mirror_info(self): + output = self._get_output(['mirror', 'http://pgxn.justatheory.com/']) + self.assertEqual(output, b("""\ +uri: http://pgxn.justatheory.com/ +frequency: daily +location: Portland, OR, USA +bandwidth: Cable +organization: David E. Wheeler +email: justatheory.com|pgxn +timezone: America/Los_Angeles +src: rsync://master.pgxn.org/pgxn/ +rsync: +notes: + +""")) + class CommandTestCase(unittest.TestCase): def test_popen_raises(self): @@ -78,7 +120,7 @@ class DownloadTestCase(unittest.TestCase): - @patch('pgxnclient.api.get_file') + @patch('pgxnclient.network.get_file') def test_download_latest(self, mock): mock.side_effect = fake_get_file @@ -92,7 +134,7 @@ finally: ifunlink(fn) - @patch('pgxnclient.api.get_file') + @patch('pgxnclient.network.get_file') def test_download_testing(self, mock): mock.side_effect = fake_get_file @@ -106,7 +148,21 @@ finally: ifunlink(fn) - @patch('pgxnclient.api.get_file') + @patch('pgxnclient.network.get_file') + def test_download_url(self, mock): + mock.side_effect = fake_get_file + + fn = 'foobar-0.43.2b1.zip' + self.assert_(not os.path.exists(fn)) + + from pgxnclient.cli import main + try: + main(['download', 'http://api.pgxn.org/dist/foobar/0.43.2b1/foobar-0.43.2b1.zip']) + self.assert_(os.path.exists(fn)) + finally: + ifunlink(fn) + + @patch('pgxnclient.network.get_file') def test_download_ext(self, mock): mock.side_effect = fake_get_file @@ -120,7 +176,7 @@ finally: ifunlink(fn) - @patch('pgxnclient.api.get_file') + @patch('pgxnclient.network.get_file') def test_download_rename(self, mock): mock.side_effect = fake_get_file @@ -153,7 +209,7 @@ ifunlink(fn1) ifunlink(fn2) - @patch('pgxnclient.api.get_file') + @patch('pgxnclient.network.get_file') def test_download_bad_sha1(self, mock): def fakefake(url): return fake_get_file(url, urlmap = { @@ -176,7 +232,7 @@ finally: ifunlink(fn) - @patch('pgxnclient.api.get_file') + @patch('pgxnclient.network.get_file') def test_download_case_insensitive(self, mock): mock.side_effect = fake_get_file @@ -265,171 +321,249 @@ self.assertEqual(res, cmd.get_best_version(data, spec)) +class Assertions(object): -class InstallTestCase(unittest.TestCase): - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_install_latest(self, mock_get, mock_popen): - mock_get.side_effect = fake_get_file - pop = mock_popen.return_value - pop.returncode = 0 + make = object() + + def assertCallArgs(self, pattern, args): + if len(pattern) != len(args): + self.fail('args and pattern have different lengths') + for p, a in zip(pattern, args): + if p is self.make: + if not a.endswith('make'): + self.fail('%s is not a make in %s' % (a, args)) + else: + if not a == p: + self.fail('%s is not a %s in %s' % (a, p, args)) + +# With mock patching a method seems tricky: looks there's no way to get to +# 'self' as the mock method is unbound. +from pgxnclient.tar import TarArchive +TarArchive.unpack_orig = TarArchive.unpack + +from pgxnclient.zip import ZipArchive +ZipArchive.unpack_orig = ZipArchive.unpack + +class InstallTestCase(unittest.TestCase, Assertions): + + def setUp(self): + self._p1 = patch('pgxnclient.network.get_file') + self.mock_get = self._p1.start() + self.mock_get.side_effect = fake_get_file + + self._p2 = patch('pgxnclient.commands.Popen') + self.mock_popen = self._p2.start() + self.mock_popen.return_value.returncode = 0 + + self._p3 = patch('pgxnclient.commands.WithPgConfig.call_pg_config') + self.mock_pgconfig = self._p3.start() + self.mock_pgconfig.side_effect = fake_pg_config( + libdir='/', bindir='/') + + def tearDown(self): + self._p1.stop() + self._p2.stop() + self._p3.stop() + + def test_install_latest(self): + from pgxnclient.cli import main + main(['install', '--sudo', '--', 'foobar']) + + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs(['sudo', self.make], self.mock_popen.call_args_list[1][0][0][:2]) + + def test_install_missing_sudo(self): + from pgxnclient.cli import main + self.assertRaises(InsufficientPrivileges, main, ['install', 'foobar']) + + def test_install_local(self): + self.mock_pgconfig.side_effect = fake_pg_config( + libdir=os.environ['HOME'], bindir='/') from pgxnclient.cli import main main(['install', 'foobar']) - self.assertEquals(mock_popen.call_count, 2) - self.assertEquals(['make'], mock_popen.call_args_list[0][0][0][:1]) - self.assertEquals(['sudo', 'make'], mock_popen.call_args_list[1][0][0][:2]) - - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_install_fails(self, mock_get, mock_popen): - mock_get.side_effect = fake_get_file - pop = mock_popen.return_value - pop.returncode = 1 + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[1][0][0][:1]) + + def test_install_url(self): + self.mock_pgconfig.side_effect = fake_pg_config( + libdir=os.environ['HOME'], bindir='/') + + from pgxnclient.cli import main + main(['install', 'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip']) + + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[1][0][0][:1]) + + def test_install_fails(self): + self.mock_popen.return_value.returncode = 1 + self.mock_pgconfig.side_effect = fake_pg_config( + libdir=os.environ['HOME'], bindir='/') from pgxnclient.cli import main self.assertRaises(PgxnClientException, main, ['install', 'foobar']) - self.assertEquals(mock_popen.call_count, 1) + self.assertEquals(self.mock_popen.call_count, 1) - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_install_bad_sha1(self, mock_get, mock_popen): + def test_install_bad_sha1(self): def fakefake(url): return fake_get_file(url, urlmap = { 'http://api.pgxn.org/dist/foobar/0.42.1/META.json': 'http://api.pgxn.org/dist/foobar/0.42.1/META-badsha1.json'}) - mock_get.side_effect = fakefake - pop = mock_popen.return_value - pop.returncode = 0 + self.mock_get.side_effect = fakefake from pgxnclient.cli import main from pgxnclient.errors import BadChecksum self.assertRaises(BadChecksum, - main, ['install', 'foobar']) + main, ['install', '--sudo', '--', 'foobar']) - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_install_nosudo(self, mock_get, mock_popen): - mock_get.side_effect = fake_get_file - pop = mock_popen.return_value - pop.returncode = 0 + def test_install_nosudo(self): + self.mock_pgconfig.side_effect = fake_pg_config(libdir=os.environ['HOME']) from pgxnclient.cli import main main(['install', '--nosudo', 'foobar']) - self.assertEquals(mock_popen.call_count, 2) - self.assertEquals(['make'], mock_popen.call_args_list[0][0][0][:1]) - self.assertEquals(['make'], mock_popen.call_args_list[1][0][0][:1]) - - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_install_sudo(self, mock_get, mock_popen): - mock_get.side_effect = fake_get_file - pop = mock_popen.return_value - pop.returncode = 0 + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[1][0][0][:1]) + def test_install_sudo(self): from pgxnclient.cli import main main(['install', '--sudo', 'gksudo -d "hello world"', 'foobar']) - self.assertEquals(mock_popen.call_count, 2) - self.assertEquals(['make'], mock_popen.call_args_list[0][0][0][:1]) - self.assertEquals(['gksudo', '-d', 'hello world', 'make'], - mock_popen.call_args_list[1][0][0][:4]) - - @patch('pgxnclient.commands.unpack') - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_install_local_zip(self, mock_get, mock_popen, mock_unpack): - mock_get.side_effect = lambda *args: self.fail('network invoked') - pop = mock_popen.return_value - pop.returncode = 0 - from pgxnclient.utils.zip import unpack - mock_unpack.side_effect = unpack + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], + self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs(['gksudo', '-d', 'hello world', self.make], + self.mock_popen.call_args_list[1][0][0][:4]) + + @patch('pgxnclient.tar.TarArchive.unpack') + def test_install_local_tar(self, mock_unpack): + fn = get_test_filename('foobar-0.42.1.tar.gz') + mock_unpack.side_effect = TarArchive(fn).unpack_orig + + from pgxnclient.cli import main + main(['install', '--sudo', '--', fn]) + + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs(['sudo', self.make], + self.mock_popen.call_args_list[1][0][0][:2]) + make_cwd = self.mock_popen.call_args_list[1][1]['cwd'] + + self.assertEquals(mock_unpack.call_count, 1) + tmpdir, = mock_unpack.call_args[0] + self.assertEqual(make_cwd, os.path.join(tmpdir, 'foobar-0.42.1')) + + @patch('pgxnclient.zip.ZipArchive.unpack') + def test_install_local_zip(self, mock_unpack): + fn = get_test_filename('foobar-0.42.1.zip') + mock_unpack.side_effect = ZipArchive(fn).unpack_orig from pgxnclient.cli import main - main(['install', get_test_filename('foobar-0.42.1.zip')]) - - self.assertEquals(mock_popen.call_count, 2) - self.assertEquals(['make'], mock_popen.call_args_list[0][0][0][:1]) - self.assertEquals(['sudo', 'make'], - mock_popen.call_args_list[1][0][0][:2]) - make_cwd = mock_popen.call_args_list[1][1]['cwd'] + main(['install', '--sudo', '--', fn]) + + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs(['sudo', self.make], + self.mock_popen.call_args_list[1][0][0][:2]) + make_cwd = self.mock_popen.call_args_list[1][1]['cwd'] self.assertEquals(mock_unpack.call_count, 1) - zipname, tmpdir = mock_unpack.call_args[0] - self.assertEqual(zipname, get_test_filename('foobar-0.42.1.zip')) + tmpdir, = mock_unpack.call_args[0] self.assertEqual(make_cwd, os.path.join(tmpdir, 'foobar-0.42.1')) - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_install_local_dir(self, mock_get, mock_popen): - mock_get.side_effect = lambda *args: self.fail('network invoked') - pop = mock_popen.return_value - pop.returncode = 0 + def test_install_url_file(self): + fn = get_test_filename('foobar-0.42.1.zip') + url = 'file://' + os.path.abspath(fn).replace("f", '%%%2x' % ord('f')) + + from pgxnclient.cli import main + main(['install', '--sudo', '--', url]) + + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs(['sudo', self.make], + self.mock_popen.call_args_list[1][0][0][:2]) + + def test_install_local_dir(self): + self.mock_get.side_effect = lambda *args: self.fail('network invoked') tdir = tempfile.mkdtemp() try: - from pgxnclient.utils.zip import unpack + from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) from pgxnclient.cli import main - main(['install', dir]) + main(['install', '--sudo', '--', dir]) finally: shutil.rmtree(tdir) - self.assertEquals(mock_popen.call_count, 2) - self.assertEquals(['make'], mock_popen.call_args_list[0][0][0][:1]) - self.assertEquals(dir, mock_popen.call_args_list[0][1]['cwd']) - self.assertEquals(['sudo', 'make'], - mock_popen.call_args_list[1][0][0][:2]) - self.assertEquals(dir, mock_popen.call_args_list[1][1]['cwd']) + self.assertEquals(self.mock_popen.call_count, 2) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + self.assertCallArgs(dir, self.mock_popen.call_args_list[0][1]['cwd']) + self.assertCallArgs(['sudo', self.make], + self.mock_popen.call_args_list[1][0][0][:2]) + self.assertEquals(dir, self.mock_popen.call_args_list[1][1]['cwd']) + + +class CheckTestCase(unittest.TestCase, Assertions): + def setUp(self): + self._p1 = patch('pgxnclient.network.get_file') + self.mock_get = self._p1.start() + self.mock_get.side_effect = fake_get_file + + self._p2 = patch('pgxnclient.commands.Popen') + self.mock_popen = self._p2.start() + self.mock_popen.return_value.returncode = 0 + + self._p3 = patch('pgxnclient.commands.WithPgConfig.call_pg_config') + self.mock_pgconfig = self._p3.start() + self.mock_pgconfig.side_effect = fake_pg_config( + libdir='/', bindir='/') + + def tearDown(self): + self._p1.stop() + self._p2.stop() + self._p3.stop() + def test_check_latest(self): + from pgxnclient.cli import main + main(['check', 'foobar']) -class CheckTestCase(unittest.TestCase): - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_check_latest(self, mock_get, mock_popen): - mock_get.side_effect = fake_get_file - pop = mock_popen.return_value - pop.returncode = 0 + self.assertEquals(self.mock_popen.call_count, 1) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) + def test_check_url(self): from pgxnclient.cli import main - main(['check', 'foobar']) + main(['check', 'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip']) - self.assertEquals(mock_popen.call_count, 1) - self.assertEquals(['make'], mock_popen.call_args_list[0][0][0][:1]) + self.assertEquals(self.mock_popen.call_count, 1) + self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1]) - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_check_fails(self, mock_get, mock_popen): - mock_get.side_effect = fake_get_file - pop = mock_popen.return_value - pop.returncode = 1 + def test_check_fails(self): + self.mock_popen.return_value.returncode = 1 from pgxnclient.cli import main self.assertRaises(PgxnClientException, main, ['check', 'foobar']) - self.assertEquals(mock_popen.call_count, 1) - - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_check_diff_moved(self, mock_get, mock_popen): - mock_get.side_effect = fake_get_file + self.assertEquals(self.mock_popen.call_count, 1) + def test_check_diff_moved(self): def create_regression_files(*args, **kwargs): cwd = kwargs['cwd'] open(os.path.join(cwd, 'regression.out'), 'w').close() open(os.path.join(cwd, 'regression.diffs'), 'w').close() return Mock() - mock_popen.side_effect = create_regression_files - pop = mock_popen.return_value - pop.returncode = 1 + self.mock_popen.side_effect = create_regression_files + self.mock_popen.return_value.returncode = 1 self.assert_(not os.path.exists('regression.out'), "Please remove temp file 'regression.out' from current dir") @@ -440,33 +574,49 @@ try: self.assertRaises(PgxnClientException, main, ['check', 'foobar']) - self.assertEquals(mock_popen.call_count, 1) + self.assertEquals(self.mock_popen.call_count, 1) self.assert_(os.path.exists('regression.out')) self.assert_(os.path.exists('regression.diffs')) finally: ifunlink('regression.out') ifunlink('regression.diffs') - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_check_bad_sha1(self, mock_get, mock_popen): + def test_check_bad_sha1(self): def fakefake(url): return fake_get_file(url, urlmap = { 'http://api.pgxn.org/dist/foobar/0.42.1/META.json': 'http://api.pgxn.org/dist/foobar/0.42.1/META-badsha1.json'}) - mock_get.side_effect = fakefake - pop = mock_popen.return_value - pop.returncode = 1 + self.mock_get.side_effect = fakefake + self.mock_popen.return_value.returncode = 1 from pgxnclient.cli import main from pgxnclient.errors import BadChecksum self.assertRaises(BadChecksum, main, ['check', 'foobar']) - self.assertEquals(mock_popen.call_count, 0) + self.assertEquals(self.mock_popen.call_count, 0) class LoadTestCase(unittest.TestCase): + def setUp(self): + self._p1 = patch('pgxnclient.commands.Popen') + self.mock_popen = self._p1.start() + self.mock_popen.return_value.returncode = 0 + self.mock_popen.return_value.communicate.return_value = (b(''), b('')) + + self._p2 = patch('pgxnclient.commands.install.LoadUnload.is_extension') + self.mock_isext = self._p2.start() + self.mock_isext.return_value = True + + self._p3 = patch('pgxnclient.commands.install.LoadUnload.get_pg_version') + self.mock_pgver = self._p3.start() + self.mock_pgver.return_value = (9,1,0) + + def tearDown(self): + self._p1.stop() + self._p2.stop() + self._p3.stop() + def test_parse_version(self): from pgxnclient.commands.install import Load cmd = Load(None) @@ -477,76 +627,67 @@ 'PostgreSQL 9.1alpha5 on i686-pc-linux-gnu, compiled by GCC gcc' ' (Ubuntu/Linaro 4.4.4-14ubuntu5) 4.4.5, 32-bit ')) - @patch('pgxnclient.commands.install.Load.is_extension') - @patch('pgxnclient.commands.install.Load.get_pg_version') - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_check_psql_options(self, - mock_get, mock_popen, mock_pgver, mock_isext): + @patch('pgxnclient.network.get_file') + def test_check_psql_options(self, mock_get): mock_get.side_effect = fake_get_file - pop = mock_popen.return_value - pop.returncode = 0 - pop.communicate.return_value = (b(''), b('')) - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True from pgxnclient.cli import main main(['load', '--yes', '--dbname', 'dbdb', 'foobar']) - args = mock_popen.call_args[0][0] + args = self.mock_popen.call_args[0][0] self.assertEqual('dbdb', args[args.index('--dbname') + 1]) main(['load', '--yes', '-U', 'meme', 'foobar']) - args = mock_popen.call_args[0][0] + args = self.mock_popen.call_args[0][0] self.assertEqual('meme', args[args.index('--username') + 1]) main(['load', '--yes', '--port', '666', 'foobar']) - args = mock_popen.call_args[0][0] + args = self.mock_popen.call_args[0][0] self.assertEqual('666', args[args.index('--port') + 1]) main(['load', '--yes', '-h', 'somewhere', 'foobar']) - args = mock_popen.call_args[0][0] + args = self.mock_popen.call_args[0][0] self.assertEqual('somewhere', args[args.index('--host') + 1]) - @patch('pgxnclient.commands.install.Load.is_extension') - @patch('pgxnclient.commands.install.Load.get_pg_version') - @patch('pgxnclient.commands.unpack') - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_load_local_zip(self, mock_get, mock_popen, mock_unpack, - mock_pgver, mock_isext): + @patch('pgxnclient.zip.ZipArchive.unpack') + @patch('pgxnclient.network.get_file') + def test_load_local_zip(self, mock_get, mock_unpack): mock_get.side_effect = lambda *args: self.fail('network invoked') - pop = mock_popen.return_value - pop.returncode = 0 - from pgxnclient.utils.zip import unpack - mock_unpack.side_effect = unpack - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True + mock_unpack.side_effect = ZipArchive.unpack_orig from pgxnclient.cli import main main(['load', '--yes', get_test_filename('foobar-0.42.1.zip')]) self.assertEquals(mock_unpack.call_count, 0) - self.assertEquals(mock_popen.call_count, 1) - self.assert_('psql' in mock_popen.call_args[0][0][0]) - self.assertEquals(pop.communicate.call_args[0][0], - 'CREATE EXTENSION foobar;') - - @patch('pgxnclient.commands.install.Load.is_extension') - @patch('pgxnclient.commands.install.Load.get_pg_version') - @patch('pgxnclient.commands.Popen') - @patch('pgxnclient.api.get_file') - def test_load_local_dir(self, mock_get, mock_popen, - mock_pgver, mock_isext): + self.assertEquals(self.mock_popen.call_count, 1) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args[0][0], + b('CREATE EXTENSION foobar;')) + + @patch('pgxnclient.tar.TarArchive.unpack') + @patch('pgxnclient.network.get_file') + def test_load_local_tar(self, mock_get, mock_unpack): + mock_get.side_effect = lambda *args: self.fail('network invoked') + mock_unpack.side_effect = TarArchive.unpack_orig + + from pgxnclient.cli import main + main(['load', '--yes', get_test_filename('foobar-0.42.1.tar.gz')]) + + self.assertEquals(mock_unpack.call_count, 0) + self.assertEquals(self.mock_popen.call_count, 1) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args[0][0], + b('CREATE EXTENSION foobar;')) + + @patch('pgxnclient.network.get_file') + def test_load_local_dir(self, mock_get): mock_get.side_effect = lambda *args: self.fail('network invoked') - pop = mock_popen.return_value - pop.returncode = 0 - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True tdir = tempfile.mkdtemp() try: - from pgxnclient.utils.zip import unpack + from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) from pgxnclient.cli import main @@ -555,23 +696,50 @@ finally: shutil.rmtree(tdir) - self.assertEquals(mock_popen.call_count, 1) - self.assert_('psql' in mock_popen.call_args[0][0][0]) - self.assertEquals(pop.communicate.call_args[0][0], - 'CREATE EXTENSION foobar;') - - @patch('pgxnclient.commands.install.Load.is_extension') - @patch('pgxnclient.commands.install.Load.get_pg_version') - @patch('pgxnclient.commands.Popen') - def test_load_extensions_order(self, mock_popen, mock_pgver, mock_isext): - pop = mock_popen.return_value - pop.returncode = 0 - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True + self.assertEquals(self.mock_popen.call_count, 1) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args[0][0], + b('CREATE EXTENSION foobar;')) + + @patch('pgxnclient.zip.ZipArchive.unpack') + @patch('pgxnclient.network.get_file') + def test_load_zip_url(self, mock_get, mock_unpack): + mock_get.side_effect = fake_get_file + mock_unpack.side_effect = ZipArchive.unpack_orig + + from pgxnclient.cli import main + main(['load', '--yes', + 'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip']) + + self.assertEquals(mock_unpack.call_count, 0) + self.assertEquals(self.mock_popen.call_count, 1) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args[0][0], + b('CREATE EXTENSION foobar;')) + + @patch('pgxnclient.tar.TarArchive.unpack') + @patch('pgxnclient.network.get_file') + def test_load_tar_url(self, mock_get, mock_unpack): + mock_get.side_effect = fake_get_file + mock_unpack.side_effect = TarArchive.unpack_orig + + from pgxnclient.cli import main + main(['load', '--yes', + 'http://example.org/foobar-0.42.1.tar.gz']) + + self.assertEquals(mock_unpack.call_count, 0) + self.assertEquals(self.mock_popen.call_count, 1) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args[0][0], + b('CREATE EXTENSION foobar;')) + def test_load_extensions_order(self): tdir = tempfile.mkdtemp() try: - from pgxnclient.utils.zip import unpack + from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), @@ -583,29 +751,22 @@ finally: shutil.rmtree(tdir) - self.assertEquals(mock_popen.call_count, 4) - self.assert_('psql' in mock_popen.call_args[0][0][0]) - self.assertEquals(pop.communicate.call_args_list[0][0][0], - 'CREATE EXTENSION foo;') - self.assertEquals(pop.communicate.call_args_list[1][0][0], - 'CREATE EXTENSION bar;') - self.assertEquals(pop.communicate.call_args_list[2][0][0], - 'CREATE EXTENSION baz;') - self.assertEquals(pop.communicate.call_args_list[3][0][0], - 'CREATE EXTENSION qux;') - - @patch('pgxnclient.commands.install.Unload.is_extension') - @patch('pgxnclient.commands.install.Unload.get_pg_version') - @patch('pgxnclient.commands.Popen') - def test_unload_extensions_order(self, mock_popen, mock_pgver, mock_isext): - pop = mock_popen.return_value - pop.returncode = 0 - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True + self.assertEquals(self.mock_popen.call_count, 4) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args_list[0][0][0], + b('CREATE EXTENSION foo;')) + self.assertEquals(communicate.call_args_list[1][0][0], + b('CREATE EXTENSION bar;')) + self.assertEquals(communicate.call_args_list[2][0][0], + b('CREATE EXTENSION baz;')) + self.assertEquals(communicate.call_args_list[3][0][0], + b('CREATE EXTENSION qux;')) + def test_unload_extensions_order(self): tdir = tempfile.mkdtemp() try: - from pgxnclient.utils.zip import unpack + from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), @@ -617,29 +778,22 @@ finally: shutil.rmtree(tdir) - self.assertEquals(mock_popen.call_count, 4) - self.assert_('psql' in mock_popen.call_args[0][0][0]) - self.assertEquals(pop.communicate.call_args_list[0][0][0], - 'DROP EXTENSION qux;') - self.assertEquals(pop.communicate.call_args_list[1][0][0], - 'DROP EXTENSION baz;') - self.assertEquals(pop.communicate.call_args_list[2][0][0], - 'DROP EXTENSION bar;') - self.assertEquals(pop.communicate.call_args_list[3][0][0], - 'DROP EXTENSION foo;') - - @patch('pgxnclient.commands.install.Load.is_extension') - @patch('pgxnclient.commands.install.Load.get_pg_version') - @patch('pgxnclient.commands.Popen') - def test_load_list(self, mock_popen, mock_pgver, mock_isext): - pop = mock_popen.return_value - pop.returncode = 0 - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True + self.assertEquals(self.mock_popen.call_count, 4) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args_list[0][0][0], + b('DROP EXTENSION qux;')) + self.assertEquals(communicate.call_args_list[1][0][0], + b('DROP EXTENSION baz;')) + self.assertEquals(communicate.call_args_list[2][0][0], + b('DROP EXTENSION bar;')) + self.assertEquals(communicate.call_args_list[3][0][0], + b('DROP EXTENSION foo;')) + def test_load_list(self): tdir = tempfile.mkdtemp() try: - from pgxnclient.utils.zip import unpack + from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), @@ -651,25 +805,18 @@ finally: shutil.rmtree(tdir) - self.assertEquals(mock_popen.call_count, 2) - self.assert_('psql' in mock_popen.call_args[0][0][0]) - self.assertEquals(pop.communicate.call_args_list[0][0][0], - 'CREATE EXTENSION baz;') - self.assertEquals(pop.communicate.call_args_list[1][0][0], - 'CREATE EXTENSION foo;') - - @patch('pgxnclient.commands.install.Unload.is_extension') - @patch('pgxnclient.commands.install.Unload.get_pg_version') - @patch('pgxnclient.commands.Popen') - def test_unload_list(self, mock_popen, mock_pgver, mock_isext): - pop = mock_popen.return_value - pop.returncode = 0 - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True + self.assertEquals(self.mock_popen.call_count, 2) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args_list[0][0][0], + b('CREATE EXTENSION baz;')) + self.assertEquals(communicate.call_args_list[1][0][0], + b('CREATE EXTENSION foo;')) + def test_unload_list(self): tdir = tempfile.mkdtemp() try: - from pgxnclient.utils.zip import unpack + from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), @@ -681,25 +828,18 @@ finally: shutil.rmtree(tdir) - self.assertEquals(mock_popen.call_count, 2) - self.assert_('psql' in mock_popen.call_args[0][0][0]) - self.assertEquals(pop.communicate.call_args_list[0][0][0], - 'DROP EXTENSION baz;') - self.assertEquals(pop.communicate.call_args_list[1][0][0], - 'DROP EXTENSION foo;') - - @patch('pgxnclient.commands.install.Load.is_extension') - @patch('pgxnclient.commands.install.Load.get_pg_version') - @patch('pgxnclient.commands.Popen') - def test_load_missing(self, mock_popen, mock_pgver, mock_isext): - pop = mock_popen.return_value - pop.returncode = 0 - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True + self.assertEquals(self.mock_popen.call_count, 2) + self.assert_('psql' in self.mock_popen.call_args[0][0][0]) + communicate = self.mock_popen.return_value.communicate + self.assertEquals(communicate.call_args_list[0][0][0], + b('DROP EXTENSION baz;')) + self.assertEquals(communicate.call_args_list[1][0][0], + b('DROP EXTENSION foo;')) + def test_load_missing(self): tdir = tempfile.mkdtemp() try: - from pgxnclient.utils.zip import unpack + from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), @@ -712,20 +852,12 @@ finally: shutil.rmtree(tdir) - self.assertEquals(mock_popen.call_count, 0) - - @patch('pgxnclient.commands.install.Unload.is_extension') - @patch('pgxnclient.commands.install.Unload.get_pg_version') - @patch('pgxnclient.commands.Popen') - def test_unload_missing(self, mock_popen, mock_pgver, mock_isext): - pop = mock_popen.return_value - pop.returncode = 0 - mock_pgver.return_value = (9,1,0) - mock_isext.return_value = True + self.assertEquals(self.mock_popen.call_count, 0) + def test_unload_missing(self): tdir = tempfile.mkdtemp() try: - from pgxnclient.utils.zip import unpack + from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), @@ -738,12 +870,26 @@ finally: shutil.rmtree(tdir) - self.assertEquals(mock_popen.call_count, 0) + self.assertEquals(self.mock_popen.call_count, 0) + + def test_missing_meta_dir(self): + # issue #19 + tdir = tempfile.mkdtemp() + try: + from pgxnclient.zip import unpack + dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) + os.unlink(os.path.join(dir, 'META.json')) + + from pgxnclient.cli import main + self.assertRaises(PgxnClientException, main, ['load', dir]) + + finally: + shutil.rmtree(tdir) class SearchTestCase(unittest.TestCase): @patch('sys.stdout') - @patch('pgxnclient.api.get_file') + @patch('pgxnclient.network.get_file') def test_search_quoting(self, mock_get, stdout): mock_get.side_effect = fake_get_file from pgxnclient.cli import main diff -Nru pgxnclient-1.0.3/pgxnclient/tests/test_semver.py pgxnclient-1.2.1/pgxnclient/tests/test_semver.py --- pgxnclient-1.0.3/pgxnclient/tests/test_semver.py 2011-11-29 22:51:08.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/tests/test_semver.py 2013-05-12 15:06:20.000000000 +0000 @@ -2,6 +2,9 @@ from pgxnclient import SemVer +# Tests based on +# https://github.com/theory/pg-semver/blob/master/test/sql/base.sql + class SemVerTestCase(unittest.TestCase): def test_ok(self): for s in [ @@ -11,8 +14,10 @@ '0.0.0', '0.1.999', '9999.9999999.823823', - '1.0.0beta1', - '1.0.0beta2', + '1.0.0beta1', # no more valid according to semver + '1.0.0beta2', # no more valid according to semver + '1.0.0-beta1', + '1.0.0-beta2', '1.0.0', '20110204.0.0', ]: self.assertEqual(SemVer(s), s) @@ -43,8 +48,8 @@ ('1.2.23', '1.2.23'), ('0.0.0', '0.0.0'), ('999.888.7777', '999.888.7777'), - ('0.1.2beta3', '0.1.2beta3'), - ('1.0.0rc-1', '1.0.0RC-1'), ]: + ('0.1.2-beta3', '0.1.2-beta3'), + ('1.0.0-rc-1', '1.0.0-RC-1'), ]: self.assertEqual(SemVer(s1), SemVer(s2)) self.assertEqual(hash(SemVer(s1)), hash(SemVer(s2))) self.assert_(SemVer(s1) <= SemVer(s2), @@ -58,10 +63,10 @@ ('0.0.1', '1.0.0'), ('1.0.1', '1.1.0'), ('1.1.1', '1.1.0'), - ('1.2.3b', '1.2.3'), - ('1.2.3', '1.2.3b'), - ('1.2.3a', '1.2.3b'), - ('1.2.3aaaaaaa1', '1.2.3aaaaaaa2'), ]: + ('1.2.3-b', '1.2.3'), + ('1.2.3', '1.2.3-b'), + ('1.2.3-a', '1.2.3-b'), + ('1.2.3-aaaaaaa1', '1.2.3-aaaaaaa2'), ]: self.assertNotEqual(SemVer(s1), SemVer(s2)) self.assertNotEqual(hash(SemVer(s1)), hash(SemVer(s2))) @@ -70,10 +75,10 @@ ('2.2.2', '1.1.1'), ('2.2.2', '2.1.1'), ('2.2.2', '2.2.1'), - ('2.2.2b', '2.2.1'), - ('2.2.2', '2.2.2b'), - ('2.2.2c', '2.2.2b'), - ('2.2.2rc-2', '2.2.2RC-1'), + ('2.2.2-b', '2.2.1'), + ('2.2.2', '2.2.2-b'), + ('2.2.2-c', '2.2.2-b'), + ('2.2.2-rc-2', '2.2.2-RC-1'), ('0.9.10', '0.9.9'), ]: self.assert_(SemVer(s1) >= SemVer(s2), "%s >= %s failed" % (s1, s2)) @@ -90,26 +95,30 @@ ('01.2.2', '1.2.2'), ('1.02.2', '1.2.2'), ('1.2.02', '1.2.2'), - ('1.2.02b', '1.2.2b'), - ('1.2.02beta-3 ', '1.2.2beta-3'), - ('1.02.02rc1', '1.2.2rc1'), + ('1.2.02b', '1.2.2-b'), + ('1.2.02beta-3 ', '1.2.2-beta-3'), + ('1.02.02rc1', '1.2.2-rc1'), ('1.0', '1.0.0'), ('1', '1.0.0'), ('.0.02', '0.0.2'), ('1..02', '1.0.2'), ('1..', '1.0.0'), ('1.1', '1.1.0'), - ('1.2.b1', '1.2.0b1'), - ('9.0beta4', '9.0.0beta4'), # PostgreSQL format. - ('9b', '9.0.0b'), - ('rc1', '0.0.0rc1'), + ('1.2.b1', '1.2.0-b1'), + ('9.0beta4', '9.0.0-beta4'), # PostgreSQL format. + ('9b', '9.0.0-b'), + ('rc1', '0.0.0-rc1'), ('', '0.0.0'), ('..2', '0.0.2'), - ('1.2.3 a', '1.2.3a'), - ('..2 b', '0.0.2b'), + ('1.2.3 a', '1.2.3-a'), + ('..2 b', '0.0.2-b'), (' 012.2.2', '12.2.2'), ('20110204', '20110204.0.0'), ]: - self.assertEqual(SemVer.clean(s1), SemVer(s2)) + try: + self.assertEqual(SemVer.clean(s1), SemVer(s2)) + except: + print s1, s2 + raise def test_cant_clean(self): def ar(s): diff -Nru pgxnclient-1.0.3/pgxnclient/tests/testutils.py pgxnclient-1.2.1/pgxnclient/tests/testutils.py --- pgxnclient-1.0.3/pgxnclient/tests/testutils.py 2011-05-09 08:19:38.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/tests/testutils.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,7 +2,7 @@ pgxnclient -- unit test utilities """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/utils/__init__.py pgxnclient-1.2.1/pgxnclient/utils/__init__.py --- pgxnclient-1.0.3/pgxnclient/utils/__init__.py 2011-11-29 22:51:08.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/utils/__init__.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,15 +2,17 @@ pgxnclient -- misc utilities package """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client -__all__ = ['OrderedDict', 'load_json', 'load_jsons', 'sha1', 'b'] +__all__ = ['OrderedDict', 'load_json', 'load_jsons', 'sha1', 'b', + 'find_executable'] import sys +import os # OrderedDict available from Python 2.7 if sys.version_info >= (2, 7): @@ -19,12 +21,13 @@ from pgxnclient.utils.ordereddict import OrderedDict -# Import the proper JSON library -# dependencies note: simplejson is certified for Python 2.5, and supports -# Python 2.4 up to version 2.0.9. After that the package is in the stdlib +# Import the proper JSON library. # -# We use json only from 2.7 as it supports ordered dicts. For Python 2.6 -# simplejson >= 2.1 should be used. +# Dependencies note: simplejson is certified for Python 2.5. Support for +# Python 2.4 was available up to version 2.0.9, but this version doesn't +# support ordered dicts. In Py 2.6 the package is in stdlib, but without +# orddict support, so we use simplejson 2.1 again. From Python 2.7 the stdlilb +# json module has orddict support so we don't need the external dependency. if sys.version_info >= (2, 7): import json else: @@ -37,20 +40,11 @@ return load_jsons(data) def load_jsons(data): - # order required to keep "provides" extensions in order. - # Python 2.4 is only compatible with a simplejson version that doesn't - # support ordered dict. - if sys.version_info >= (2, 5): - return json.loads(data, object_pairs_hook=OrderedDict) - else: - return json.loads(data) + return json.loads(data, object_pairs_hook=OrderedDict) # Import the sha1 object without warnings -if sys.version_info >= (2, 5): - from hashlib import sha1 -else: - from sha import new as sha1 +from hashlib import sha1 # For compatibility from Python 2.4 to 3.x @@ -62,3 +56,16 @@ def b(s): return s.encode('utf8') + +def find_executable(name): + """ + Find executable by ``name`` by inspecting PATH environment variable, return + ``None`` if nothing found. + """ + for dir in os.environ.get('PATH', '').split(os.pathsep): + if not dir: + continue + fn = os.path.abspath(os.path.join(dir, name)) + if os.path.exists(fn): + return os.path.abspath(fn) + diff -Nru pgxnclient-1.0.3/pgxnclient/utils/semver.py pgxnclient-1.2.1/pgxnclient/utils/semver.py --- pgxnclient-1.0.3/pgxnclient/utils/semver.py 2011-11-29 22:51:08.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/utils/semver.py 2013-05-12 15:06:20.000000000 +0000 @@ -1,10 +1,21 @@ """ -SemVer -- semantic version specification +SemVer -- (not quite) semantic version specification http://semver.org/ -This implementation is conform to the SemVer 0.2.1 implementation by David -Wheeler (http://pgxn.org/dist/semver/0.2.1/) and passes all its unit test. +IMPORTANT: don't trust this implementation. And don't trust SemVer AT ALL. +We have a bloody mess because the specification changed after being published +and after several extension had been uploaded with a version number that +suddenly had become no more valid. + +https://github.com/mojombo/semver.org/issues/49 + +My plea for forking the spec and keep our schema has been ignored. So this +module only tries to make sure people can use PGXN, not to be conform to an +half-aborted specification. End of rant. + +This implementation is conform to the SemVer 0.3.0 implementation by David +Wheeler (http://pgxn.org/dist/semver/0.3.0/) and passes all its unit test. Note that it is slightly non conform to the original specification, as the trailing part should be compared in ascii order while our comparison is not @@ -15,7 +26,7 @@ """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2013 Daniele Varrazzo # This file is part of the PGXN client @@ -46,6 +57,9 @@ @property def trail(self): return self.tuple[3] + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, str(self)) + def __eq__(self, other): if isinstance(other, SemVer): return self.tuple[:3] == other.tuple[:3] \ @@ -117,7 +131,7 @@ maj = maj and int(maj) or 0 min = min and int(min) or 0 patch = patch and int(patch) or 0 - trail = trail and trail.strip() or '' + trail = trail and '-' + trail.strip() or '' return "%d.%d.%d%s" % (maj, min, patch, trail) re_semver = re.compile(r""" @@ -125,7 +139,10 @@ (0|[1-9][0-9]*) \. (0|[1-9][0-9]*) \. (0|[1-9][0-9]*) - ([a-z][a-z0-9-]*)? + (?: + -? # should be mandatory, but see rant above + ([a-z][a-z0-9-]*) + )? $ """, re.IGNORECASE | re.VERBOSE) @@ -135,7 +152,10 @@ ([0-9]+)? \.? ([0-9]+)? \.? ([0-9]+)? - \s* ([a-z][a-z0-9-]*)? + (?: + -? \s* + ([a-z][a-z0-9-]*) + )? $ """, re.IGNORECASE | re.VERBOSE) diff -Nru pgxnclient-1.0.3/pgxnclient/utils/strings.py pgxnclient-1.2.1/pgxnclient/utils/strings.py --- pgxnclient-1.0.3/pgxnclient/utils/strings.py 2012-03-13 04:00:05.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/utils/strings.py 2013-05-12 15:06:13.000000000 +0000 @@ -2,7 +2,7 @@ Strings -- implementation of a few specific string subclasses. """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client diff -Nru pgxnclient-1.0.3/pgxnclient/utils/temp.py pgxnclient-1.2.1/pgxnclient/utils/temp.py --- pgxnclient-1.0.3/pgxnclient/utils/temp.py 1970-01-01 00:00:00.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/utils/temp.py 2013-05-12 15:06:13.000000000 +0000 @@ -0,0 +1,19 @@ +""" +pgxnclient -- temp files utilities +""" + +# Copyright (C) 2011-2012 Daniele Varrazzo + +# This file is part of the PGXN client + +import shutil +import tempfile +import contextlib + +@contextlib.contextmanager +def temp_dir(): + """Context manager to create a temp dir and delete after usage.""" + dir = tempfile.mkdtemp() + yield dir + shutil.rmtree(dir) + diff -Nru pgxnclient-1.0.3/pgxnclient/utils/zip.py pgxnclient-1.2.1/pgxnclient/utils/zip.py --- pgxnclient-1.0.3/pgxnclient/utils/zip.py 2012-05-19 20:33:27.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/utils/zip.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -""" -pgxnclient -- zip file utilities -""" - -# Copyright (C) 2011 Daniele Varrazzo - -# This file is part of the PGXN client - -import os -import stat -from zipfile import ZipFile - -from pgxnclient.utils import b, load_jsons -from pgxnclient.i18n import _ -from pgxnclient.errors import PgxnClientException - -import logging -logger = logging.getLogger('pgxnclient.utils.zip') - -def unpack(zipname, destdir): - logger.info(_("unpacking: %s"), zipname) - destdir = os.path.abspath(destdir) - zf = ZipFile(zipname, 'r') - try: - for fn in zf.namelist(): - fname = os.path.abspath(os.path.join(destdir, fn)) - if not fname.startswith(destdir): - raise PgxnClientException( - _("archive file '%s' trying to escape!") % fname) - - # Looks like checking for a trailing / is the only way to - # tell if the file is a directory. - if fn.endswith('/'): - os.makedirs(fname) - continue - - # The directory is not always explicitly present in the archive - if not os.path.exists(os.path.dirname(fname)): - os.makedirs(os.path.dirname(fname)) - - # Copy the file content - logger.debug(_("saving: %s"), fname) - fout = open(fname, "wb") - try: - data = zf.read(fn) - # In order to restore the executable bit, I haven't find - # anything that looks like an executable flag in the zipinfo, - # so look at the hashbangs... - isexec = data[:2] == b('#!') - fout.write(data) - finally: - fout.close() - - if isexec: - os.chmod(fname, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC) - - finally: - zf.close() - - # Choose the directory where to work. Because we are mostly a wrapper for - # pgxs, let's look for a makefile. The zip should contain a single base - # directory, so return the first dir we found containing a Makefile, - # alternatively just return the unpacked dir - for dir in os.listdir(destdir): - for fn in ('Makefile', 'makefile', 'GNUmakefile', 'configure'): - if os.path.exists(os.path.join(destdir, dir, fn)): - return os.path.join(destdir, dir) - - return destdir - -def get_meta_from_zip(filename): - try: - zf = ZipFile(filename, 'r') - except Exception, e: - raise PgxnClientException( - _("cannot open archive '%s': %s") % (filename, e)) - - try: - # Return the first file with the expected name - for fn in zf.namelist(): - if fn.endswith('META.json'): - return load_jsons(zf.read(fn).decode('utf8')) - else: - raise PgxnClientException( - _("file 'META.json' not found in archive '%s'") % filename) - finally: - zf.close() - diff -Nru pgxnclient-1.0.3/pgxnclient/zip.py pgxnclient-1.2.1/pgxnclient/zip.py --- pgxnclient-1.0.3/pgxnclient/zip.py 1970-01-01 00:00:00.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient/zip.py 2013-05-12 15:06:13.000000000 +0000 @@ -0,0 +1,97 @@ +""" +pgxnclient -- zip file utilities +""" + +# Copyright (C) 2011-2012 Daniele Varrazzo + +# This file is part of the PGXN client + +import os +import stat +import zipfile + +from pgxnclient.utils import b +from pgxnclient.i18n import _ +from pgxnclient.errors import PgxnClientException +from pgxnclient.archive import Archive + +import logging +logger = logging.getLogger('pgxnclient.zip') + + +class ZipArchive(Archive): + """Handle .zip archives""" + + _file = None + + def can_open(self): + return zipfile.is_zipfile(self.filename) + + def open(self): + assert not self._file, "archive already open" + try: + self._file = zipfile.ZipFile(self.filename, 'r') + except Exception, e: + raise PgxnClientException( + _("cannot open archive '%s': %s") % (self.filename, e)) + + def close(self): + if self._file is not None: + self._file.close() + self._file = None + + def list_files(self): + assert self._file, "archive not open" + return self._file.namelist() + + def read(self, fn): + assert self._file, "archive not open" + return self._file.read(fn) + + def unpack(self, destdir): + zipname = self.filename + logger.info(_("unpacking: %s"), zipname) + destdir = os.path.abspath(destdir) + self.open() + try: + for fn in self.list_files(): + fname = os.path.abspath(os.path.join(destdir, fn)) + if not fname.startswith(destdir): + raise PgxnClientException( + _("archive file '%s' trying to escape!") % fname) + + # Looks like checking for a trailing / is the only way to + # tell if the file is a directory. + if fn.endswith('/'): + os.makedirs(fname) + continue + + # The directory is not always explicitly present in the archive + if not os.path.exists(os.path.dirname(fname)): + os.makedirs(os.path.dirname(fname)) + + # Copy the file content + logger.debug(_("saving: %s"), fname) + fout = open(fname, "wb") + try: + data = self.read(fn) + # In order to restore the executable bit, I haven't find + # anything that looks like an executable flag in the zipinfo, + # so look at the hashbangs... + isexec = data[:2] == b('#!') + fout.write(data) + finally: + fout.close() + + if isexec: + os.chmod(fname, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC) + + finally: + self.close() + + return self._find_work_directory(destdir) + + +def unpack(filename, destdir): + return ZipArchive(filename).unpack(destdir) + diff -Nru pgxnclient-1.0.3/pgxnclient.egg-info/PKG-INFO pgxnclient-1.2.1/pgxnclient.egg-info/PKG-INFO --- pgxnclient-1.0.3/pgxnclient.egg-info/PKG-INFO 2012-05-19 20:37:51.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient.egg-info/PKG-INFO 2013-05-12 15:08:59.000000000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: pgxnclient -Version: 1.0.3 +Version: 1.2.1 Summary: A command line tool to interact with the PostgreSQL Extension Network. Home-page: http://pgxnclient.projects.postgresql.org/ Author: Daniele Varrazzo @@ -15,5 +15,10 @@ Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 Classifier: Topic :: Database diff -Nru pgxnclient-1.0.3/pgxnclient.egg-info/SOURCES.txt pgxnclient-1.2.1/pgxnclient.egg-info/SOURCES.txt --- pgxnclient-1.0.3/pgxnclient.egg-info/SOURCES.txt 2012-05-19 20:37:51.000000000 +0000 +++ pgxnclient-1.2.1/pgxnclient.egg-info/SOURCES.txt 2013-05-12 15:08:59.000000000 +0000 @@ -1,3 +1,4 @@ +AUTHORS CHANGES COPYING MANIFEST.in @@ -15,11 +16,14 @@ docs/usage.rst pgxnclient/__init__.py pgxnclient/api.py +pgxnclient/archive.py pgxnclient/cli.py pgxnclient/errors.py pgxnclient/i18n.py pgxnclient/network.py pgxnclient/spec.py +pgxnclient/tar.py +pgxnclient/zip.py pgxnclient.egg-info/PKG-INFO pgxnclient.egg-info/SOURCES.txt pgxnclient.egg-info/dependency_links.txt @@ -42,6 +46,7 @@ pgxnclient/libexec/pgxn-uninstall pgxnclient/libexec/pgxn-unload pgxnclient/tests/__init__.py +pgxnclient/tests/test_archives.py pgxnclient/tests/test_commands.py pgxnclient/tests/test_label.py pgxnclient/tests/test_semver.py @@ -52,10 +57,11 @@ pgxnclient/utils/ordereddict.py pgxnclient/utils/semver.py pgxnclient/utils/strings.py +pgxnclient/utils/temp.py pgxnclient/utils/uri.py -pgxnclient/utils/zip.py testdata/META-manyext.json testdata/download.py +testdata/foobar-0.42.1.tar.gz testdata/foobar-0.42.1.zip testdata/http%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffoobar%2F0.42.1%2FMETA-badsha1.json testdata/http%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffoobar%2F0.42.1%2FMETA.json @@ -70,4 +76,8 @@ testdata/http%3A%2F%2Fapi.pgxn.org%2Fdist%2Fpyrseas.json testdata/http%3A%2F%2Fapi.pgxn.org%2Fextension%2Famqp.json testdata/http%3A%2F%2Fapi.pgxn.org%2Findex.json -testdata/http%3A%2F%2Fapi.pgxn.org%2Fsearch%2Fdocs%2F%3Fq%3D%2522foo%2Bbar%2522%2Bbaz \ No newline at end of file +testdata/http%3A%2F%2Fapi.pgxn.org%2Fmeta%2Fmirrors.json +testdata/http%3A%2F%2Fapi.pgxn.org%2Fsearch%2Fdocs%2F%3Fq%3D%2522foo%2Bbar%2522%2Bbaz +testdata/http%3A%2F%2Fexample.org%2Ffoobar-0.42.1.tar.gz +testdata/tar.ext +testdata/zip.ext \ No newline at end of file diff -Nru pgxnclient-1.0.3/setup.py pgxnclient-1.2.1/setup.py --- pgxnclient-1.0.3/setup.py 2011-11-29 22:51:08.000000000 +0000 +++ pgxnclient-1.2.1/setup.py 2013-05-12 15:06:13.000000000 +0000 @@ -3,7 +3,7 @@ pgxnclient -- setup script """ -# Copyright (C) 2011 Daniele Varrazzo +# Copyright (C) 2011-2012 Daniele Varrazzo # This file is part of the PGXN client @@ -31,7 +31,7 @@ tests_require = [] if sys.version_info < (2, 5): - requires.append('simplejson<=2.0.9') + raise ValueError("PGXN client requires at least Python 2.5") elif sys.version_info < (2, 7): requires.append('simplejson>=2.1') @@ -48,7 +48,12 @@ License :: OSI Approved :: BSD License Operating System :: POSIX Programming Language :: Python :: 2 +Programming Language :: Python :: 2.5 +Programming Language :: Python :: 2.6 +Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 +Programming Language :: Python :: 3.1 +Programming Language :: Python :: 3.2 Topic :: Database """ Binary files /tmp/c3PNxX3OxH/pgxnclient-1.0.3/testdata/foobar-0.42.1.tar.gz and /tmp/nsFYFgv44w/pgxnclient-1.2.1/testdata/foobar-0.42.1.tar.gz differ diff -Nru pgxnclient-1.0.3/testdata/http%3A%2F%2Fapi.pgxn.org%2Fmeta%2Fmirrors.json pgxnclient-1.2.1/testdata/http%3A%2F%2Fapi.pgxn.org%2Fmeta%2Fmirrors.json --- pgxnclient-1.0.3/testdata/http%3A%2F%2Fapi.pgxn.org%2Fmeta%2Fmirrors.json 1970-01-01 00:00:00.000000000 +0000 +++ pgxnclient-1.2.1/testdata/http%3A%2F%2Fapi.pgxn.org%2Fmeta%2Fmirrors.json 2013-05-12 15:06:13.000000000 +0000 @@ -0,0 +1,98 @@ +[ + { + "uri": "http://pgxn.depesz.com/", + "frequency": "every 6 hours", + "location": "Nürnberg, Germany", + "organization": "depesz Software Hubert Lubaczewski", + "timezone": "CEST", + "email": "depesz.com|web_pgxn", + "bandwidth": "100Mbps", + "src": "rsync://master.pgxn.org/pgxn/", + "rsync": "", + "notes": "access via http only" + }, + { + "uri": "http://www.postgres-support.ch/pgxn/", + "frequency": "hourly", + "location": "Basel, Switzerland, Europe", + "organization": "micro systems", + "timezone": "CEST", + "email": "msys.ch|marc", + "bandwidth": "10Mbps", + "src": "rsync://master.pgxn.org/pgxn", + "rsync": "", + "notes": "" + }, + { + "uri": "http://pgxn.justatheory.com/", + "frequency": "daily", + "location": "Portland, OR, USA", + "organization": "David E. Wheeler", + "timezone": "America/Los_Angeles", + "email": "justatheory.com|pgxn", + "bandwidth": "Cable", + "src": "rsync://master.pgxn.org/pgxn/", + "rsync": "", + "notes": "" + }, + { + "uri": "http://pgxn.darkixion.com/", + "frequency": "hourly", + "location": "London, UK", + "organization": "Thom Brown", + "timezone": "Europe/London", + "email": "darkixion.com|pgxn", + "bandwidth": "1Gbps", + "src": "rsync://master.pgxn.org/pgxn", + "rsync": "rsync://pgxn.darkixion.com/pgxn", + "notes": "" + }, + { + "uri": "http://mirrors.cat.pdx.edu/pgxn/", + "frequency": "hourly", + "location": "Portland, OR, USA", + "organization": "PSU Computer Action Team", + "timezone": "America/Los_Angeles", + "email": "cat.pdx.edu|support", + "bandwidth": "60Mbsec", + "src": "rsync://master.pgxn.org/pgxn", + "rsync": "rsync://mirrors.cat.pdx.edu/pgxn", + "notes": "I2 and IPv6" + }, + { + "uri": "http://pgxn.dalibo.org/", + "frequency": "hourly", + "location": "Marseille, France", + "organization": "DALIBO SARL", + "timezone": "CEST", + "email": "dalibo.com|contact", + "bandwidth": "100Mbps", + "src": "rsync://master.pgxn.org/pgxn/", + "rsync": "", + "notes": "" + }, + { + "uri": "http://pgxn.cxsoftware.org/", + "frequency": "hourly", + "location": "Seattle, WA, USA", + "organization": "CxNet", + "timezone": "America/Los_Angeles", + "email": "cxnet.cl|cristobal", + "bandwidth": "100Mbps", + "src": "rsync://master.pgxn.org/pgxn/", + "rsync": "", + "notes": "" + }, + { + "uri": "http://api.pgxn.org/", + "frequency": "hourly", + "location": "Portland, OR, USA", + "organization": "PGXN", + "timezone": "America/Los_Angeles", + "email": "pgexperts.com|pgxn", + "bandwidth": "10MBps", + "src": "rsync://master.pgxn.org/pgxn", + "rsync": "", + "notes": "API server." + } +] Binary files /tmp/c3PNxX3OxH/pgxnclient-1.0.3/testdata/http%3A%2F%2Fexample.org%2Ffoobar-0.42.1.tar.gz and /tmp/nsFYFgv44w/pgxnclient-1.2.1/testdata/http%3A%2F%2Fexample.org%2Ffoobar-0.42.1.tar.gz differ Binary files /tmp/c3PNxX3OxH/pgxnclient-1.0.3/testdata/tar.ext and /tmp/nsFYFgv44w/pgxnclient-1.2.1/testdata/tar.ext differ Binary files /tmp/c3PNxX3OxH/pgxnclient-1.0.3/testdata/zip.ext and /tmp/nsFYFgv44w/pgxnclient-1.2.1/testdata/zip.ext differ