Skip to content
This repository
Browse code

Merge branch 'url-spec' into devel

  • Loading branch information...
commit 5635ba96ed9d993094789c3a3dc22c1f5fd7d520 2 parents e2297e2 + 34e5c9e
Daniele Varrazzo authored August 30, 2012
4  CHANGES
@@ -6,7 +6,9 @@ PGXN Client changes log
6 6
 pgxnclient 1.2
7 7
 ==============
8 8
 
9  
-- ``gmake`` if used in favour of ``make`` for platforms where the two are
  9
+- Packages can be downloaded, installed, loaded specifying an URL
  10
+  (ticket #15).
  11
+- Use ``gmake`` in favour of ``make`` for platforms where the two are
10 12
   distinct, such as BSD (ticket #14).
11 13
 - Added ``--make`` option to select the make executable (ticket #16).
12 14
 
38  docs/usage.rst
Source Rendered
@@ -65,6 +65,9 @@ directory containing a distribution: in this case the specification should
65 65
 contain at least a path separator to disambiguate it from a distribution name,
66 66
 for instance ``pgxn install ./foo.zip``.
67 67
 
  68
+A few commands also allow specifying a package with an URL. Currently the
  69
+schemas ``http://`` and ``https://`` are supported.
  70
+
68 71
 
69 72
 .. _install:
70 73
 
@@ -85,7 +88,8 @@ Usage:
85 88
 
86 89
 The program takes a `package specification`_ identifying the distribution to
87 90
 work with.  The download phase is skipped if the distribution specification
88  
-refers to a local directory or package.
  91
+refers to a local directory or package.  The package may be specified with an
  92
+URL.
89 93
 
90 94
 Note that the built extension is not loaded in any database: use the command
91 95
 `load`_ for this purpose.
@@ -144,8 +148,9 @@ Usage:
144 148
                *SPEC*
145 149
 
146 150
 The command takes a `package specification`_ identifying the distribution to
147  
-work with, which can also be a local file or directory. The distribution is
148  
-unpacked if required and the ``installcheck`` make target is run.
  151
+work with, which can also be a local file or directory or an URL. The
  152
+distribution is unpacked if required and the ``installcheck`` make target is
  153
+run.
149 154
 
150 155
 .. note::
151 156
     The command doesn't run ``make all`` before ``installcheck``: if any file
@@ -224,11 +229,11 @@ Usage:
224 229
               *SPEC* [*EXT* [*EXT* ...]]
225 230
 
226 231
 The distribution is specified according to the `package specification`_ and
227  
-can refer to a local directory or file. No consistency check is performed
228  
-between the packages specified in the ``install`` and ``load`` command: the
229  
-specifications should refer to compatible packages. The specified distribution
230  
-is only used to read the metadata: only installed files are actually used to
231  
-issue database commands.
  232
+can refer to a local directory or file or to an URL.  No consistency check is
  233
+performed between the packages specified in the ``install`` and ``load``
  234
+command: the specifications should refer to compatible packages. The specified
  235
+distribution is only used to read the metadata: only installed files are
  236
+actually used to issue database commands.
232 237
 
233 238
 The database to install into can be specified using options
234 239
 ``-d``/``--dbname``, ``-h``/``--host``, ``-p``/``--port``,
@@ -334,11 +339,12 @@ Usage:
334 339
                   [--target *PATH*]
335 340
                   *SPEC*
336 341
 
337  
-The distribution is specified according to the `package specification`_.  The
338  
-file is saved in the current directory with name usually
339  
-:samp:`{distribution}-{version}.zip`. If a file with the same name exists, a
340  
-suffix ``-1``, ``-2`` etc. is added to the name, before the extension.  A
341  
-different directory or name can be specified using the ``--target`` option.
  342
+The distribution is specified according to the `package specification`_ and
  343
+can be represented by an URL.  The file is saved in the current directory with
  344
+name usually :samp:`{distribution}-{version}.zip`. If a file with the same
  345
+name exists, a suffix ``-1``, ``-2`` etc. is added to the name, before the
  346
+extension.  A different directory or name can be specified using the
  347
+``--target`` option.
342 348
 
343 349
 
344 350
 .. _pgxn-search:
@@ -419,9 +425,9 @@ Usage:
419 425
               [--details | --meta | --readme | --versions]
420 426
               *SPEC*
421 427
 
422  
-The distribution is specified according to the `package specification`_.
423  
-The command output is a list of values obtained by the distribution's
424  
-``META.json`` file, for example:
  428
+The distribution is specified according to the `package specification`_.  It
  429
+cannot be a local dir or file nor an URL.  The command output is a list of
  430
+values obtained by the distribution's ``META.json`` file, for example:
425 431
 
426 432
 .. code-block:: console
427 433
 
6  pgxnclient/api.py
@@ -10,9 +10,9 @@
10 10
 
11 11
 from urllib import urlencode
12 12
 
  13
+from pgxnclient import network
13 14
 from pgxnclient.utils import load_json
14 15
 from pgxnclient.errors import NetworkError, NotFound, ResourceNotFound
15  
-from pgxnclient.network import get_file
16 16
 from pgxnclient.utils.uri import expand_template
17 17
 
18 18
 
@@ -78,7 +78,7 @@ def user(self, username):
78 78
 
79 79
     def call(self, meth, args=None, query=None):
80 80
         url = self.get_url(meth, args, query)
81  
-        return get_file(url)
  81
+        return network.get_file(url)
82 82
 
83 83
     def get_url(self, meth, args=None, query=None):
84 84
         tmpl = self.get_template(meth)
@@ -98,7 +98,7 @@ def get_index(self):
98 98
         if self._api_index is None:
99 99
             url = self.mirror.rstrip('/') + '/index.json'
100 100
             try:
101  
-                with get_file(url) as f:
  101
+                with network.get_file(url) as f:
102 102
                     self._api_index = load_json(f)
103 103
             except ResourceNotFound:
104 104
                 raise NetworkError("API index not found at '%s'" % url)
62  pgxnclient/commands/__init__.py
@@ -20,10 +20,12 @@
20 20
 from pgxnclient.utils import load_json, argparse, find_executable
21 21
 
22 22
 from pgxnclient import __version__
  23
+from pgxnclient import network
23 24
 from pgxnclient import Spec, SemVer
24 25
 from pgxnclient.api import Api
25 26
 from pgxnclient.i18n import _, gettext
26 27
 from pgxnclient.errors import NotFound, PgxnClientException, ProcessError, ResourceNotFound, UserAbort
  28
+from pgxnclient.utils.temp import temp_dir
27 29
 
28 30
 logger = logging.getLogger('pgxnclient.commands')
29 31
 
@@ -265,7 +267,7 @@ def customize_parser(self, parser, subparsers,
265 267
 
266 268
         return subp
267 269
 
268  
-    def get_spec(self, _can_be_local=False):
  270
+    def get_spec(self, _can_be_local=False, _can_be_url=False):
269 271
         """
270 272
         Return the package specification requested.
271 273
 
@@ -283,6 +285,10 @@ def get_spec(self, _can_be_local=False):
283 285
             raise PgxnClientException(
284 286
                 _("you cannot use a local resource with this command"))
285 287
 
  288
+        if not _can_be_url and spec.is_url():
  289
+            raise PgxnClientException(
  290
+                _("you cannot use an url with this command"))
  291
+
286 292
         return spec
287 293
 
288 294
     def get_best_version(self, data, spec, quiet=False):
@@ -359,7 +365,7 @@ def get_meta(self, spec):
359 365
 
360 366
         Return the object obtained parsing the JSON.
361 367
         """
362  
-        if not spec.is_local():
  368
+        if spec.is_name():
363 369
             # Get the metadata from the API
364 370
             try:
365 371
                 data = self.api.dist(spec.name)
@@ -387,6 +393,14 @@ def get_meta(self, spec):
387 393
             # Get the metadata from a zip file
388 394
             return get_meta_from_zip(spec.filename)
389 395
 
  396
+        elif spec.is_url():
  397
+            with network.get_file(spec.url) as fin:
  398
+                with temp_dir() as dir:
  399
+                    fn = network.download(fin, dir)
  400
+                    return get_meta_from_zip(fn)
  401
+
  402
+        else:
  403
+            assert False
390 404
 
391 405
 class WithSpecLocal(WithSpec):
392 406
     """
@@ -405,35 +419,29 @@ def customize_parser(self, parser, subparsers, epilog=None, **kwargs):
405 419
 
406 420
         return subp
407 421
 
408  
-    def get_spec(self):
409  
-        return super(WithSpecLocal, self).get_spec(_can_be_local=True)
410  
-
  422
+    def get_spec(self, **kwargs):
  423
+        kwargs['_can_be_local'] = True
  424
+        return super(WithSpecLocal, self).get_spec(**kwargs)
411 425
 
412  
-import shutil
413  
-import tempfile
414  
-from pgxnclient.utils.zip import unpack
415  
-
416  
-class WithUnpacking(object):
  426
+class WithSpecUrl(WithSpec):
417 427
     """
418  
-    Mixin to implement commands that may deal with zip files.
  428
+    Mixin to implement commands that can also refer to a URL.
419 429
     """
420  
-    def call_with_temp_dir(self, f, *args, **kwargs):
421  
-        """
422  
-        Call a function in the context of a temporary directory.
423 430
 
424  
-        Create the temp directory and pass its name as first argument to *f*.
425  
-        Other arguments and keywords are passed to *f* too. Upon exit delete
426  
-        the directory.
427  
-        """
428  
-        dir = tempfile.mkdtemp()
429  
-        try:
430  
-            return f(dir, *args, **kwargs)
431  
-        finally:
432  
-            shutil.rmtree(dir)
  431
+    @classmethod
  432
+    def customize_parser(self, parser, subparsers, epilog=None, **kwargs):
  433
+        epilog = _("""
  434
+SPEC may also be an url specifying a protocol such as 'http://' or 'https://'.
  435
+""") + (epilog or "")
  436
+
  437
+        subp = super(WithSpecUrl, self).customize_parser(
  438
+            parser, subparsers, epilog=epilog, **kwargs)
  439
+
  440
+        return subp
433 441
 
434  
-    def unpack(self, zipname, destdir):
435  
-        """Unpack the zip file *zipname* into *destdir*."""
436  
-        return unpack(zipname, destdir)
  442
+    def get_spec(self, **kwargs):
  443
+        kwargs['_can_be_url'] = True
  444
+        return super(WithSpecUrl, self).get_spec(**kwargs)
437 445
 
438 446
 
439 447
 class WithPgConfig(object):
@@ -489,7 +497,7 @@ def get_pg_config(self):
489 497
 
490 498
 import shlex
491 499
 
492  
-class WithMake(WithPgConfig, WithUnpacking):
  500
+class WithMake(WithPgConfig):
493 501
     """
494 502
     Mixin to implement commands that should invoke :program:`make`.
495 503
     """
48  pgxnclient/commands/install.py
@@ -17,18 +17,20 @@
17 17
 from subprocess import PIPE
18 18
 
19 19
 from pgxnclient import SemVer
  20
+from pgxnclient import network
20 21
 from pgxnclient.i18n import _, N_
21 22
 from pgxnclient.utils import sha1, b
22 23
 from pgxnclient.errors import BadChecksum, PgxnClientException, InsufficientPrivileges
23  
-from pgxnclient.network import download
24 24
 from pgxnclient.commands import Command, WithDatabase, WithMake, WithPgConfig
25  
-from pgxnclient.commands import WithSpec, WithSpecLocal, WithSudo
  25
+from pgxnclient.commands import WithSpecUrl, WithSpecLocal, WithSudo
  26
+from pgxnclient.utils.zip import unpack
  27
+from pgxnclient.utils.temp import temp_dir
26 28
 from pgxnclient.utils.strings import Identifier
27 29
 
28 30
 logger = logging.getLogger('pgxnclient.commands')
29 31
 
30 32
 
31  
-class Download(WithSpec, Command):
  33
+class Download(WithSpecUrl, Command):
32 34
     name = 'download'
33 35
     description = N_("download a distribution from the network")
34 36
 
@@ -43,6 +45,11 @@ def customize_parser(self, parser, subparsers, **kwargs):
43 45
 
44 46
     def run(self):
45 47
         spec = self.get_spec()
  48
+        assert not spec.is_local()
  49
+
  50
+        if spec.is_url():
  51
+            return self._run_url(spec)
  52
+
46 53
         data = self.get_meta(spec)
47 54
 
48 55
         try:
@@ -52,11 +59,17 @@ def run(self):
52 59
                 "sha1 missing from the distribution meta")
53 60
 
54 61
         with self.api.download(data['name'], SemVer(data['version'])) as fin:
55  
-            fn = self._get_local_file_name(fin.url)
56  
-            fn = download(fin, fn, rename=True)
  62
+            fn = network.download(fin, self.opts.target)
  63
+
57 64
         self.verify_checksum(fn, chk)
58 65
         return fn
59 66
 
  67
+    def _run_url(self, spec):
  68
+        with network.get_file(spec.url) as fin:
  69
+            fn = network.download(fin, self.opts.target)
  70
+
  71
+        return fn
  72
+
60 73
     def verify_checksum(self, fn, chk):
61 74
         """Verify that a downloaded file has the expected sha1."""
62 75
         sha = sha1()
@@ -77,34 +90,27 @@ def verify_checksum(self, fn, chk):
77 90
                 fn, sha, chk)
78 91
             raise BadChecksum(_("bad sha1 in downloaded file"))
79 92
 
80  
-    def _get_local_file_name(self, url):
81  
-        from urlparse import urlsplit
82  
-        if os.path.isdir(self.opts.target):
83  
-            basename = urlsplit(url)[2].rsplit('/', 1)[-1]
84  
-            fn = os.path.join(self.opts.target, basename)
85  
-        else:
86  
-            fn = self.opts.target
87  
-
88  
-        return os.path.abspath(fn)
89 93
 
90  
-
91  
-class InstallUninstall(WithMake, WithSpecLocal, Command):
  94
+class InstallUninstall(WithMake, WithSpecUrl, WithSpecLocal, Command):
92 95
     """
93 96
     Base class to implement the ``install`` and ``uninstall`` commands.
94 97
     """
95 98
     def run(self):
96  
-        return self.call_with_temp_dir(self._run)
  99
+        with temp_dir() as dir:
  100
+            return self._run(dir)
97 101
 
98 102
     def _run(self, dir):
99 103
         spec = self.get_spec()
100 104
         if spec.is_dir():
101 105
             pdir = os.path.abspath(spec.dirname)
102 106
         elif spec.is_file():
103  
-            pdir = self.unpack(spec.filename, dir)
104  
-        else:   # download
  107
+            pdir = unpack(spec.filename, dir)
  108
+        elif not spec.is_local():
105 109
             self.opts.target = dir
106 110
             fn = Download(self.opts).run()
107  
-            pdir = self.unpack(fn, dir)
  111
+            pdir = unpack(fn, dir)
  112
+        else:
  113
+            assert False
108 114
 
109 115
         self.maybe_run_configure(pdir)
110 116
 
@@ -220,7 +226,7 @@ def _inun(self, pdir):
220 226
             raise
221 227
 
222 228
 
223  
-class LoadUnload(WithPgConfig, WithDatabase, WithSpecLocal, Command):
  229
+class LoadUnload(WithPgConfig, WithDatabase, WithSpecUrl, WithSpecLocal, Command):
224 230
     """
225 231
     Base class to implement the ``load`` and ``unload`` commands.
226 232
     """
22  pgxnclient/network.py
@@ -8,6 +8,7 @@
8 8
 
9 9
 import os
10 10
 import urllib2
  11
+from urlparse import urlsplit
11 12
 from itertools import count
12 13
 from contextlib import closing
13 14
 
@@ -39,16 +40,33 @@ def get_file(url):
39 40
     except urllib2.URLError, e:
40 41
         raise NetworkError(_("network error: %s") % e.reason)
41 42
 
  43
+def get_local_file_name(target, url):
  44
+    """Return a good name for a local file.
  45
+
  46
+    If *target* is a dir, make a name out of the url. Otherwise return target
  47
+    itself. Always return an absolute path.
  48
+    """
  49
+    if os.path.isdir(target):
  50
+        basename = urlsplit(url)[2].rsplit('/', 1)[-1]
  51
+        fn = os.path.join(target, basename)
  52
+    else:
  53
+        fn = target
  54
+
  55
+    return os.path.abspath(fn)
  56
+
42 57
 def download(f, fn, rename=True):
43 58
     """Download a file locally.
44 59
 
45  
-    :param f: open file read
46  
-    :param fn: name of the file to write
  60
+    :param f: open file to read
  61
+    :param fn: name of the file to write. If a dir, save into it.
47 62
     :param rename: if true and a file *fn* exist, rename the downloaded file
48 63
         adding a prefix ``-1``, ``-2``... before the extension.
49 64
     
50 65
     Return the name of the file saved.
51 66
     """
  67
+    if os.path.isdir(fn):
  68
+        fn = get_local_file_name(fn, f.url)
  69
+
52 70
     if rename:
53 71
         if os.path.exists(fn):
54 72
             base, ext = os.path.splitext(fn)
17  pgxnclient/spec.py
@@ -33,14 +33,18 @@ class Spec(object):
33 33
         'stable': STABLE, }
34 34
 
35 35
     def __init__(self, name=None, op=None, ver=None,
36  
-            dirname=None, filename=None):
  36
+            dirname=None, filename=None, url=None):
37 37
         self.name = name and name.lower()
38 38
         self.op = op
39 39
         self.ver = ver
40 40
 
41  
-        # point to local files
  41
+        # point to local files or specific resources
42 42
         self.dirname = dirname
43 43
         self.filename = filename
  44
+        self.url = url
  45
+
  46
+    def is_name(self):
  47
+        return self.name is not None
44 48
 
45 49
     def is_dir(self):
46 50
         return self.dirname is not None
@@ -48,11 +52,14 @@ def is_dir(self):
48 52
     def is_file(self):
49 53
         return self.filename is not None
50 54
 
  55
+    def is_url(self):
  56
+        return self.url is not None
  57
+
51 58
     def is_local(self):
52 59
         return self.is_dir() or self.is_file()
53 60
 
54 61
     def __str__(self):
55  
-        name = self.name or self.filename or self.dirname or "???"
  62
+        name = self.name or self.filename or self.dirname or self.url or "???"
56 63
         if self.op is None:
57 64
             return name
58 65
         else:
@@ -64,6 +71,10 @@ def parse(self, spec):
64 71
 
65 72
         Raise BadSpecError if couldn't parse.
66 73
         """
  74
+        # TODO: handle file:// too
  75
+        if spec.startswith('http://') or spec.startswith('https://'):
  76
+            return Spec(url=spec)
  77
+
67 78
         if os.sep in spec:
68 79
             # This is a local thing, let's see what
69 80
             if os.path.isdir(spec):
80  pgxnclient/tests/test_commands.py
@@ -44,7 +44,7 @@ def f(what):
44 44
 class InfoTestCase(unittest.TestCase):
45 45
     def _get_output(self, cmdline):
46 46
         @patch('sys.stdout')
47  
-        @patch('pgxnclient.api.get_file')
  47
+        @patch('pgxnclient.network.get_file')
48 48
         def do(mock, stdout):
49 49
             mock.side_effect = fake_get_file
50 50
             from pgxnclient.cli import main
@@ -120,7 +120,7 @@ def test_popen_raises(self):
120 120
 
121 121
 
122 122
 class DownloadTestCase(unittest.TestCase):
123  
-    @patch('pgxnclient.api.get_file')
  123
+    @patch('pgxnclient.network.get_file')
124 124
     def test_download_latest(self, mock):
125 125
         mock.side_effect = fake_get_file
126 126
 
@@ -134,7 +134,7 @@ def test_download_latest(self, mock):
134 134
         finally:
135 135
             ifunlink(fn)
136 136
 
137  
-    @patch('pgxnclient.api.get_file')
  137
+    @patch('pgxnclient.network.get_file')
138 138
     def test_download_testing(self, mock):
139 139
         mock.side_effect = fake_get_file
140 140
 
@@ -148,7 +148,21 @@ def test_download_testing(self, mock):
148 148
         finally:
149 149
             ifunlink(fn)
150 150
 
151  
-    @patch('pgxnclient.api.get_file')
  151
+    @patch('pgxnclient.network.get_file')
  152
+    def test_download_url(self, mock):
  153
+        mock.side_effect = fake_get_file
  154
+
  155
+        fn = 'foobar-0.43.2b1.zip'
  156
+        self.assert_(not os.path.exists(fn))
  157
+
  158
+        from pgxnclient.cli import main
  159
+        try:
  160
+            main(['download', 'http://api.pgxn.org/dist/foobar/0.43.2b1/foobar-0.43.2b1.zip'])
  161
+            self.assert_(os.path.exists(fn))
  162
+        finally:
  163
+            ifunlink(fn)
  164
+
  165
+    @patch('pgxnclient.network.get_file')
152 166
     def test_download_ext(self, mock):
153 167
         mock.side_effect = fake_get_file
154 168
 
@@ -162,7 +176,7 @@ def test_download_ext(self, mock):
162 176
         finally:
163 177
             ifunlink(fn)
164 178
 
165  
-    @patch('pgxnclient.api.get_file')
  179
+    @patch('pgxnclient.network.get_file')
166 180
     def test_download_rename(self, mock):
167 181
         mock.side_effect = fake_get_file
168 182
 
@@ -195,7 +209,7 @@ def test_download_rename(self, mock):
195 209
             ifunlink(fn1)
196 210
             ifunlink(fn2)
197 211
 
198  
-    @patch('pgxnclient.api.get_file')
  212
+    @patch('pgxnclient.network.get_file')
199 213
     def test_download_bad_sha1(self, mock):
200 214
         def fakefake(url):
201 215
             return fake_get_file(url, urlmap = {
@@ -218,7 +232,7 @@ def fakefake(url):
218 232
         finally:
219 233
             ifunlink(fn)
220 234
 
221  
-    @patch('pgxnclient.api.get_file')
  235
+    @patch('pgxnclient.network.get_file')
222 236
     def test_download_case_insensitive(self, mock):
223 237
         mock.side_effect = fake_get_file
224 238
 
@@ -325,7 +339,7 @@ def assertCallArgs(self, pattern, args):
325 339
 class InstallTestCase(unittest.TestCase, Assertions):
326 340
 
327 341
     def setUp(self):
328  
-        self._p1 = patch('pgxnclient.api.get_file')
  342
+        self._p1 = patch('pgxnclient.network.get_file')
329 343
         self.mock_get = self._p1.start()
330 344
         self.mock_get.side_effect = fake_get_file
331 345
 
@@ -366,6 +380,17 @@ def test_install_local(self):
366 380
         self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1])
367 381
         self.assertCallArgs([self.make], self.mock_popen.call_args_list[1][0][0][:1])
368 382
 
  383
+    def test_install_url(self):
  384
+        self.mock_pgconfig.side_effect = fake_pg_config(
  385
+            libdir=os.environ['HOME'], bindir='/')
  386
+
  387
+        from pgxnclient.cli import main
  388
+        main(['install', 'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip'])
  389
+
  390
+        self.assertEquals(self.mock_popen.call_count, 2)
  391
+        self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1])
  392
+        self.assertCallArgs([self.make], self.mock_popen.call_args_list[1][0][0][:1])
  393
+
369 394
     def test_install_fails(self):
370 395
         self.mock_popen.return_value.returncode = 1
371 396
         self.mock_pgconfig.side_effect = fake_pg_config(
@@ -409,7 +434,7 @@ def test_install_sudo(self):
409 434
         self.assertCallArgs(['gksudo', '-d', 'hello world', self.make],
410 435
             self.mock_popen.call_args_list[1][0][0][:4])
411 436
 
412  
-    @patch('pgxnclient.commands.unpack')
  437
+    @patch('pgxnclient.commands.install.unpack')
413 438
     def test_install_local_zip(self, mock_unpack):
414 439
         from pgxnclient.utils.zip import unpack
415 440
         mock_unpack.side_effect = unpack
@@ -452,7 +477,7 @@ def test_install_local_dir(self):
452 477
 
453 478
 class CheckTestCase(unittest.TestCase, Assertions):
454 479
     def setUp(self):
455  
-        self._p1 = patch('pgxnclient.api.get_file')
  480
+        self._p1 = patch('pgxnclient.network.get_file')
456 481
         self.mock_get = self._p1.start()
457 482
         self.mock_get.side_effect = fake_get_file
458 483
 
@@ -477,6 +502,13 @@ def test_check_latest(self):
477 502
         self.assertEquals(self.mock_popen.call_count, 1)
478 503
         self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1])
479 504
 
  505
+    def test_check_url(self):
  506
+        from pgxnclient.cli import main
  507
+        main(['check', 'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip'])
  508
+
  509
+        self.assertEquals(self.mock_popen.call_count, 1)
  510
+        self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1])
  511
