From 643ba70bad0f19a68c06ec95de2691c28e060e48 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Tue, 11 Oct 2016 03:58:09 +0100 Subject: [PATCH] Added ipaddress objects conversion Close #387 --- NEWS | 5 +- doc/src/conf.py | 2 +- doc/src/extras.rst | 31 ++++++---- doc/src/usage.rst | 5 +- lib/_ipaddress.py | 89 +++++++++++++++++++++++++++ lib/extras.py | 4 ++ tests/__init__.py | 11 ++-- tests/test_ipaddress.py | 131 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 254 insertions(+), 24 deletions(-) create mode 100644 lib/_ipaddress.py create mode 100755 tests/test_ipaddress.py diff --git a/NEWS b/NEWS index a102e523..b7efef29 100644 --- a/NEWS +++ b/NEWS @@ -19,8 +19,9 @@ New features: - The attributes `~connection.notices` and `~connection.notifies` can be customized replacing them with any object exposing an `!append()` method (:ticket:`#326`). -- old ``inet`` adapters deprecated, but arrays of network types converted - to lists by default (:tickets:`#343, #387`). +- Adapt network types to `ipaddress` objects when available. When not + enabled, convert arrays of network types to lists by default. The old `!Inet` + adapter is deprecated (:tickets:`#317, #343, #387`). - Added `~psycopg2.extensions.quote_ident()` function (:ticket:`#359`). - Added `~connection.get_dsn_parameters()` connection method (:ticket:`#364`). diff --git a/doc/src/conf.py b/doc/src/conf.py index 94ffa349..22c5c46f 100644 --- a/doc/src/conf.py +++ b/doc/src/conf.py @@ -62,7 +62,7 @@ except ImportError: intersphinx_mapping = { 'py': ('http://docs.python.org/', None), - 'py3': ('http://docs.python.org/3.2', None), + 'py3': ('http://docs.python.org/3.4', None), } # Pattern to generate links to the bug tracker diff --git a/doc/src/extras.rst b/doc/src/extras.rst index cf871afe..d33b8eed 100644 --- a/doc/src/extras.rst +++ b/doc/src/extras.rst @@ -933,18 +933,27 @@ UUID data type pair: CIDR; Data types pair: MACADDR; Data types -Networking data type -^^^^^^^^^^^^^^^^^^^^ +.. _adapt-network: -Psycopg casts the PostgreSQL networking data types (:sql:`inet`, :sql:`cidr`, -:sql:`macaddr`) into ordinary strings. However their array are detected as -arrays and directly cast into lists. +Networking data types +^^^^^^^^^^^^^^^^^^^^^ + +By default Psycopg casts the PostgreSQL networking data types (:sql:`inet`, +:sql:`cidr`, :sql:`macaddr`) into ordinary strings; array of such types are +converted into lists of strings. .. versionchanged:: 2.7 - in previous version array of networking types were not treated as arrays + in previous version array of networking types were not treated as arrays. + +.. autofunction:: register_ipaddress + .. autofunction:: register_inet + .. deprecated:: 2.7 + this function will not receive further development and disappear in + future versions. + .. doctest:: >>> psycopg2.extras.register_inet() @@ -957,16 +966,12 @@ arrays and directly cast into lists. >>> cur.fetchone()[0].addr '192.168.0.1/24' -.. deprecated:: 2.7 - this function will not receive further development and disappear in future - versions - .. autoclass:: Inet -.. deprecated:: 2.7 - this object will not receive further development and disappear in future - versions + .. deprecated:: 2.7 + this object will not receive further development and may disappear in + future versions. .. index:: diff --git a/doc/src/usage.rst b/doc/src/usage.rst index 3b42aeb9..e768f372 100644 --- a/doc/src/usage.rst +++ b/doc/src/usage.rst @@ -264,7 +264,10 @@ types: +--------------------+-------------------------+--------------------------+ | Anything\ |tm| | :sql:`json` | :ref:`adapt-json` | +--------------------+-------------------------+--------------------------+ - | `uuid` | :sql:`uuid` | :ref:`adapt-uuid` | + | `~uuid.UUID` | :sql:`uuid` | :ref:`adapt-uuid` | + +--------------------+-------------------------+--------------------------+ + | `ipaddress` | | :sql:`inet` | :ref:`adapt-network` | + | objects | | :sql:`cidr` | | +--------------------+-------------------------+--------------------------+ .. |tm| unicode:: U+2122 diff --git a/lib/_ipaddress.py b/lib/_ipaddress.py new file mode 100644 index 00000000..ee05a260 --- /dev/null +++ b/lib/_ipaddress.py @@ -0,0 +1,89 @@ +"""Implementation of the ipaddres-based network types adaptation +""" + +# psycopg/_ipaddress.py - Ipaddres-based network types adaptation +# +# Copyright (C) 2016 Daniele Varrazzo +# +# psycopg2 is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# In addition, as a special exception, the copyright holders give +# permission to link this program with the OpenSSL library (or with +# modified versions of OpenSSL that use the same license as OpenSSL), +# and distribute linked combinations including the two. +# +# You must obey the GNU Lesser General Public License in all respects for +# all of the code used other than OpenSSL. +# +# psycopg2 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 Lesser General Public +# License for more details. + +from psycopg2.extensions import ( + new_type, new_array_type, register_type, register_adapter, QuotedString) + +# The module is imported on register_ipaddress +ipaddress = None + +# The typecasters are created only once +_casters = None + + +def register_ipaddress(conn_or_curs=None): + """ + Register conversion support between `ipaddress` objects and `network types`__. + + :param conn_or_curs: the scope where to register the type casters. + If `!None` register them globally. + + After the function is called, PostgreSQL :sql:`inet` values will be + converted into `~ipaddress.IPv4Interface` or `~ipaddress.IPv6Interface` + objects, :sql:`cidr` values into into `~ipaddress.IPv4Network` or + `~ipaddress.IPv6Network`. + + .. __: https://www.postgresql.org/docs/current/static/datatype-net-types.html + """ + global ipaddress + import ipaddress + + global _casters + if _casters is None: + _casters = _make_casters() + + for c in _casters: + register_type(c, conn_or_curs) + + for t in [ipaddress.IPv4Interface, ipaddress.IPv6Interface, + ipaddress.IPv4Network, ipaddress.IPv6Network]: + register_adapter(t, adapt_ipaddress) + + +def _make_casters(): + inet = new_type((869,), 'INET', cast_interface) + ainet = new_array_type((1041,), 'INET[]', inet) + + cidr = new_type((650,), 'CIDR', cast_network) + acidr = new_array_type((651,), 'CIDR[]', cidr) + + return [inet, ainet, cidr, acidr] + + +def cast_interface(s, cur=None): + if s is None: + return None + # Py2 version force the use of unicode. meh. + return ipaddress.ip_interface(unicode(s)) + + +def cast_network(s, cur=None): + if s is None: + return None + return ipaddress.ip_network(unicode(s)) + + +def adapt_ipaddress(obj): + return QuotedString(str(obj)) diff --git a/lib/extras.py b/lib/extras.py index a9f5b447..5c4f5d2a 100644 --- a/lib/extras.py +++ b/lib/extras.py @@ -59,6 +59,10 @@ from psycopg2._range import ( # noqa register_range, RangeAdapter, RangeCaster) +# Expose ipaddress-related objects +from psycopg2._ipaddress import register_ipaddress # noqa + + class DictCursorBase(_cursor): """Base class for all dict-like cursors.""" diff --git a/tests/__init__.py b/tests/__init__.py index ada55276..1a240994 100755 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -38,6 +38,7 @@ import test_dates import test_errcodes import test_extras_dictcursor import test_green +import test_ipaddress import test_lobject import test_module import test_notify @@ -46,11 +47,7 @@ import test_quote import test_transaction import test_types_basic import test_types_extras - -if sys.version_info[:2] >= (2, 5): - import test_with -else: - test_with = None +import test_with def test_suite(): @@ -78,6 +75,7 @@ def test_suite(): suite.addTest(test_errcodes.test_suite()) suite.addTest(test_extras_dictcursor.test_suite()) suite.addTest(test_green.test_suite()) + suite.addTest(test_ipaddress.test_suite()) suite.addTest(test_lobject.test_suite()) suite.addTest(test_module.test_suite()) suite.addTest(test_notify.test_suite()) @@ -86,8 +84,7 @@ def test_suite(): suite.addTest(test_transaction.test_suite()) suite.addTest(test_types_basic.test_suite()) suite.addTest(test_types_extras.test_suite()) - if test_with: - suite.addTest(test_with.test_suite()) + suite.addTest(test_with.test_suite()) return suite if __name__ == '__main__': diff --git a/tests/test_ipaddress.py b/tests/test_ipaddress.py new file mode 100755 index 00000000..97eabbaf --- /dev/null +++ b/tests/test_ipaddress.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# # test_ipaddress.py - tests for ipaddress support # +# Copyright (C) 2016 Daniele Varrazzo +# +# psycopg2 is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# psycopg2 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 Lesser General Public +# License for more details. + +from __future__ import unicode_literals + +import sys +from functools import wraps + +from testutils import unittest, ConnectingTestCase, decorate_all_tests + +import psycopg2 +import psycopg2.extras + + +def skip_if_no_ipaddress(f): + @wraps(f) + def skip_if_no_ipaddress_(self): + if sys.version_info[:2] < (3, 3): + try: + import ipaddress # noqa + except ImportError: + return self.skipTest("'ipaddress' module not available") + + return f(self) + + return skip_if_no_ipaddress_ + + +class NetworkingTestCase(ConnectingTestCase): + def test_inet_cast(self): + import ipaddress as ip + cur = self.conn.cursor() + psycopg2.extras.register_ipaddress(cur) + + cur.execute("select null::inet") + self.assert_(cur.fetchone()[0] is None) + + cur.execute("select '127.0.0.1/24'::inet") + obj = cur.fetchone()[0] + self.assert_(isinstance(obj, ip.IPv4Interface), repr(obj)) + self.assertEquals(obj, ip.ip_interface('127.0.0.1/24')) + + cur.execute("select '::ffff:102:300/128'::inet") + obj = cur.fetchone()[0] + self.assert_(isinstance(obj, ip.IPv6Interface), repr(obj)) + self.assertEquals(obj, ip.ip_interface('::ffff:102:300/128')) + + def test_inet_array_cast(self): + import ipaddress as ip + cur = self.conn.cursor() + psycopg2.extras.register_ipaddress(cur) + cur.execute("select '{NULL,127.0.0.1,::ffff:102:300/128}'::inet[]") + l = cur.fetchone()[0] + self.assert_(l[0] is None) + self.assertEquals(l[1], ip.ip_interface('127.0.0.1')) + self.assertEquals(l[2], ip.ip_interface('::ffff:102:300/128')) + self.assert_(isinstance(l[1], ip.IPv4Interface), l) + self.assert_(isinstance(l[2], ip.IPv6Interface), l) + + def test_inet_adapt(self): + import ipaddress as ip + cur = self.conn.cursor() + psycopg2.extras.register_ipaddress(cur) + + cur.execute("select %s", [ip.ip_interface('127.0.0.1/24')]) + self.assertEquals(cur.fetchone()[0], '127.0.0.1/24') + + cur.execute("select %s", [ip.ip_interface('::ffff:102:300/128')]) + self.assertEquals(cur.fetchone()[0], '::ffff:102:300/128') + + def test_cidr_cast(self): + import ipaddress as ip + cur = self.conn.cursor() + psycopg2.extras.register_ipaddress(cur) + + cur.execute("select null::cidr") + self.assert_(cur.fetchone()[0] is None) + + cur.execute("select '127.0.0.0/24'::cidr") + obj = cur.fetchone()[0] + self.assert_(isinstance(obj, ip.IPv4Network), repr(obj)) + self.assertEquals(obj, ip.ip_network('127.0.0.0/24')) + + cur.execute("select '::ffff:102:300/128'::cidr") + obj = cur.fetchone()[0] + self.assert_(isinstance(obj, ip.IPv6Network), repr(obj)) + self.assertEquals(obj, ip.ip_network('::ffff:102:300/128')) + + def test_cidr_array_cast(self): + import ipaddress as ip + cur = self.conn.cursor() + psycopg2.extras.register_ipaddress(cur) + cur.execute("select '{NULL,127.0.0.1,::ffff:102:300/128}'::cidr[]") + l = cur.fetchone()[0] + self.assert_(l[0] is None) + self.assertEquals(l[1], ip.ip_network('127.0.0.1')) + self.assertEquals(l[2], ip.ip_network('::ffff:102:300/128')) + self.assert_(isinstance(l[1], ip.IPv4Network), l) + self.assert_(isinstance(l[2], ip.IPv6Network), l) + + def test_cidr_adapt(self): + import ipaddress as ip + cur = self.conn.cursor() + psycopg2.extras.register_ipaddress(cur) + + cur.execute("select %s", [ip.ip_network('127.0.0.0/24')]) + self.assertEquals(cur.fetchone()[0], '127.0.0.0/24') + + cur.execute("select %s", [ip.ip_network('::ffff:102:300/128')]) + self.assertEquals(cur.fetchone()[0], '::ffff:102:300/128') + +decorate_all_tests(NetworkingTestCase, skip_if_no_ipaddress) + + +def test_suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + + +if __name__ == "__main__": + unittest.main()