diff --git a/CHANGES.txt b/CHANGES.txt index 22cd5348..7d58509a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,6 +5,8 @@ Changes for crate Unreleased ========== +- Add deprecation warning about dropping support for SQLAlchemy 1.3 soon, it is + effectively EOL. 2022/12/08 0.29.0 diff --git a/src/crate/client/sqlalchemy/__init__.py b/src/crate/client/sqlalchemy/__init__.py index 52864719..2a7a1da7 100644 --- a/src/crate/client/sqlalchemy/__init__.py +++ b/src/crate/client/sqlalchemy/__init__.py @@ -23,10 +23,28 @@ from .dialect import CrateDialect from .sa_version import SA_1_4, SA_VERSION -# SQLAlchemy 1.3 does not have the `exec_driver_sql` method. + if SA_VERSION < SA_1_4: + import textwrap + import warnings + + # SQLAlchemy 1.3 is effectively EOL. + SA13_DEPRECATION_WARNING = textwrap.dedent(""" + WARNING: SQLAlchemy 1.3 is effectively EOL. + + SQLAlchemy 1.3 is EOL since 2023-01-27. + Future versions of the CrateDB SQLAlchemy dialect will drop support for SQLAlchemy 1.3. + It is recommended that you transition to using SQLAlchemy 1.4 or 2.0: + + - https://docs.sqlalchemy.org/en/14/changelog/migration_14.html + - https://docs.sqlalchemy.org/en/20/changelog/migration_20.html + """.lstrip("\n")) + warnings.warn(message=SA13_DEPRECATION_WARNING, category=DeprecationWarning) + + # SQLAlchemy 1.3 does not have the `exec_driver_sql` method, so add it. monkeypatch_add_exec_driver_sql() + __all__ = [ CrateDialect, ] diff --git a/src/crate/client/sqlalchemy/tests/__init__.py b/src/crate/client/sqlalchemy/tests/__init__.py index 61a2669b..00abe4bf 100644 --- a/src/crate/client/sqlalchemy/tests/__init__.py +++ b/src/crate/client/sqlalchemy/tests/__init__.py @@ -21,6 +21,7 @@ from .array_test import SqlAlchemyArrayTypeTest from .dialect_test import SqlAlchemyDialectTest from .function_test import SqlAlchemyFunctionTest +from .warnings_test import SqlAlchemyWarningsTest def test_suite(): @@ -38,4 +39,5 @@ def test_suite(): tests.addTest(makeSuite(SqlAlchemyDialectTest)) tests.addTest(makeSuite(SqlAlchemyFunctionTest)) tests.addTest(makeSuite(SqlAlchemyArrayTypeTest)) + tests.addTest(makeSuite(SqlAlchemyWarningsTest)) return tests diff --git a/src/crate/client/sqlalchemy/tests/warnings_test.py b/src/crate/client/sqlalchemy/tests/warnings_test.py new file mode 100644 index 00000000..c300ad8c --- /dev/null +++ b/src/crate/client/sqlalchemy/tests/warnings_test.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8; -*- +import sys +import warnings +from unittest import TestCase, skipIf + +from crate.client.sqlalchemy import SA_1_4, SA_VERSION +from crate.testing.util import ExtraAssertions + + +class SqlAlchemyWarningsTest(TestCase, ExtraAssertions): + + @skipIf(SA_VERSION >= SA_1_4, "There is no deprecation warning for " + "SQLAlchemy 1.3 on higher versions") + def test_sa13_deprecation_warning(self): + """ + Verify that a `DeprecationWarning` is issued when running SQLAlchemy 1.3. + + https://docs.python.org/3/library/warnings.html#testing-warnings + """ + with warnings.catch_warnings(record=True) as w: + + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + + # Trigger a warning by importing the SQLAlchemy dialect module. + # Because it already has been loaded, unload it beforehand. + del sys.modules["crate.client.sqlalchemy"] + import crate.client.sqlalchemy # noqa: F401 + + # Verify details of the SA13 EOL/deprecation warning. + self.assertEqual(len(w), 1) + self.assertIsSubclass(w[-1].category, DeprecationWarning) + self.assertIn("SQLAlchemy 1.3 is effectively EOL.", str(w[-1].message)) diff --git a/src/crate/testing/util.py b/src/crate/testing/util.py new file mode 100644 index 00000000..3e9885d6 --- /dev/null +++ b/src/crate/testing/util.py @@ -0,0 +1,20 @@ +class ExtraAssertions: + """ + Additional assert methods for unittest. + + - https://github.com/python/cpython/issues/71339 + - https://bugs.python.org/issue14819 + - https://bugs.python.org/file43047/extra_assertions.patch + """ + + def assertIsSubclass(self, cls, superclass, msg=None): + try: + r = issubclass(cls, superclass) + except TypeError: + if not isinstance(cls, type): + self.fail(self._formatMessage(msg, + '%r is not a class' % (cls,))) + raise + if not r: + self.fail(self._formatMessage(msg, + '%r is not a subclass of %r' % (cls, superclass)))