+
480 512
     def test_check_fails(self):
481 513
         self.mock_popen.return_value.returncode = 1
482 514
 
@@ -558,7 +590,7 @@ def test_parse_version(self):
558 590
             'PostgreSQL 9.1alpha5 on i686-pc-linux-gnu, compiled by GCC gcc'
559 591
             ' (Ubuntu/Linaro 4.4.4-14ubuntu5) 4.4.5, 32-bit '))
560 592
 
561  
-    @patch('pgxnclient.api.get_file')
  593
+    @patch('pgxnclient.network.get_file')
562 594
     def test_check_psql_options(self, mock_get):
563 595
         mock_get.side_effect = fake_get_file
564 596
 
@@ -580,8 +612,8 @@ def test_check_psql_options(self, mock_get):
580 612
         args = self.mock_popen.call_args[0][0]
581 613
         self.assertEqual('somewhere', args[args.index('--host') + 1])
582 614
 
583  
-    @patch('pgxnclient.commands.unpack')
584  
-    @patch('pgxnclient.api.get_file')
  615
+    @patch('pgxnclient.commands.install.unpack')
  616
+    @patch('pgxnclient.network.get_file')
585 617
     def test_load_local_zip(self, mock_get, mock_unpack):
586 618
         mock_get.side_effect = lambda *args: self.fail('network invoked')
