From 08bda82c237b9ad071538c2386b8bfc62fef7f7f Mon Sep 17 00:00:00 2001
From: chillaranand <anand21nanda@gmail.com>
Date: Wed, 15 Feb 2017 21:44:21 +0530
Subject: [PATCH] Fixed #27830 -- Used distutils.version.LooseVersion for
 version parsing.

---
 .../contrib/gis/db/backends/postgis/operations.py | 14 ++------------
 .../gis/db/backends/spatialite/operations.py      | 15 ++-------------
 django/contrib/gis/geos/collections.py            |  4 ++--
 django/contrib/gis/geos/libgeos.py                |  6 ++++++
 django/db/backends/postgresql/base.py             |  3 ++-
 django/utils/version.py                           | 15 +++++++++++++++
 tests/backends/tests.py                           |  9 ++++-----
 tests/gis_tests/geos_tests/test_geos.py           | 10 +++++++---
 tests/gis_tests/tests.py                          | 15 ++++++---------
 tests/version/tests.py                            |  6 ++++++
 10 files changed, 52 insertions(+), 45 deletions(-)

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<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\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<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\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))