Hello community,

here is the log from the commit of package python-pyodbc for openSUSE:Factory 
checked in at 2019-03-11 11:17:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pyodbc (Old)
 and      /work/SRC/openSUSE:Factory/.python-pyodbc.new.28833 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pyodbc"

Mon Mar 11 11:17:53 2019 rev:4 rq:683721 version:4.0.26

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pyodbc/python-pyodbc.changes      
2019-02-02 21:50:03.539938732 +0100
+++ /work/SRC/openSUSE:Factory/.python-pyodbc.new.28833/python-pyodbc.changes   
2019-03-11 11:17:57.397284479 +0100
@@ -1,0 +2,9 @@
+Sun Mar 10 16:04:24 UTC 2019 - Dirk Hartmann <[email protected]>
+
+- Update to version 4.0.26:
+  * Issue #506 uncovered a potentially serious error where 
+    Unicode strings may not get a NULL terminator when being converted.
+  * Issue #504 was a double decref in the error return path of executemany.
+  
+
+-------------------------------------------------------------------

Old:
----
  pyodbc-4.0.25.tar.gz

New:
----
  pyodbc-4.0.26.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pyodbc.spec ++++++
--- /var/tmp/diff_new_pack.dhwPlY/_old  2019-03-11 11:17:58.673284019 +0100
+++ /var/tmp/diff_new_pack.dhwPlY/_new  2019-03-11 11:17:58.673284019 +0100
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-pyodbc
-Version:        4.0.25
+Version:        4.0.26
 Release:        0
 Summary:        Python ODBC API
 License:        MIT

++++++ pyodbc-4.0.25.tar.gz -> pyodbc-4.0.26.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyodbc-4.0.25/PKG-INFO new/pyodbc-4.0.26/PKG-INFO
--- old/pyodbc-4.0.25/PKG-INFO  2018-12-14 03:56:00.000000000 +0100
+++ new/pyodbc-4.0.26/PKG-INFO  2019-02-23 20:17:56.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: pyodbc
-Version: 4.0.25
+Version: 4.0.26
 Summary: DB API Module for ODBC
 Home-page: https://github.com/mkleehammer/pyodbc
 Author: Michael Kleehammer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyodbc-4.0.25/pyodbc.egg-info/PKG-INFO 
new/pyodbc-4.0.26/pyodbc.egg-info/PKG-INFO
--- old/pyodbc-4.0.25/pyodbc.egg-info/PKG-INFO  2018-12-14 03:55:58.000000000 
+0100
+++ new/pyodbc-4.0.26/pyodbc.egg-info/PKG-INFO  2019-02-23 20:17:55.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: pyodbc
-Version: 4.0.25
+Version: 4.0.26
 Summary: DB API Module for ODBC
 Home-page: https://github.com/mkleehammer/pyodbc
 Author: Michael Kleehammer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyodbc-4.0.25/src/connection.cpp 
new/pyodbc-4.0.26/src/connection.cpp
--- old/pyodbc-4.0.25/src/connection.cpp        2018-09-28 20:47:41.000000000 
+0200
+++ new/pyodbc-4.0.26/src/connection.cpp        2019-02-23 19:31:35.000000000 
+0100
@@ -1259,6 +1259,45 @@
     Py_RETURN_NONE;
 }
 
