← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stub/lazr-postgresql/lpbits into lp:lazr-postgresql

 

Stuart Bishop has proposed merging lp:~stub/lazr-postgresql/lpbits into lp:lazr-postgresql with lp:~stub/lazr-postgresql/devel as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~stub/lazr-postgresql/lpbits/+merge/104385

Extract some helpers from Launchpad.

ConnectionString is used to decode and transform libpq format connection strings.

quoting helpers for identifiers for generating SQL safely and without surprises. You can't use bind variables for quoting identifiers. Well, you could if you registered identifier types with psycopg2, but to create those types you will need these helpers...
-- 
https://code.launchpad.net/~stub/lazr-postgresql/lpbits/+merge/104385
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stub/lazr-postgresql/lpbits into lp:lazr-postgresql.
=== added file 'src/lazr/postgresql/connectionstring.py'
--- src/lazr/postgresql/connectionstring.py	1970-01-01 00:00:00 +0000
+++ src/lazr/postgresql/connectionstring.py	2012-05-02 13:37:18 +0000
@@ -0,0 +1,119 @@
+# Copyright (c) 2012, Canonical Ltd. All rights reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# GNU General Public License version 3 (see the file LICENSE).
+
+from __future__ import (
+    division, absolute_import, with_statement,
+    print_function, unicode_literals)
+__metaclass__ = type
+
+__all__ = ['ConnectionString']
+
+import re
+
+
+class ConnectionString(object):
+    """A libpq connection string.
+
+    Some PostgreSQL tools take libpq connection strings. Other tools
+    need the components separated out (such as pg_dump command line
+    arguments). This class allows you to switch easily between formats.
+
+    Quoted or escaped values are not supported.
+
+    >>> cs = ConnectionString('user=foo dbname=launchpad_dev')
+    >>> cs.dbname
+    u'launchpad_dev'
+    >>> cs.user
+    u'foo'
+    >>> print(cs)
+    dbname=launchpad_dev user=foo
+    >>> print(repr(cs))
+    'dbname=launchpad_dev user=foo'
+    """
+    CONNECTION_KEYS = [
+        'dbname', 'user', 'host', 'port', 'connect_timeout', 'sslmode']
+
+    def __init__(self, conn_str):
+        if "'" in conn_str or "\\" in conn_str:
+            raise NotImplementedError(
+                "quoted or escaped values are not supported")
+
+        if '=' not in conn_str:
+            # Just a dbname
+            for key in self.CONNECTION_KEYS:
+                setattr(self, key, None)
+            self.dbname = conn_str.strip()
+        else:
+            # A 'key=value' connection string.
+            # We don't check for required attributes, as these might
+            # be added after construction or not actually required
+            # at all in some instances.
+            for key in self.CONNECTION_KEYS:
+                match = re.search(r'%s=([^ ]+)' % key, conn_str)
+                if match is None:
+                    setattr(self, key, None)
+                else:
+                    setattr(self, key, match.group(1))
+
+    def __str__(self):
+        params = []
+        for key in self.CONNECTION_KEYS:
+            val = getattr(self, key, None)
+            if val is not None:
+                params.append('%s=%s' % (key, val))
+        return str(' '.join(params))
+
+    def __repr__(self):
+        return repr(str(self))
+
+    def asPGCommandLineArgs(self):
+        """Return a string suitable for the PostgreSQL standard tools
+        command line arguments.
+
+        >>> cs = ConnectionString('host=localhost user=slony dbname=test')
+        >>> cs.asPGCommandLineArgs()
+        u'--host=localhost --username=slony test'
+
+        >>> cs = ConnectionString('port=5433 dbname=test')
+        >>> cs.asPGCommandLineArgs()
+        u'--port=5433 test'
+        """
+        params = []
+        if self.host is not None:
+            params.append("--host=%s" % self.host)
+        if self.port is not None:
+            params.append("--port=%s" % self.port)
+        if self.user is not None:
+            params.append("--username=%s" % self.user)
+        if self.dbname is not None:
+            params.append(self.dbname)
+        return ' '.join(params)
+
+    def asLPCommandLineArgs(self):
+        """Deprecated! Return a string suitable for use by the LP tools
+        using db_options() to parse the command line.
+
+        >>> cs = ConnectionString('host=localhost user=slony dbname=test')
+        >>> cs.asLPCommandLineArgs()
+        u'--host=localhost --user=slony --dbname=test'
+        """
+        params = []
+        if self.host is not None:
+            params.append("--host=%s" % self.host)
+        if self.user is not None:
+            params.append("--user=%s" % self.user)
+        if self.dbname is not None:
+            params.append("--dbname=%s" % self.dbname)
+        return ' '.join(params)

