Author: gsim Date: Fri Oct 10 12:54:43 2014 New Revision: 1630834 URL: http://svn.apache.org/r1630834 Log: PROTON-693: Python Url class to wrap C function pni_parse_url
Also added unit tests. Added: qpid/proton/branches/examples/tests/python/proton_tests/url.py Modified: qpid/proton/branches/examples/proton-c/bindings/python/cproton.i qpid/proton/branches/examples/proton-c/bindings/python/proton.py qpid/proton/branches/examples/tests/python/proton_tests/__init__.py Modified: qpid/proton/branches/examples/proton-c/bindings/python/cproton.i URL: http://svn.apache.org/viewvc/qpid/proton/branches/examples/proton-c/bindings/python/cproton.i?rev=1630834&r1=1630833&r2=1630834&view=diff ============================================================================== --- qpid/proton/branches/examples/proton-c/bindings/python/cproton.i (original) +++ qpid/proton/branches/examples/proton-c/bindings/python/cproton.i Fri Oct 10 12:54:43 2014 @@ -280,4 +280,41 @@ int pn_ssl_get_peer_hostname(pn_ssl_t *s } %} + +/** + pni_parse_url(char* url, char **scheme, char **user, char **pass, char **host, char **port, char **path) + The following type maps convert this into a python function that taks a URL string argument + and returns a list of strings [scheme, user, pass, host, port, path] + This probably could be done more neatly. +*/ + +// Typemap to copy the url string as it will be modified by parse_url +%typemap(in,noblock=1,fragment="SWIG_AsCharPtrAndSize") char *url (int res, char *t = 0, size_t n = 0, int alloc = 0) { + res = SWIG_AsCharPtrAndSize($input, &t, &n, &alloc); + if (!SWIG_IsOK(res)) { + %argument_fail(res, "char *url", $symname, $argnum); + } + $1 = %new_array(n, $*1_ltype); + memcpy($1,t,sizeof(char)*n); + if (alloc == SWIG_NEWOBJ) %delete_array(t); + $1[n-1] = 0; +} +%typemap(freearg,match="in") char *url "free($1);"; +%typemap(argout) char *url ""; + +// Typemap for char** return strings. Don't free them. +%typemap(in,numinputs=0) char **OUTSTR($*1_ltype temp = 0) "$1 = &temp;"; +%typemap(freearg,match="in") char **OUTSTR ""; +%typemap(argout,noblock=1,fragment="SWIG_FromCharPtr") char **OUTSTR { + %append_output(SWIG_FromCharPtr(*$1)); +} + +// Typemap to initialize result as empty list +%typemap(out) void "$result = PyList_New(0);"; + + +%apply char** OUTSTR {char **scheme, char **user, char **pass, char **host, char **port, char **path}; +void pni_parse_url(char* url, char **scheme, char **user, char **pass, char **host, char **port, char **path); +%ignore pni_parse_url; + %include "proton/cproton.i" Modified: qpid/proton/branches/examples/proton-c/bindings/python/proton.py URL: http://svn.apache.org/viewvc/qpid/proton/branches/examples/proton-c/bindings/python/proton.py?rev=1630834&r1=1630833&r2=1630834&view=diff ============================================================================== --- qpid/proton/branches/examples/proton-c/bindings/python/proton.py (original) +++ qpid/proton/branches/examples/proton-c/bindings/python/proton.py Fri Oct 10 12:54:43 2014 @@ -3654,3 +3654,117 @@ __all__ = [ "timestamp", "ulong" ] + + +class Url(object): + """ + Simple URL parser/constructor, handles URLs of the form: + + <scheme>://<user>:<password>@<host>:<port>/<path> + + All components can be None if not specifeid in the URL string. + + The port can be specified as a service name, e.g. 'amqp' in the + URL string but Url.port always gives the integer value. + + @ivar scheme: Url scheme e.g. 'amqp' or 'amqps' + @ivar user: Username + @ivar password: Password + @ivar host: Host name, ipv6 literal or ipv4 dotted quad. + @ivar port: Integer port. + @ivar host_port: Returns host:port + """ + + AMQPS = "amqps" + AMQP = "amqp" + + class Port(int): + """An integer port number that can also have an associated service name string""" + + def __new__(cls, value): + port = super(Url.Port, cls).__new__(cls, cls.port_int(value)) + setattr(port, 'name', str(value)) + return port + + def __eq__(self, x): return str(self) == x or int(self) == x + def __ne__(self, x): return not self == x + def __str__(self): return str(self.name) + + @staticmethod + def port_int(value): + """Convert service, an integer or a service name, into an integer port number.""" + try: + return int(value) + except ValueError: + try: + return socket.getservbyname(value) + except socket.error: + raise ValueError("Not a valid port number or service name: '%s'" % value) + + def __init__(self, url=None, **kwargs): + """ + @param url: String or Url instance to parse or copy. + @param kwargs: URL fields: scheme, user, password, host, port, path. + If specified, replaces corresponding component in url. + """ + + fields = ['scheme', 'user', 'password', 'host', 'port', 'path'] + + for f in fields: setattr(self, f, None) + for k in kwargs: getattr(self, k) # Check for invalid kwargs + + if isinstance(url, Url): # Copy from another Url instance. + self.__dict__.update(url.__dict__) + elif url is not None: # Parse from url + parts = pni_parse_url(str(url)) + if not filter(None, parts): raise ValueError("Invalid AMQP URL: '%s'" % url) + self.scheme, self.user, self.password, self.host, port, self.path = parts + if not self.host: self.host = None + self.port = port and self.Port(port) + + # Let kwargs override values previously set from url + for field in fields: + setattr(self, field, kwargs.get(field, getattr(self, field))) + + def __repr__(self): + return "Url(%r)" % str(self) + + def __str__(self): + s = "" + if self.scheme: + s += "%s://" % self.scheme + if self.user: + s += self.user + if self.password: + s += ":%s" % self.password + if self.user or self.password: + s += '@' + if self.host and ':' in self.host: + s += "[%s]" % self.host + elif self.host: + s += self.host + if self.port: + s += ":%s" % self.port + if self.path: + s += "/%s" % self.path + return s + + def __eq__(self, url): + return \ + self.scheme == url.scheme and \ + self.user == url.user and self.password == url.password and \ + self.host == url.host and self.port == url.port and \ + self.path == url.path + + def __ne__(self, url): + return not self.__eq__(url) + + def defaults(self): + """ + Fill in missing values with defaults + @return: self + """ + self.scheme = self.scheme or self.AMQP + self.host = self.host or '0.0.0.0' + self.port = self.port or self.Port(self.scheme) + return self Modified: qpid/proton/branches/examples/tests/python/proton_tests/__init__.py URL: http://svn.apache.org/viewvc/qpid/proton/branches/examples/tests/python/proton_tests/__init__.py?rev=1630834&r1=1630833&r2=1630834&view=diff ============================================================================== --- qpid/proton/branches/examples/tests/python/proton_tests/__init__.py (original) +++ qpid/proton/branches/examples/tests/python/proton_tests/__init__.py Fri Oct 10 12:54:43 2014 @@ -26,4 +26,4 @@ import proton_tests.transport import proton_tests.ssl import proton_tests.interop import proton_tests.soak - +import proton_tests.url Added: qpid/proton/branches/examples/tests/python/proton_tests/url.py URL: http://svn.apache.org/viewvc/qpid/proton/branches/examples/tests/python/proton_tests/url.py?rev=1630834&view=auto ============================================================================== --- qpid/proton/branches/examples/tests/python/proton_tests/url.py (added) +++ qpid/proton/branches/examples/tests/python/proton_tests/url.py Fri Oct 10 12:54:43 2014 @@ -0,0 +1,117 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + + +import common +from proton import Url + +class UrlTest(common.Test): + def assertEqual(self, a, b): + assert a == b, "%s != %s" % (a, b) + + def assertNotEqual(self, a, b): + assert a != b, "%s == %s" % (a, b) + + def assertUrl(self, u, scheme, user, password, host, port, path): + self.assertEqual((u.scheme, u.user, u.password, u.host, u.port, u.path), + (scheme, user, password, host, port, path)) + + def testUrl(self): + url = Url('amqp://me:secret@myhost:1234/foobar') + self.assertEqual(str(url), "amqp://me:secret@myhost:1234/foobar") + self.assertUrl(url, 'amqp', 'me', 'secret', 'myhost', 1234, 'foobar') + self.assertEqual(str(url), "amqp://me:secret@myhost:1234/foobar") + + def testDefaults(self): + # Check that we allow None for scheme, port + url = Url(user='me', password='secret', host='myhost', path='foobar') + self.assertEqual(str(url), "me:secret@myhost/foobar") + self.assertUrl(url, None, 'me', 'secret', 'myhost', None, 'foobar') + + # Scheme defaults + self.assertEqual(str(Url("me:secret@myhost/foobar").defaults()), + "amqp://me:secret@myhost:amqp/foobar") + # Correct port for amqps vs. amqps + self.assertEqual(str(Url("amqps://me:secret@myhost/foobar").defaults()), + "amqps://me:secret@myhost:amqps/foobar") + self.assertEqual(str(Url("amqp://me:secret@myhost/foobar").defaults()), + "amqp://me:secret@myhost:amqp/foobar") + + # Empty string vs. None for path + self.assertEqual(Url("myhost/").path, "") + assert Url("myhost").path is None + + def assertPort(self, port, portint, portstr): + self.assertEqual((port, port), (portint, portstr)) + self.assertEqual((int(port), str(port)), (portint, portstr)) + + def testPort(self): + self.assertPort(Url.Port('amqp'), 5672, 'amqp') + self.assertPort(Url.Port(5672), 5672, '5672') + self.assertPort(Url.Port('amqps'), 5671, 'amqps') + self.assertPort(Url.Port(5671), 5671, '5671') + self.assertEqual(Url.Port(5671)+1, 5672) # Treat as int + self.assertEqual(str(Url.Port(5672)), '5672') + + self.assertPort(Url.Port(Url.Port('amqp')), 5672, 'amqp') + self.assertPort(Url.Port(Url.Port(5672)), 5672, '5672') + + try: + Url.Port('xxx') + assert False, "Expected ValueError" + except ValueError: pass + + self.assertEqual(str(Url("host:amqp")), "host:amqp") + self.assertEqual(Url("host:amqp").port, 5672) + self.assertEqual(str(Url("host:amqps")), "host:amqps") + self.assertEqual(Url("host:amqps").port, 5671) + + def testArgs(self): + u = Url("amqp://u:p@host:amqp/path", scheme='foo', host='bar', port=1234, path='garden') + self.assertUrl(u, 'foo', 'u', 'p', 'bar', 1234, 'garden') + u = Url() + self.assertUrl(u, None, None, None, None, None, None) + + def assertRaises(self, exception, function, *args, **kwargs): + try: + function(*args, **kwargs) + assert False, "Expected exception %s" % exception.__name__ + except exception: pass + + def testMissing(self): + self.assertUrl(Url(), None, None, None, None, None, None) + self.assertUrl(Url('amqp://'), 'amqp', None, None, None, None, None) + self.assertUrl(Url('user@'), None, 'user', None, None, None, None) + self.assertUrl(Url(':pass@'), None, '', 'pass', None, None, None) + self.assertUrl(Url('host'), None, None, None, 'host', None, None) + self.assertUrl(Url(':1234'), None, None, None, None, 1234, None) + self.assertUrl(Url('/path'), None, None, None, None, None, 'path') + + for s in ['amqp://', 'user@', ':pass@', ':1234', '/path']: + self.assertEqual(s, str(Url(s))) + + for s, full in [ + ('amqp://', 'amqp://0.0.0.0:amqp'), + ('user@', 'amqp://user@0.0.0.0:amqp'), + (':pass@', 'amqp://:pass@0.0.0.0:amqp'), + (':1234', 'amqp://0.0.0.0:1234'), + ('/path', 'amqp://0.0.0.0:amqp/path')]: + self.assertEqual(str(Url(s).defaults()), full) + + self.assertRaises(ValueError, Url, '') --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org