diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index ef443c47d6..da673312db 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -11,6 +11,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db.backends.postgresql.operations import DatabaseOperations from django.db.utils import ProgrammingError from django.utils.functional import cached_property +from django.utils.version import get_version_tuple from .adapter import PostGISAdapter from .models import PostGISGeometryColumns, PostGISSpatialRefSys @@ -109,7 +110,6 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations): postgis = True geography = True geom_func_prefix = 'ST_' - version_regex = re.compile(r'^(?P\d)\.(?P\d)\.(?P\d+)') Adapter = PostGISAdapter @@ -353,18 +353,8 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations): Return the PostGIS version as a tuple (version string, major, minor, subminor). """ - # Getting the PostGIS version version = self.postgis_lib_version() - m = self.version_regex.match(version) - - if m: - major = int(m.group('major')) - minor1 = int(m.group('minor1')) - minor2 = int(m.group('minor2')) - else: - raise Exception('Could not parse PostGIS version string: %s' % version) - - return (version, major, minor1, minor2) + return (version,) + get_version_tuple(version) def proj_version_tuple(self): """ diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 3458239bed..5575164f91 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -3,8 +3,6 @@ SQL functions reference lists: https://web.archive.org/web/20130407175746/https://www.gaia-gis.it/gaia-sins/spatialite-sql-4.0.0.html https://www.gaia-gis.it/gaia-sins/spatialite-sql-4.2.1.html """ -import re - from django.contrib.gis.db.backends.base.operations import ( BaseSpatialOperations, ) @@ -16,6 +14,7 @@ from django.contrib.gis.measure import Distance from django.core.exceptions import ImproperlyConfigured from django.db.backends.sqlite3.operations import DatabaseOperations from django.utils.functional import cached_property +from django.utils.version import get_version_tuple class SpatiaLiteDistanceOperator(SpatialOperator): @@ -35,7 +34,6 @@ class SpatiaLiteDistanceOperator(SpatialOperator): class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): name = 'spatialite' spatialite = True - version_regex = re.compile(r'^(?P\d)\.(?P\d)\.(?P\d+)') Adapter = SpatiaLiteAdapter @@ -189,16 +187,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): minor, subminor). """ version = self.spatialite_version() - - m = self.version_regex.match(version) - if m: - major = int(m.group('major')) - minor1 = int(m.group('minor1')) - minor2 = int(m.group('minor2')) - else: - raise Exception('Could not parse SpatiaLite version string: %s' % version) - - return (version, major, minor1, minor2) + return (version,) + get_version_tuple(version) def spatial_aggregate_name(self, agg_name): """ diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py index c3904774dd..83df035b07 100644 --- a/django/contrib/gis/geos/collections.py +++ b/django/contrib/gis/geos/collections.py @@ -8,7 +8,7 @@ from ctypes import byref, c_int, c_uint from django.contrib.gis.geos import prototypes as capi from django.contrib.gis.geos.error import GEOSException from django.contrib.gis.geos.geometry import GEOSGeometry, LinearGeometryMixin -from django.contrib.gis.geos.libgeos import geos_version_info, get_pointer_arr +from django.contrib.gis.geos.libgeos import geos_version_tuple, get_pointer_arr from django.contrib.gis.geos.linestring import LinearRing, LineString from django.contrib.gis.geos.point import Point from django.contrib.gis.geos.polygon import Polygon @@ -115,7 +115,7 @@ class MultiLineString(LinearGeometryMixin, GeometryCollection): @property def closed(self): - if geos_version_info()['version'] < '3.5': + if geos_version_tuple() < (3, 5): raise GEOSException("MultiLineString.closed requires GEOS >= 3.5.0.") return super().closed diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index d3fd674918..810161ffeb 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -15,6 +15,7 @@ from ctypes.util import find_library from django.contrib.gis.geos.error import GEOSException from django.core.exceptions import ImproperlyConfigured from django.utils.functional import SimpleLazyObject +from django.utils.version import get_version_tuple logger = logging.getLogger('django.contrib.gis') @@ -199,3 +200,8 @@ def geos_version_info(): raise GEOSException('Could not parse version info string "%s"' % ver) return {key: m.group(key) for key in ( 'version', 'release_candidate', 'capi_version', 'major', 'minor', 'subminor')} + + +def geos_version_tuple(): + """Return the GEOS version as a tuple (major, minor, subminor).""" + return get_version_tuple(geos_version_info()['version']) diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 15adb0282e..ad1fd0fd0b 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -14,6 +14,7 @@ from django.db.backends.base.base import BaseDatabaseWrapper from django.db.utils import DatabaseError as WrappedDatabaseError from django.utils.functional import cached_property from django.utils.safestring import SafeText +from django.utils.version import get_version_tuple try: import psycopg2 as Database @@ -25,7 +26,7 @@ except ImportError as e: def psycopg2_version(): version = psycopg2.__version__.split(' ', 1)[0] - return tuple(int(v) for v in version.split('.') if v.isdigit()) + return get_version_tuple(version) PSYCOPG2_VERSION = psycopg2_version() diff --git a/django/utils/version.py b/django/utils/version.py index 790a68c19c..4569abb608 100644 --- a/django/utils/version.py +++ b/django/utils/version.py @@ -2,6 +2,7 @@ import datetime import functools import os import subprocess +from distutils.version import LooseVersion def get_version(version=None): @@ -77,3 +78,17 @@ def get_git_changeset(): except ValueError: return None return timestamp.strftime('%Y%m%d%H%M%S') + + +def get_version_tuple(version): + """ + Return a tuple of version numbers (e.g. (1, 2, 3)) from the version + string (e.g. '1.2.3'). + """ + loose_version = LooseVersion(version) + version_numbers = [] + for item in loose_version.version: + if not isinstance(item, int): + break + version_numbers.append(item) + return tuple(version_numbers) diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 5e7c6ae99c..8847b178ef 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -323,13 +323,12 @@ class PostgreSQLTests(TestCase): def test_correct_extraction_psycopg2_version(self): from django.db.backends.postgresql.base import psycopg2_version - version_path = 'django.db.backends.postgresql.base.Database.__version__' - with mock.patch(version_path, '2.6.9'): - self.assertEqual(psycopg2_version(), (2, 6, 9)) + with mock.patch('psycopg2.__version__', '4.2.1 (dt dec pq3 ext lo64)'): + self.assertEqual(psycopg2_version(), (4, 2, 1)) - with mock.patch(version_path, '2.5.dev0'): - self.assertEqual(psycopg2_version(), (2, 5)) + with mock.patch('psycopg2.__version__', '4.2b0.dev1 (dt dec pq3 ext lo64)'): + self.assertEqual(psycopg2_version(), (4, 2)) class DateQuotingTest(TestCase): diff --git a/tests/gis_tests/geos_tests/test_geos.py b/tests/gis_tests/geos_tests/test_geos.py index 602c37503d..c75ef31d70 100644 --- a/tests/gis_tests/geos_tests/test_geos.py +++ b/tests/gis_tests/geos_tests/test_geos.py @@ -12,7 +12,7 @@ from django.contrib.gis.geos import ( MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, fromfile, fromstr, ) -from django.contrib.gis.geos.libgeos import geos_version_info +from django.contrib.gis.geos.libgeos import geos_version_tuple from django.contrib.gis.shortcuts import numpy from django.template import Context from django.template.engine import Engine @@ -670,11 +670,11 @@ class GEOSTest(SimpleTestCase, TestDataMixin): self.assertFalse(ls_not_closed.closed) self.assertTrue(ls_closed.closed) - if geos_version_info()['version'] >= '3.5': + if geos_version_tuple() >= (3, 5): self.assertFalse(MultiLineString(ls_closed, ls_not_closed).closed) self.assertTrue(MultiLineString(ls_closed, ls_closed).closed) - with mock.patch('django.contrib.gis.geos.collections.geos_version_info', lambda: {'version': '3.4.9'}): + with mock.patch('django.contrib.gis.geos.libgeos.geos_version_info', lambda: {'version': '3.4.9'}): with self.assertRaisesMessage(GEOSException, "MultiLineString.closed requires GEOS >= 3.5.0."): MultiLineString().closed @@ -1305,6 +1305,10 @@ class GEOSTest(SimpleTestCase, TestDataMixin): self.assertEqual(m.group('version'), v_geos) self.assertEqual(m.group('capi_version'), v_capi) + def test_geos_version_tuple(self): + with mock.patch('django.contrib.gis.geos.libgeos.geos_version_info', lambda: {'version': '3.4.9'}): + self.assertEqual(geos_version_tuple(), (3, 4, 9)) + def test_from_gml(self): self.assertEqual( GEOSGeometry('POINT(0 0)'), diff --git a/tests/gis_tests/tests.py b/tests/gis_tests/tests.py index 6b42384203..2f55406f56 100644 --- a/tests/gis_tests/tests.py +++ b/tests/gis_tests/tests.py @@ -68,6 +68,12 @@ class TestPostGISVersionCheck(unittest.TestCase): actual = ops.postgis_version_tuple() self.assertEqual(expect, actual) + def test_version_loose_tuple(self): + expect = ('1.2.3b1.dev0', 1, 2, 3) + ops = FakePostGISOperations(expect[0]) + actual = ops.postgis_version_tuple() + self.assertEqual(expect, actual) + def test_valid_version_numbers(self): versions = [ ('1.3.0', 1, 3, 0), @@ -81,15 +87,6 @@ class TestPostGISVersionCheck(unittest.TestCase): actual = ops.spatial_version self.assertEqual(version[1:], actual) - def test_invalid_version_numbers(self): - versions = ['nope', '123'] - - for version in versions: - with self.subTest(version=version): - ops = FakePostGISOperations(version) - with self.assertRaises(Exception): - ops.spatial_version - def test_no_version_number(self): ops = FakePostGISOperations() with self.assertRaises(ImproperlyConfigured): diff --git a/tests/version/tests.py b/tests/version/tests.py index b9541cd31a..c0075744bc 100644 --- a/tests/version/tests.py +++ b/tests/version/tests.py @@ -1,5 +1,6 @@ from django import get_version from django.test import SimpleTestCase +from django.utils.version import get_version_tuple class VersionTests(SimpleTestCase): @@ -22,3 +23,8 @@ class VersionTests(SimpleTestCase): ) for ver_tuple, ver_string in tuples_to_strings: self.assertEqual(get_version(ver_tuple), ver_string) + + def test_get_version_tuple(self): + self.assertEqual(get_version_tuple('1.2.3'), (1, 2, 3)) + self.assertEqual(get_version_tuple('1.2.3b2'), (1, 2, 3)) + self.assertEqual(get_version_tuple('1.2.3b2.dev0'), (1, 2, 3))