=== added file 'src/lazr/postgresql/quoting.py'
--- src/lazr/postgresql/quoting.py	1970-01-01 00:00:00 +0000
+++ src/lazr/postgresql/quoting.py	2012-05-02 13:37:18 +0000
@@ -0,0 +1,83 @@
+# Copyright (c) 2012, Canonical Ltd. All rights reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# GNU General Public License version 3 (see the file LICENSE).
+
+from __future__ import (
+    division, absolute_import, with_statement,
+    print_function, unicode_literals)
+__metaclass__ = type
+
+__all__ = ['fqn', 'quote_identifier']
+
+import re
+
+
+def quote_identifier(identifier):
+    r'''Quote an identifier, such as a table or role name.
+
+    In SQL, identifiers are quoted using " rather than ' (which is reserved
+    for strings).
+
+    >>> print(quote_identifier('hello'))
+    "hello"
+
+    Quotes and Unicode are handled if you make use of them in your
+    identifiers.
+
+    >>> print(quote_identifier("'"))
+    "'"
+    >>> print(quote_identifier('"'))
+    """"
+    >>> print(quote_identifier("\\"))
+    "\"
+    >>> print(quote_identifier('\\"'))
+    "\"""
+    >>> print(quote_identifier('\\ aargh \u0441\u043b\u043e\u043d'))
+    U&"\\ aargh \0441\043b\043e\043d"
+    '''
+    try:
+        return '"%s"' % identifier.encode('US-ASCII').replace('"', '""')
+    except UnicodeEncodeError:
+        escaped = []
+        for c in identifier:
+            if c == '\\':
+                escaped.append('\\\\')
+            elif c == '"':
+                escaped.append('""')
+            else:
+                c = c.encode('US-ASCII', 'backslashreplace')
+                # Note Python only supports 32 bit unicode, so we use
+                # the 4 hexdigit PostgreSQL syntax (\1234) rather than
+                # the 6 hexdigit format (\+123456).
+                if c.startswith('\\u'):
+                    c = '\\' + c[2:]
+                escaped.append(c)
+        return 'U&"%s"' % ''.join(escaped)
+
+
+def fqn(namespace, name):
+    """Return the fully qualified name by combining the namespace and name.
+
+    Quoting is done for the non trivial cases.
+
+    >>> print(fqn('public', 'foo'))
+    public.foo
+    >>> print(fqn(' foo ', '$bar'))
+    " foo "."$bar"
+    """
+    if re.search(r"[^a-z_]", namespace) is not None:
+        namespace = quote_identifier(namespace)
+    if re.search(r"[^a-z_]", name) is not None:
+        name = quote_identifier(name)
+    return "%s.%s" % (namespace, name)

=== modified file 'src/lazr/postgresql/tests/__init__.py'
--- src/lazr/postgresql/tests/__init__.py	2012-05-02 13:37:18 +0000
+++ src/lazr/postgresql/tests/__init__.py	2012-05-02 13:37:18 +0000
@@ -15,6 +15,7 @@
 
 """Tests for lazr.postgresql."""
 
+import doctest
 import os
 
 from van.pg import DatabaseManager
@@ -39,5 +40,13 @@
         'upgrade',
         ]
     loader = testresources.TestLoader()
-    return loader.loadTestsFromNames(
+    suite = loader.loadTestsFromNames(
         ['lazr.postgresql.tests.test_' + name for name in test_mod_names])
+
+    import lazr.postgresql.connectionstring
+    suite.addTest(doctest.DocTestSuite(lazr.postgresql.connectionstring))
+
+    import lazr.postgresql.quoting
+    suite.addTest(doctest.DocTestSuite(lazr.postgresql.quoting))
+
+    return suite