587 619
         from pgxnclient.utils.zip import unpack
@@ -597,7 +629,7 @@ def test_load_local_zip(self, mock_get, mock_unpack):
597 629
         self.assertEquals(communicate.call_args[0][0],
598 630
             'CREATE EXTENSION foobar;')
599 631
 
600  
-    @patch('pgxnclient.api.get_file')
  632
+    @patch('pgxnclient.network.get_file')
601 633
     def test_load_local_dir(self, mock_get):
602 634
         mock_get.side_effect = lambda *args: self.fail('network invoked')
603 635
 
@@ -618,6 +650,24 @@ def test_load_local_dir(self, mock_get):
618 650
         self.assertEquals(communicate.call_args[0][0],
619 651
             'CREATE EXTENSION foobar;')
620 652
 
  653
+    @patch('pgxnclient.commands.install.unpack')
  654
+    @patch('pgxnclient.network.get_file')
  655
+    def test_load_url(self, mock_get, mock_unpack):
  656
+        mock_get.side_effect = fake_get_file
  657
+        from pgxnclient.utils.zip import unpack
  658
+        mock_unpack.side_effect = unpack
  659
+
  660
+        from pgxnclient.cli import main
  661
+        main(['load', '--yes',
  662
+            'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip'])
  663
+
  664
