From 9d93dff33338c90a55f7158fbbe0d82e88e13fa3 Mon Sep 17 00:00:00 2001
From: Simon Charette <charettes@users.noreply.github.com>
Date: Fri, 6 Oct 2017 12:47:08 -0400
Subject: [PATCH] Fixed #28665 -- Change some database exceptions to
 NotImplementedError per PEP 249.

---
 django/contrib/gis/db/backends/base/operations.py    | 5 +++--
 django/contrib/gis/db/backends/postgis/operations.py | 4 ++--
 django/contrib/gis/db/models/functions.py            | 9 +++++----
 django/db/backends/base/features.py                  | 6 +++---
 django/db/backends/base/operations.py                | 2 +-
 django/db/backends/sqlite3/operations.py             | 2 +-
 django/db/models/lookups.py                          | 2 +-
 docs/releases/2.1.txt                                | 4 +++-
 tests/backends/sqlite/tests.py                       | 9 +++++----
 tests/custom_lookups/tests.py                        | 2 +-
 tests/gis_tests/distapp/tests.py                     | 6 +++---
 tests/gis_tests/geoapp/test_functions.py             | 4 ++--
 tests/gis_tests/geoapp/tests.py                      | 4 ++--
 tests/gis_tests/geogapp/tests.py                     | 4 ++--
 tests/gis_tests/relatedapp/tests.py                  | 6 +++---
 15 files changed, 37 insertions(+), 32 deletions(-)

diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py
index af85b83df6..4b1be4bedc 100644
--- a/django/contrib/gis/db/backends/base/operations.py
+++ b/django/contrib/gis/db/backends/base/operations.py
@@ -3,6 +3,7 @@ from django.contrib.gis.db.models.functions import Distance
 from django.contrib.gis.measure import (
     Area as AreaMeasure, Distance as DistanceMeasure,
 )
+from django.db.utils import NotSupportedError
 from django.utils.functional import cached_property
 
 
@@ -105,7 +106,7 @@ class BaseSpatialOperations:
 
     def check_expression_support(self, expression):
         if isinstance(expression, self.disallowed_aggregates):
