]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Disable "check unicode returns" under Python 3
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 7 May 2020 14:53:15 +0000 (10:53 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 19 May 2020 17:53:39 +0000 (13:53 -0400)
Disabled the "unicode returns" check that runs on dialect startup when
running under Python 3, which for many years has occurred in order to test
the current DBAPI's behavior for whether or not it returns Python Unicode
or Py2K strings for the VARCHAR and NVARCHAR datatypes.  The check still
occurs by default under Python 2, however the mechanism to test the
behavior will be removed in SQLAlchemy 2.0 when Python 2 support is also
removed.

This logic was very effective when it was needed, however now that Python 3
is standard, all DBAPIs are expected to return Python 3 strings for
character datatypes.  In the unlikely case that a third party DBAPI does
not support this, the conversion logic within :class:`.String` is still
available and the third party dialect may specify this in its upfront
dialect flags by setting the dialect level flag ``returns_unicode_strings``
to one of :attr:`.String.RETURNS_CONDITIONAL` or
:attr:`.String.RETURNS_BYTES`, both of which will enable Unicode conversion
even under Python 3.

As part of this change, disabling testing of the doctest tutorials under
Python 2.

Fixes: #5315
Change-Id: I1260e894611409d3b7fe1a92bd90c52043bbcf19

doc/build/changelog/unreleased_14/5315.rst [new file with mode: 0644]
doc/build/core/tutorial.rst
doc/build/orm/tutorial.rst
lib/sqlalchemy/engine/default.py
lib/sqlalchemy/sql/sqltypes.py
test/base/test_tutorials.py
test/engine/test_execute.py
test/engine/test_reconnect.py
test/sql/test_types.py

diff --git a/doc/build/changelog/unreleased_14/5315.rst b/doc/build/changelog/unreleased_14/5315.rst
new file mode 100644 (file)
index 0000000..ff0073c
--- /dev/null
@@ -0,0 +1,21 @@
+.. change::
+    :tags: change, performance, engine, py3k
+    :tickets: 5315
+
+    Disabled the "unicode returns" check that runs on dialect startup when
+    running under Python 3, which for many years has occurred in order to test
+    the current DBAPI's behavior for whether or not it returns Python Unicode
+    or Py2K strings for the VARCHAR and NVARCHAR datatypes.  The check still
+    occurs by default under Python 2, however the mechanism to test the
+    behavior will be removed in SQLAlchemy 2.0 when Python 2 support is also
+    removed.
+
+    This logic was very effective when it was needed, however now that Python 3
+    is standard, all DBAPIs are expected to return Python 3 strings for
+    character datatypes.  In the unlikely case that a third party DBAPI does
+    not support this, the conversion logic within :class:`.String` is still
+    available and the third party dialect may specify this in its upfront
+    dialect flags by setting the dialect level flag ``returns_unicode_strings``
+    to one of :attr:`.String.RETURNS_CONDITIONAL` or
+    :attr:`.String.RETURNS_BYTES`, both of which will enable Unicode conversion
+    even under Python 3.
index 1f475abe34d91f6c6a3f7b6d88bd412d46d6c9ef..a504f85785b61d1506bf0229fd5eb3fb05f9d76e 100644 (file)
@@ -150,7 +150,7 @@ each table first before creating, so it's safe to call multiple times:
 .. sourcecode:: pycon+sql
 
     {sql}>>> metadata.create_all(engine)
-    SE...
+    PRAGMA...
     CREATE TABLE users (
         id INTEGER NOT NULL,
         name VARCHAR,
index e77964a7b6c314287b8e8fdacedd5ca05b702138..c08daa7fd6fcef612e6b02b60b33fe3db61adf46 100644 (file)
@@ -209,7 +209,6 @@ the actual ``CREATE TABLE`` statement:
 .. sourcecode:: python+sql
 
     >>> Base.metadata.create_all(engine)
-    SELECT ...
     PRAGMA main.table_info("users")
     ()
     PRAGMA temp.table_info("users")
index 20f73111602800c7a3e44f1ca3c4fc076ea0063b..d9b4cdda69dc45b24c5e85eda14dfafb72896f14 100644 (file)
@@ -94,12 +94,12 @@ class DefaultDialect(interfaces.Dialect):
     if util.py3k:
         supports_unicode_statements = True
         supports_unicode_binds = True
-        returns_unicode_strings = True
+        returns_unicode_strings = sqltypes.String.RETURNS_UNICODE
         description_encoding = None
     else:
         supports_unicode_statements = False
         supports_unicode_binds = False
-        returns_unicode_strings = False
+        returns_unicode_strings = sqltypes.String.RETURNS_UNKNOWN
         description_encoding = "use_encoding"
 
     name = "default"
@@ -325,7 +325,14 @@ class DefaultDialect(interfaces.Dialect):
         except NotImplementedError:
             self.default_isolation_level = None
 
-        self.returns_unicode_strings = self._check_unicode_returns(connection)
+        if self.returns_unicode_strings is sqltypes.String.RETURNS_UNKNOWN:
+            if util.py3k:
+                raise exc.InvalidRequestError(
+                    "RETURNS_UNKNOWN is unsupported in Python 3"
+                )
+            self.returns_unicode_strings = self._check_unicode_returns(
+                connection
+            )
 
         if (
             self.description_encoding is not None
@@ -415,9 +422,13 @@ class DefaultDialect(interfaces.Dialect):
         results = {check_unicode(test) for test in tests}
 
         if results.issuperset([True, False]):
-            return "conditional"
+            return sqltypes.String.RETURNS_CONDITIONAL
         else:
-            return results == {True}
+            return (
+                sqltypes.String.RETURNS_UNICODE
+                if results == {True}
+                else sqltypes.String.RETURNS_BYTES
+            )
 
     def _check_unicode_description(self, connection):
         # all DBAPIs on Py2K return cursor.description as encoded
index a65989e93179d72b2ca56b44952a58094175efe2..8684a792269f05b60535bb159a8ad4328f96a3aa 100644 (file)
@@ -135,6 +135,67 @@ class String(Concatenable, TypeEngine):
 
     __visit_name__ = "string"
 
+    RETURNS_UNICODE = util.symbol(
+        "RETURNS_UNICODE",
+        """Indicates that the DBAPI returns Python Unicode for VARCHAR,
+        NVARCHAR, and other character-based datatypes in all cases.
+
+        This is the default value for
+        :attr:`.DefaultDialect.returns_unicode_strings` under Python 3.
+
+        .. versionadded:: 1.4
+
+        """,
+    )
+
+    RETURNS_BYTES = util.symbol(
+        "RETURNS_BYTES",
+        """Indicates that the DBAPI returns byte objects under Python 3
+        or non-Unicode string objects under Python 2 for VARCHAR, NVARCHAR,
+        and other character-based datatypes in all cases.
+
+        This may be applied to the
+        :attr:`.DefaultDialect.returns_unicode_strings` attribute.
+
+        .. versionadded:: 1.4
+
+        """,
+    )
+
+    RETURNS_CONDITIONAL = util.symbol(
+        "RETURNS_CONDITIONAL",
+        """Indicates that the DBAPI may return Unicode or bytestrings for
+        VARCHAR, NVARCHAR, and other character-based datatypes, and that
+        SQLAlchemy's default String datatype will need to test on a per-row
+        basis for Unicode or bytes.
+
+        This may be applied to the
+        :attr:`.DefaultDialect.returns_unicode_strings` attribute.
+
+        .. versionadded:: 1.4
+
+        """,
+    )
+
+    RETURNS_UNKNOWN = util.symbol(
+        "RETURNS_UNKNOWN",
+        """Indicates that the dialect should test on first connect what the
+        string-returning behavior of character-based datatypes is.
+
+        This is the default value for DefaultDialect.unicode_returns under
+        Python 2.
+
+        This may be applied to the
+        :attr:`.DefaultDialect.returns_unicode_strings` attribute under
+        Python 2 only.   The value is disallowed under Python 3.
+
+        .. versionadded:: 1.4
+
+        .. deprecated:: 1.4  This value will be removed in SQLAlchemy 2.0.
+
+        """,
+    )
+
     @util.deprecated_params(
         convert_unicode=(
             "1.3",
@@ -293,12 +354,13 @@ class String(Concatenable, TypeEngine):
     def result_processor(self, dialect, coltype):
         wants_unicode = self._expect_unicode or dialect.convert_unicode
         needs_convert = wants_unicode and (
-            dialect.returns_unicode_strings is not True
+            dialect.returns_unicode_strings is not String.RETURNS_UNICODE
             or self._expect_unicode in ("force", "force_nocheck")
         )
         needs_isinstance = (
             needs_convert
             and dialect.returns_unicode_strings
+            in (String.RETURNS_CONDITIONAL, String.RETURNS_UNICODE,)
             and self._expect_unicode != "force_nocheck"
         )
         if needs_convert:
index 3ac7913f9d331bf5e1d87a533c8eddbfcd106281..69f9f7e9001180b763ae6c4c25e400f1a5f2e2bf 100644 (file)
@@ -12,6 +12,8 @@ from sqlalchemy.testing import fixtures
 
 
 class DocTest(fixtures.TestBase):
+    __requires__ = ("python3",)
+
     def _setup_logger(self):
         rootlogger = logging.getLogger("sqlalchemy.engine.Engine")
 
index a8d7b397ec1c2cb6db23f4329c5182721fb11d49..285dd2acf700a35ca73c2991a6db874844c81edf 100644 (file)
@@ -648,6 +648,32 @@ class ExecuteTest(fixtures.TablesTest):
         eng2 = eng.execution_options(foo="bar")
         assert eng2._has_events
 
+    def test_works_after_dispose(self):
+        eng = create_engine(testing.db.url)
+        for i in range(3):
+            eq_(eng.scalar(select([1])), 1)
+            eng.dispose()
+
+    def test_works_after_dispose_testing_engine(self):
+        eng = engines.testing_engine()
+        for i in range(3):
+            eq_(eng.scalar(select([1])), 1)
+            eng.dispose()
+
+
+class UnicodeReturnsTest(fixtures.TestBase):
+    @testing.requires.python3
+    def test_unicode_test_not_in_python3(self):
+        eng = engines.testing_engine()
+        eng.dialect.returns_unicode_strings = String.RETURNS_UNKNOWN
+
+        assert_raises_message(
+            tsa.exc.InvalidRequestError,
+            "RETURNS_UNKNOWN is unsupported in Python 3",
+            eng.connect,
+        )
+
+    @testing.requires.python2
     def test_unicode_test_fails_warning(self):
         class MockCursor(engines.DBAPIProxyCursor):
             def execute(self, stmt, params=None, **kw):
@@ -663,21 +689,9 @@ class ExecuteTest(fixtures.TablesTest):
             eng.connect()
 
         # because plain varchar passed, we don't know the correct answer
-        eq_(eng.dialect.returns_unicode_strings, "conditional")
+        eq_(eng.dialect.returns_unicode_strings, String.RETURNS_CONDITIONAL)
         eng.dispose()
 
-    def test_works_after_dispose(self):
-        eng = create_engine(testing.db.url)
-        for i in range(3):
-            eq_(eng.scalar(select([1])), 1)
-            eng.dispose()
-
-    def test_works_after_dispose_testing_engine(self):
-        eng = engines.testing_engine()
-        for i in range(3):
-            eq_(eng.scalar(select([1])), 1)
-            eng.dispose()
-
 
 class ConvenienceExecuteTest(fixtures.TablesTest):
     __backend__ = True
index f0d0a9b2fba7f09196bdc61793bb9ef5a384f90d..48e324806444e831e33e7ce30dc69cc13f7bc85c 100644 (file)
@@ -950,6 +950,16 @@ class CursorErrTest(fixtures.TestBase):
             url, options=dict(module=dbapi, _initialize=initialize)
         )
         eng.pool.logger = Mock()
+
+        def get_default_schema_name(connection):
+            try:
+                cursor = connection.connection.cursor()
+                connection._cursor_execute(cursor, "statement", {})
+                cursor.close()
+            except exc.DBAPIError:
+                util.warn("Exception attempting to detect")
+
+        eng.dialect._get_default_schema_name = get_default_schema_name
         return eng
 
     def test_cursor_explode(self):
index e0c1359b464fdc2661c235c12f4755677e1ea66e..a0c4ee02208ae742a44f53ca99b202c8c0523adc 100644 (file)
@@ -86,6 +86,7 @@ from sqlalchemy.testing.schema import Table
 from sqlalchemy.testing.util import picklers
 from sqlalchemy.testing.util import round_decimal
 from sqlalchemy.util import OrderedDict
+from sqlalchemy.util import u
 
 
 def _all_dialect_modules():
@@ -763,6 +764,50 @@ class UserDefinedTest(
         eq_(a.dialect_specific_args["bar"], "bar")
 
 
+class StringConvertUnicodeTest(fixtures.TestBase):
+    @testing.combinations((Unicode,), (String,), argnames="datatype")
+    @testing.combinations((True,), (False,), argnames="convert_unicode")
+    @testing.combinations(
+        (String.RETURNS_CONDITIONAL,),
+        (String.RETURNS_BYTES,),
+        (String.RETURNS_UNICODE),
+        argnames="returns_unicode_strings",
+    )
+    def test_convert_unicode(
+        self, datatype, convert_unicode, returns_unicode_strings
+    ):
+        s1 = datatype()
+        dialect = mock.Mock(
+            returns_unicode_strings=returns_unicode_strings,
+            encoding="utf-8",
+            convert_unicode=convert_unicode,
+        )
+
+        proc = s1.result_processor(dialect, None)
+
+        string = u("méil")
+        bytestring = string.encode("utf-8")
+
+        if (
+            datatype is Unicode or convert_unicode
+        ) and returns_unicode_strings in (
+            String.RETURNS_CONDITIONAL,
+            String.RETURNS_BYTES,
+        ):
+            eq_(proc(bytestring), string)
+
+            if returns_unicode_strings is String.RETURNS_CONDITIONAL:
+                eq_(proc(string), string)
+            else:
+                if util.py3k:
+                    # trying to decode a unicode
+                    assert_raises(TypeError, proc, string)
+                else:
+                    assert_raises(UnicodeEncodeError, proc, string)
+        else:
+            is_(proc, None)
+
+
 class TypeCoerceCastTest(fixtures.TablesTest):
     __backend__ = True