+static char conv_get_doc[] =
+    "get_output_converter(sqltype) --> <class 'function'>\n"
+    "\n"
+    "Get the output converter function that was registered with\n"
+    "add_output_converter.  It is safe to call if no converter is\n"
+    "registered for the type (returns None).\n"
+    "\n"
+    "sqltype\n"
+    "  The integer SQL type value being converted, which can be one of the 
defined\n"
+    "  standard constants (e.g. pyodbc.SQL_VARCHAR) or a database-specific 
value\n"
+    "  (e.g. -151 for the SQL Server 2008 geometry data type).\n"
+    ;
+
+static PyObject* _get_converter(PyObject* self, SQLSMALLINT sqltype)
+{
+    Connection* cnxn = (Connection*)self;
+
+    if (cnxn->conv_count)
+    {
+        for (int i = 0; i < cnxn->conv_count; i++)
+        {
+            if (cnxn->conv_types[i] == sqltype)
+            {
+                return cnxn->conv_funcs[i];
+            }
+        }
+    }
+    Py_RETURN_NONE;
+}
+
+static PyObject* Connection_conv_get(PyObject* self, PyObject* args)
+{
+    int sqltype;
+    if (!PyArg_ParseTuple(args, "i", &sqltype))
+        return 0;
+
+    return _get_converter(self, (SQLSMALLINT)sqltype);
+}
+
 static void NormalizeCodecName(const char* src, char* dest, size_t cbDest)
 {
     // Copies the codec name to dest, lowercasing it and replacing underscores 
with dashes.
@@ -1564,6 +1603,7 @@
     { "getinfo",                 Connection_getinfo,         METH_VARARGS, 
getinfo_doc    },
     { "add_output_converter",    Connection_conv_add,        METH_VARARGS, 
conv_add_doc   },
     { "remove_output_converter", Connection_conv_remove,     METH_VARARGS, 
conv_remove_doc },
+    { "get_output_converter",    Connection_conv_get,        METH_VARARGS, 
conv_get_doc },
     { "clear_output_converters", Connection_conv_clear,      METH_NOARGS,  
conv_clear_doc },
     { "setdecoding",             (PyCFunction)Connection_setdecoding,     
METH_VARARGS|METH_KEYWORDS, setdecoding_doc },
     { "setencoding",             (PyCFunction)Connection_setencoding,     
METH_VARARGS|METH_KEYWORDS, 0 },
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyodbc-4.0.25/src/params.cpp 
new/pyodbc-4.0.26/src/params.cpp
--- old/pyodbc-4.0.25/src/params.cpp    2018-12-14 03:52:58.000000000 +0100
+++ new/pyodbc-4.0.26/src/params.cpp    2019-02-23 19:31:35.000000000 +0100
@@ -1813,12 +1813,14 @@
                     // "schema change" or conversion error. Try again on next 
batch.
                     rowptr--;
                     Py_XDECREF(colseq);
+                    colseq = 0;
                     // Finish this batch of rows and attempt to execute before 
starting another.
                     goto DoExecute;
                 }
             }
             rows_converted++;
             Py_XDECREF(colseq);