-            raise NotImplementedError(
+            raise NotSupportedError(
                 "%s spatial aggregation is not supported by this database backend." % expression.name
             )
         super().check_expression_support(expression)
@@ -115,7 +116,7 @@ class BaseSpatialOperations:
 
     def spatial_function_name(self, func_name):
         if func_name in self.unsupported_functions:
-            raise NotImplementedError("This backend doesn't support the %s function." % func_name)
+            raise NotSupportedError("This backend doesn't support the %s function." % func_name)
         return self.function_names.get(func_name, self.geom_func_prefix + func_name)
 
     # Routines for getting the OGC-compliant models.
diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
index 51c8d5006e..4fb9e61ceb 100644
--- a/django/contrib/gis/db/backends/postgis/operations.py
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -13,7 +13,7 @@ from django.contrib.gis.measure import Distance
 from django.core.exceptions import ImproperlyConfigured
 from django.db.backends.postgresql.operations import DatabaseOperations
 from django.db.models import Func, Value
-from django.db.utils import ProgrammingError
+from django.db.utils import NotSupportedError, ProgrammingError
 from django.utils.functional import cached_property
 from django.utils.version import get_version_tuple
 
@@ -231,7 +231,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
             geom_type = f.geom_type
         if f.geography:
             if f.srid != 4326:
-                raise NotImplementedError('PostGIS only supports geography columns with an SRID of 4326.')
+                raise NotSupportedError('PostGIS only supports geography columns with an SRID of 4326.')
 
             return 'geography(%s,%d)' % (geom_type, f.srid)
         else:
diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py
index 3dea6cbd13..fc46823b19 100644
--- a/django/contrib/gis/db/models/functions.py
+++ b/django/contrib/gis/db/models/functions.py
@@ -9,6 +9,7 @@ from django.db.models import (
 )
 from django.db.models.expressions import Func, Value
 from django.db.models.functions import Cast
+from django.db.utils import NotSupportedError
 from django.utils.functional import cached_property
 
 NUMERIC_TYPES = (int, float, Decimal)
@@ -123,7 +124,7 @@ class Area(OracleToleranceMixin, GeoFunc):
 
     def as_sql(self, compiler, connection, **extra_context):
         if not connection.features.supports_area_geodetic and self.geo_field.geodetic(connection):
-            raise NotImplementedError('Area on geodetic coordinate systems not supported.')
+            raise NotSupportedError('Area on geodetic coordinate systems not supported.')
         return super().as_sql(compiler, connection, **extra_context)
 
     def as_sqlite(self, compiler, connection, **extra_context):
@@ -316,7 +317,7 @@ class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
 
     def as_sql(self, compiler, connection, **extra_context):
         if self.geo_field.geodetic(connection) and not connection.features.supports_length_geodetic:
-            raise NotImplementedError("This backend doesn't support Length on geodetic fields")
+            raise NotSupportedError("This backend doesn't support Length on geodetic fields")
         return super().as_sql(compiler, connection, **extra_context)
 
     def as_postgresql(self, compiler, connection):
@@ -372,7 +373,7 @@ class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
     def as_postgresql(self, compiler, connection):
         function = None
         if self.geo_field.geodetic(connection) and not self.source_is_geography():
-            raise NotImplementedError("ST_Perimeter cannot use a non-projected non-geography field.")
+            raise NotSupportedError("ST_Perimeter cannot use a non-projected non-geography field.")
         dim = min(f.dim for f in self.get_source_fields())
         if dim > 2:
             function = connection.ops.perimeter3d
@@ -380,7 +381,7 @@ class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
 
     def as_sqlite(self, compiler, connection):
         if self.geo_field.geodetic(connection):
-            raise NotImplementedError("Perimeter cannot use a non-projected field.")
+            raise NotSupportedError("Perimeter cannot use a non-projected field.")
         return super().as_sql(compiler, connection)
 
 
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py
index 3a89cc0900..3706e12db1 100644
--- a/django/db/backends/base/features.py
+++ b/django/db/backends/base/features.py
@@ -1,5 +1,5 @@
 from django.db.models.aggregates import StdDev
-from django.db.utils import ProgrammingError
+from django.db.utils import NotSupportedError, ProgrammingError
 from django.utils.functional import cached_property
 
 
@@ -269,9 +269,9 @@ class BaseDatabaseFeatures:
         """Confirm support for STDDEV and related stats functions."""
         try:
             self.connection.ops.check_expression_support(StdDev(1))
-            return True
-        except NotImplementedError:
+        except NotSupportedError:
             return False
+        return True
 
     def introspected_boolean_field_type(self, field=None):
         """
diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py
index 3517300b50..3d476b77da 100644
--- a/django/db/backends/base/operations.py
+++ b/django/db/backends/base/operations.py
@@ -579,7 +579,7 @@ class BaseDatabaseOperations:
         This is used on specific backends to rule out known expressions
         that have problematic or nonexistent implementations. If the
         expression has a known problem, the backend should raise
-        NotImplementedError.
+        NotSupportedError.
         """
         pass
 
diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py
index 408848c9ad..e90cc052d0 100644
--- a/django/db/backends/sqlite3/operations.py
+++ b/django/db/backends/sqlite3/operations.py
@@ -43,7 +43,7 @@ class DatabaseOperations(BaseDatabaseOperations):
                     pass
                 else:
                     if isinstance(output_field, bad_fields):
-                        raise NotImplementedError(
+                        raise utils.NotSupportedError(
                             'You cannot use Sum, Avg, StdDev, and Variance '
                             'aggregations on date/time fields in sqlite3 '
                             'since date/time is saved as text.'
diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py
index f79f435515..66945c4a1a 100644
--- a/django/db/models/lookups.py
+++ b/django/db/models/lookups.py
@@ -25,7 +25,7 @@ class Lookup:
             # a bilateral transformation on a nested QuerySet: that won't work.
             from django.db.models.sql.query import Query  # avoid circular import
             if isinstance(rhs, Query):
-                raise NotImplementedError("Bilateral transformations on nested querysets are not supported.")
+                raise NotImplementedError("Bilateral transformations on nested querysets are not implemented.")
         self.bilateral_transforms = bilateral_transforms
 
     def apply_bilateral_transforms(self, value):
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 6a451bdd50..516f6163eb 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -198,7 +198,9 @@ Backwards incompatible changes in 2.1
 Database backend API
 --------------------
 
-* ...
+* To adhere to :pep:`249`, exceptions where a database doesn't support a
+  feature are changed from :exc:`NotImplementedError` to
+  :exc:`django.db.NotSupportedError`.
 
 :mod:`django.contrib.gis`
 -------------------------
diff --git a/tests/backends/sqlite/tests.py b/tests/backends/sqlite/tests.py
index 3addcc8c34..0c07f95e6f 100644
--- a/tests/backends/sqlite/tests.py
+++ b/tests/backends/sqlite/tests.py
@@ -4,6 +4,7 @@ import unittest
 
 from django.db import connection
 from django.db.models import Avg, StdDev, Sum, Variance
+from django.db.utils import NotSupportedError
 from django.test import TestCase, TransactionTestCase, override_settings
 
 from ..models import Item, Object, Square
@@ -34,13 +35,13 @@ class Tests(TestCase):
         Raise NotImplementedError when aggregating on date/time fields (#19360).
         """
         for aggregate in (Sum, Avg, Variance, StdDev):
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaises(NotSupportedError):
                 Item.objects.all().aggregate(aggregate('time'))
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaises(NotSupportedError):
                 Item.objects.all().aggregate(aggregate('date'))
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaises(NotSupportedError):
                 Item.objects.all().aggregate(aggregate('last_modified'))
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaises(NotSupportedError):
                 Item.objects.all().aggregate(
                     **{'complex': aggregate('last_modified') + aggregate('last_modified')}
                 )
diff --git a/tests/custom_lookups/tests.py b/tests/custom_lookups/tests.py
index d39ebe6cdc..bdb27a224a 100644
--- a/tests/custom_lookups/tests.py
+++ b/tests/custom_lookups/tests.py
@@ -319,7 +319,7 @@ class BilateralTransformTests(TestCase):
 
     def test_bilateral_inner_qs(self):
         with register_lookup(models.CharField, UpperBilateralTransform):
-            msg = 'Bilateral transformations on nested querysets are not supported.'
+            msg = 'Bilateral transformations on nested querysets are not implemented.'
             with self.assertRaisesMessage(NotImplementedError, msg):
                 Author.objects.filter(name__upper__in=Author.objects.values_list('name'))
 
diff --git a/tests/gis_tests/distapp/tests.py b/tests/gis_tests/distapp/tests.py
index d162759513..e9735de074 100644
--- a/tests/gis_tests/distapp/tests.py
+++ b/tests/gis_tests/distapp/tests.py
@@ -5,7 +5,7 @@ from django.contrib.gis.db.models.functions import (
 )
 from django.contrib.gis.geos import GEOSGeometry, LineString, Point
 from django.contrib.gis.measure import D  # alias for Distance
-from django.db import connection
+from django.db import NotSupportedError, connection
 from django.db.models import F, Q
 from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
 
@@ -474,7 +474,7 @@ class DistanceFunctionsTests(FuncTestMixin, TestCase):
             # TODO: test with spheroid argument (True and False)
         else:
             # Does not support geodetic coordinate systems.
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaises(NotSupportedError):
                 list(Interstate.objects.annotate(length=Length('path')))
 
         # Now doing length on a projected coordinate system.
@@ -513,7 +513,7 @@ class DistanceFunctionsTests(FuncTestMixin, TestCase):
         if connection.features.supports_perimeter_geodetic:
             self.assertAlmostEqual(qs1[0].perim.m, 18406.3818954314, 3)
         else:
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaises(NotSupportedError):
                 list(qs1)
         # But should work fine when transformed to projected coordinates
         qs2 = CensusZipcode.objects.annotate(perim=Perimeter(Transform('poly', 32140))).filter(name='77002')
diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py
index cdd05d78ff..33fe139fb0 100644
--- a/tests/gis_tests/geoapp/test_functions.py
+++ b/tests/gis_tests/geoapp/test_functions.py
@@ -8,7 +8,7 @@ from django.contrib.gis.geos import (
     GEOSGeometry, LineString, Point, Polygon, fromstr,
 )
 from django.contrib.gis.measure import Area
-from django.db import connection
+from django.db import NotSupportedError, connection
 from django.db.models import Sum
 from django.test import TestCase, skipUnlessDBFeature
 
@@ -28,7 +28,7 @@ class GISFunctionsTests(FuncTestMixin, TestCase):
     def test_asgeojson(self):
         # Only PostGIS and SpatiaLite support GeoJSON.
         if not connection.features.has_AsGeoJSON_function:
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaises(NotSupportedError):
                 list(Country.objects.annotate(json=functions.AsGeoJSON('mpoly')))
             return
 
diff --git a/tests/gis_tests/geoapp/tests.py b/tests/gis_tests/geoapp/tests.py
index f9838b461b..52a172792a 100644
--- a/tests/gis_tests/geoapp/tests.py
+++ b/tests/gis_tests/geoapp/tests.py
@@ -8,7 +8,7 @@ from django.contrib.gis.geos import (
     MultiPoint, MultiPolygon, Point, Polygon, fromstr,
 )
 from django.core.management import call_command
-from django.db import connection
+from django.db import NotSupportedError, connection
 from django.test import TestCase, skipUnlessDBFeature
 
 from ..utils import (
@@ -516,7 +516,7 @@ class GeoQuerySetTest(TestCase):
         Testing the `MakeLine` aggregate.
         """
         if not connection.features.supports_make_line_aggr:
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaises(NotSupportedError):
                 City.objects.all().aggregate(MakeLine('point'))
             return
 
diff --git a/tests/gis_tests/geogapp/tests.py b/tests/gis_tests/geogapp/tests.py
index c9986fd78b..7f6c441ba5 100644
--- a/tests/gis_tests/geogapp/tests.py
+++ b/tests/gis_tests/geogapp/tests.py
@@ -7,7 +7,7 @@ from unittest import skipIf, skipUnless
 from django.contrib.gis.db import models
 from django.contrib.gis.db.models.functions import Area, Distance
 from django.contrib.gis.measure import D
-from django.db import connection
+from django.db import NotSupportedError, connection
 from django.db.models.functions import Cast
 from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
 
@@ -152,5 +152,5 @@ class GeographyFunctionTests(FuncTestMixin, TestCase):
     @skipUnlessDBFeature("has_Area_function")
     @skipIfDBFeature("supports_area_geodetic")
     def test_geodetic_area_raises_if_not_supported(self):
-        with self.assertRaisesMessage(NotImplementedError, 'Area on geodetic coordinate systems not supported.'):
+        with self.assertRaisesMessage(NotSupportedError, 'Area on geodetic coordinate systems not supported.'):
             Zipcode.objects.annotate(area=Area('poly')).get(code='77002')
diff --git a/tests/gis_tests/relatedapp/tests.py b/tests/gis_tests/relatedapp/tests.py
index 8d6b793ce2..ba812fa9fb 100644
--- a/tests/gis_tests/relatedapp/tests.py
+++ b/tests/gis_tests/relatedapp/tests.py
@@ -1,6 +1,6 @@
 from django.contrib.gis.db.models import Collect, Count, Extent, F, Union
 from django.contrib.gis.geos import GEOSGeometry, MultiPoint, Point
-from django.db import connection
+from django.db import NotSupportedError, connection
 from django.test import TestCase, skipUnlessDBFeature
 from django.test.utils import override_settings
 from django.utils import timezone
@@ -147,7 +147,7 @@ class RelatedGeoModelTest(TestCase):
             self.assertEqual('P2', qs.get().name)
         else:
             msg = "This backend doesn't support the Transform function."
-            with self.assertRaisesMessage(NotImplementedError, msg):
+            with self.assertRaisesMessage(NotSupportedError, msg):
                 list(qs)
 
         # Should return the first Parcel, which has the center point equal
@@ -162,7 +162,7 @@ class RelatedGeoModelTest(TestCase):
             self.assertEqual('P1', qs.get().name)
         else:
             msg = "This backend doesn't support the Transform function."
-            with self.assertRaisesMessage(NotImplementedError, msg):
+            with self.assertRaisesMessage(NotSupportedError, msg):
                 list(qs)
 
     def test07_values(self):