+        self.assertEquals(mock_unpack.call_count, 0)
  665
+        self.assertEquals(self.mock_popen.call_count, 1)
  666
+        self.assert_('psql' in self.mock_popen.call_args[0][0][0])
  667
+        communicate = self.mock_popen.return_value.communicate
  668
+        self.assertEquals(communicate.call_args[0][0],
  669
+            'CREATE EXTENSION foobar;')
  670
+
621 671
     def test_load_extensions_order(self):
622 672
         tdir = tempfile.mkdtemp()
623 673
         try:
@@ -757,7 +807,7 @@ def test_unload_missing(self):
757 807
 
758 808
 class SearchTestCase(unittest.TestCase):
759 809
     @patch('sys.stdout')
760  
-    @patch('pgxnclient.api.get_file')
  810
+    @patch('pgxnclient.network.get_file')
761 811
     def test_search_quoting(self, mock_get, stdout):
762 812
         mock_get.side_effect = fake_get_file
763 813
         from pgxnclient.cli import main
19  pgxnclient/utils/temp.py
... ...
@@ -0,0 +1,19 @@
  1
+"""
  2
+pgxnclient -- temp files utilities
  3
+"""
  4
+
  5
+# Copyright (C) 2011-2012 Daniele Varrazzo
  6
+
  7
+# This file is part of the PGXN client
  8
+
  9
+import shutil
  10
+import tempfile
  11
+import contextlib
  12
+
  13
+@contextlib.contextmanager
  14
+def temp_dir():
  15
+    """Context manager to create a temp dir and delete after usage."""
  16
+    dir = tempfile.mkdtemp()
  17
+    yield dir
  18
+    shutil.rmtree(dir)
  19
+

0 notes on commit 5635ba9

Please sign in to comment.
Something went wrong with that request. Please try again.