+            colseq = 0;
             r++;
             if ( r >= rowcount )
             {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyodbc-4.0.25/src/textenc.cpp 
new/pyodbc-4.0.26/src/textenc.cpp
--- old/pyodbc-4.0.25/src/textenc.cpp   2017-10-20 21:32:12.000000000 +0200
+++ new/pyodbc-4.0.26/src/textenc.cpp   2019-02-23 19:31:35.000000000 +0100
@@ -3,6 +3,66 @@
 #include "wrapper.h"
 #include "textenc.h"
 
+
+static PyObject* nulls = PyBytes_FromStringAndSize("\0\0\0\0", 4);
+
+
+void SQLWChar::init(PyObject* src, const TextEnc& enc)
+{
+    // Initialization code common to all of the constructors.
+
+    if (src == 0 || src == Py_None)
+    {
+        psz = 0;
+        isNone = true;
+        return;
+    }
+
+    isNone = false;
+
+    // If there are optimized encodings that don't require a temporary object, 
use them.
+#if PY_MAJOR_VERSION < 3
+    if (enc.optenc == OPTENC_RAW && PyString_Check(src))
+    {
+        psz = (SQLWCHAR*)PyString_AS_STRING(src);
+        return;
+    }
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+    if (enc.optenc == OPTENC_UTF8 && PyUnicode_Check(src))
+    {
+        psz = (SQLWCHAR*)PyUnicode_AsUTF8(src);
+        return;
+    }
+#endif
+
+    PyObject* pb = PyUnicode_AsEncodedString(src, enc.name, "strict");
+    if (pb)
+    {
+        // Careful: Some encodings don't return bytes.
+
+        if (!PyBytes_Check(pb))
+        {
+            // REVIEW: Error or just return null?
+            psz = 0;
+            Py_DECREF(pb);
+            return;
+        }
+
+        PyBytes_Concat(&pb, nulls);
+        if (!pb)
+        {
+            psz = 0;
+            return;
+        }
+
+        psz = (SQLWCHAR*)PyBytes_AS_STRING(pb);
+        bytes.Attach(pb);
+    }
+}
+
+
 PyObject* TextEnc::Encode(PyObject* obj) const
 {
 #if PY_MAJOR_VERSION < 3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyodbc-4.0.25/src/textenc.h 
new/pyodbc-4.0.26/src/textenc.h
--- old/pyodbc-4.0.25/src/textenc.h     2018-09-28 20:42:45.000000000 +0200
+++ new/pyodbc-4.0.26/src/textenc.h     2019-02-23 19:31:35.000000000 +0100
@@ -83,8 +83,20 @@
     //
     // Note: This does *not* increment the refcount!
 
+    // IMPORTANT: I've made the conscious decision *not* to determine the 
character count.  If
+    // we only had to follow the ODBC specification, it would simply be the 
number of
+    // characters in the string and would be the bytelen / 2.  The problem is 
drivers that
+    // don't follow the specification and expect things like UTF-8.  What 
length do these
+    // drivers expect?  Very, very likely they want the number of *bytes*, not 
the actual
+    // number of characters.  I'm simply going to null terminate and pass 
SQL_NTS.
+    //
+    // This is a performance penalty when using utf16 since we have to copy 
the string just to
+    // add the null terminator bytes, but we don't use it very often.  If this 
becomes a
+    // bottleneck, we'll have to revisit this design.
+
     SQLWCHAR* psz;
     bool isNone;
+
     Object bytes;
     // A temporary object holding the decoded bytes if we can't use a pointer 
into the original
     // object.
@@ -108,42 +120,6 @@
         init(src, enc);
     }
 
-    void init(PyObject* src, const TextEnc& enc)
-    {
-        if (src == 0 || src == Py_None)
-        {
-            psz = 0;
-            isNone = true;
-            return;
-        }
-
-        isNone = false;
-
-        // If there are optimized encodings that don't require a temporary 
object, use them.
-        #if PY_MAJOR_VERSION < 3
-        if (enc.optenc == OPTENC_RAW && PyString_Check(src))
-        {
-            psz = (SQLWCHAR*)PyString_AS_STRING(src);
-            return;
-        }
-        #endif
-
-        #if PY_MAJOR_VERSION >= 3
-        if (enc.optenc == OPTENC_UTF8 && PyUnicode_Check(src))
-        {
-            psz = (SQLWCHAR*)PyUnicode_AsUTF8(src);
-            return;
-        }
-        #endif
-
-        bytes.Attach(PyUnicode_AsEncodedString(src, enc.name, "strict"));
-        if (bytes)
-        {
-            // Careful: Some encodings don't return bytes.  Don't use 
AS_STRING macro.
-            psz = (SQLWCHAR*)PyBytes_AsString(bytes.Get());
-        }
-    }
-
     bool isValidOrNone()
     {
         // Returns true if this object is a valid string *or* None.
@@ -156,6 +132,8 @@
     }
 
 private:
+    void init(PyObject* src, const TextEnc& enc);
+
     SQLWChar(const SQLWChar&) {}
     void operator=(const SQLWChar&) {}
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyodbc-4.0.25/tests2/sqlservertests.py 
new/pyodbc-4.0.26/tests2/sqlservertests.py
--- old/pyodbc-4.0.25/tests2/sqlservertests.py  2018-12-14 03:52:58.000000000 
+0100
+++ new/pyodbc-4.0.26/tests2/sqlservertests.py  2019-02-23 19:31:35.000000000 
+0100
@@ -36,6 +36,12 @@
 from warnings import warn
 from testutils import *
 
+# Some tests have fallback code for known driver issues.
+# Change this value to False to bypass the fallback code, e.g., to see
+#   if a newer version of the driver has fixed the underlying issue.
+#
+handle_known_issues = True
+
 _TESTSTR = '0123456789-abcdefghijklmnopqrstuvwxyz-'
 
 def _generate_test_string(length):
@@ -87,6 +93,54 @@
         elif type_name == 'freetds':
             return ('tdsodbc' in driver_name)
 
+    def handle_known_issues_for(self, type_name, print_reminder=False):
+        """
+        Checks driver `type_name` and "killswitch" variable 
`handle_known_issues` to see if
+        known issue handling should be bypassed. Optionally prints a reminder 
message to
+        help identify tests that previously had issues but may have been fixed 
by a newer
+        version of the driver.
+
+        Usage examples:
+
+        # 1. print reminder at beginning of test (before any errors can occur)
+        #
+        def test_some_feature(self):
+            self.handle_known_issues_for('freetds', print_reminder=True)
+            # (continue with test code)
+
+        # 2. conditional execution of fallback code
+        #
+        try:
+            # (some test code)
+        except pyodbc.DataError:
+            if self.handle_known_issues_for('freetds'):
+                # FREETDS_KNOWN_ISSUE
+                #
+                # (fallback code to work around exception)
+            else:
+                raise
+        """
+        if self.driver_type_is(type_name):
+            if handle_known_issues:
+                return True
+            else:
+                if print_reminder:
+                    print("Known issue handling is disabled. Does this test 
still fail?")
+        return False
+
+    def driver_type_is(self, type_name):
+        recognized_types = {
+            'msodbcsql': '(Microsoft) ODBC Driver xx for SQL Server',
+            'freetds': 'FreeTDS ODBC',
+        }
+        if not type_name in recognized_types.keys():
+            raise KeyError('"{0}" is not a recognized driver type: 
{1}'.format(type_name, list(recognized_types.keys())))
+        driver_name = self.cnxn.getinfo(pyodbc.SQL_DRIVER_NAME).lower()
+        if type_name == 'msodbcsql':
+            return ('msodbcsql' in driver_name) or ('sqlncli' in driver_name) 
or ('sqlsrv32.dll' == driver_name)
+        elif type_name == 'freetds':
+            return ('tdsodbc' in driver_name)
+
     def get_sqlserver_version(self):
         """
         Returns the major version: 8-->2000, 9-->2005, 10-->2008
@@ -1651,6 +1705,10 @@
         # pyodbc supports queries with table valued parameters in sql server
         #
 
+        if self.handle_known_issues_for('freetds', print_reminder=True):
+            warn('FREETDS_KNOWN_ISSUE - test_tvp: test cancelled.')
+            return
+
         # (Don't use "if exists" since older SQL Servers don't support it.)
         try:
             self.cursor.execute("drop procedure SelectTVP")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyodbc-4.0.25/tests3/sqlservertests.py 
new/pyodbc-4.0.26/tests3/sqlservertests.py
--- old/pyodbc-4.0.25/tests3/sqlservertests.py  2018-12-14 03:52:58.000000000 
+0100
+++ new/pyodbc-4.0.26/tests3/sqlservertests.py  2019-02-23 19:55:23.000000000 
+0100
@@ -1346,15 +1346,19 @@
 
 
     def test_output_conversion(self):
-        def convert(value):
+        def convert1(value):
             # The value is the raw bytes (as a bytes object) read from the
             # database.  We'll simply add an X at the beginning at the end.
             return 'X' + value.decode('latin1') + 'X'
 
+        def convert2(value):
+            # Same as above, but add a Y at the beginning at the end.
+            return 'Y' + value.decode('latin1') + 'Y'
+
         self.cursor.execute("create table t1(n int, v varchar(10))")
         self.cursor.execute("insert into t1 values (1, '123.45')")
 
-        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert)
+        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert1)
         value = self.cursor.execute("select v from t1").fetchone()[0]
         self.assertEqual(value, 'X123.45X')
 
@@ -1364,7 +1368,7 @@
         self.assertEqual(value, '123.45')
 
         # Same but clear using remove_output_converter.
-        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert)
+        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert1)
         value = self.cursor.execute("select v from t1").fetchone()[0]
         self.assertEqual(value, 'X123.45X')
 
@@ -1372,15 +1376,44 @@
         value = self.cursor.execute("select v from t1").fetchone()[0]
         self.assertEqual(value, '123.45')
 
-        # And lastly, clear by passing None for the converter.
-        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert)
+        # Clear via add_output_converter, passing None for the converter 
function.
+        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert1)
         value = self.cursor.execute("select v from t1").fetchone()[0]
         self.assertEqual(value, 'X123.45X')
 
         self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, None)
         value = self.cursor.execute("select v from t1").fetchone()[0]
         self.assertEqual(value, '123.45')
-
+        
+        # retrieve and temporarily replace converter (get_output_converter)
+        #
+        #   case_1: converter already registered
+        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert1)
+        value = self.cursor.execute("select v from t1").fetchone()[0]
+        self.assertEqual(value, 'X123.45X')
+        prev_converter = self.cnxn.get_output_converter(pyodbc.SQL_VARCHAR)
+        self.assertNotEqual(prev_converter, None)
+        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert2)
+        value = self.cursor.execute("select v from t1").fetchone()[0]
+        self.assertEqual(value, 'Y123.45Y')
+        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, prev_converter)
+        value = self.cursor.execute("select v from t1").fetchone()[0]
+        self.assertEqual(value, 'X123.45X')
+        #
+        #   case_2: no converter already registered
+        self.cnxn.clear_output_converters()
+        value = self.cursor.execute("select v from t1").fetchone()[0]
+        self.assertEqual(value, '123.45')
+        prev_converter = self.cnxn.get_output_converter(pyodbc.SQL_VARCHAR)
+        self.assertEqual(prev_converter, None)
+        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert2)
+        value = self.cursor.execute("select v from t1").fetchone()[0]
+        self.assertEqual(value, 'Y123.45Y')
+        self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, prev_converter)
+        value = self.cursor.execute("select v from t1").fetchone()[0]
+        self.assertEqual(value, '123.45')
+        
+        
     def test_too_large(self):
         """Ensure error raised if insert fails due to truncation"""
         value = 'x' * 1000
@@ -1550,6 +1583,26 @@
         assert row.type_name == 'varchar'
         assert row.column_size == 4, row.column_size
 
+        # <test null termination fix (issue #506)>
+        for i in range(8, 16):
+            table_name = 'pyodbc_89abcdef'[:i]
+
+            self.cursor.execute("""\
+            BEGIN TRY
+                DROP TABLE {0};
+            END TRY
+            BEGIN CATCH
+            END CATCH
+            CREATE TABLE {0} (id INT PRIMARY KEY);
+            """.format(table_name))
+
+            col_count = len([col.column_name for col in 
self.cursor.columns(table_name)])
+            # print('table [{}] ({} characters): {} 
columns{}'.format(table_name, i, col_count, ' <-' if col_count == 0 else ''))
+            self.assertEqual(col_count, 1)
+
+            self.cursor.execute("DROP TABLE {};".format(table_name))
+        # </test null termination fix (issue #506)>
+
     def test_cancel(self):
         # I'm not sure how to reliably cause a hang to cancel, so for now 
we'll settle with
         # making sure SQLCancel is called correctly.
@@ -1578,6 +1631,10 @@
         # pyodbc supports queries with table valued parameters in sql server
         #
 
+        if self.handle_known_issues_for('freetds', print_reminder=True):
+            warn('FREETDS_KNOWN_ISSUE - test_tvp: test cancelled.')
+            return
+
         # (Don't use "if exists" since older SQL Servers don't support it.)
         try:
             self.cursor.execute("drop procedure SelectTVP")


Reply via email to