From 959d7cbe33ee2e39e2a43b6e7a9c1a750664d569 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 22 Jan 2012 12:11:13 -0500 Subject: [PATCH] - [bug] Improved error messages when a non-string or invalid string is passed to any of the date/time processors used by SQLite, including C and Python versions. [ticket:2382] - changed the import model of processors.py so that we can get at the pure python versions and C versions simultaneously in tests. --- CHANGES | 5 ++ lib/sqlalchemy/cextension/processors.c | 51 ++++++++++-- lib/sqlalchemy/processors.py | 59 ++++++++------ test/engine/test_processors.py | 107 +++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 31 deletions(-) create mode 100644 test/engine/test_processors.py diff --git a/CHANGES b/CHANGES index 37b06a1108..bec9a3252d 100644 --- a/CHANGES +++ b/CHANGES @@ -45,6 +45,11 @@ CHANGES all DBAPIs support this yet, such as psycopg2. [ticket:2371] + - [bug] Improved error messages when a non-string + or invalid string is passed to any of the + date/time processors used by SQLite, including + C and Python versions. [ticket:2382] + - sqlite - [bug] the "name" of an FK constraint in SQLite is reflected as "None", not "0" or other diff --git a/lib/sqlalchemy/cextension/processors.c b/lib/sqlalchemy/cextension/processors.c index 36745c8178..68758afc8c 100644 --- a/lib/sqlalchemy/cextension/processors.c +++ b/lib/sqlalchemy/cextension/processors.c @@ -66,13 +66,21 @@ str_to_datetime(PyObject *self, PyObject *arg) { const char *str; unsigned int year, month, day, hour, minute, second, microsecond = 0; + PyObject *err_repr; if (arg == Py_None) Py_RETURN_NONE; str = PyString_AsString(arg); - if (str == NULL) + if (str == NULL) { + err_repr = PyObject_Repr(arg); + PyErr_Format( + PyExc_ValueError, + "Couldn't parse datetime string '%s' - value is not a string.", + PyString_AsString(err_repr)); + Py_DECREF(err_repr); return NULL; + } /* microseconds are optional */ /* @@ -82,7 +90,12 @@ str_to_datetime(PyObject *self, PyObject *arg) */ if (sscanf(str, "%4u-%2u-%2u %2u:%2u:%2u.%6u", &year, &month, &day, &hour, &minute, &second, µsecond) < 6) { - PyErr_SetString(PyExc_ValueError, "Couldn't parse datetime string."); + err_repr = PyObject_Repr(arg); + PyErr_Format( + PyExc_ValueError, + "Couldn't parse datetime string: %s", + PyString_AsString(err_repr)); + Py_DECREF(err_repr); return NULL; } return PyDateTime_FromDateAndTime(year, month, day, @@ -94,13 +107,21 @@ str_to_time(PyObject *self, PyObject *arg) { const char *str; unsigned int hour, minute, second, microsecond = 0; + PyObject *err_repr; if (arg == Py_None) Py_RETURN_NONE; str = PyString_AsString(arg); - if (str == NULL) + if (str == NULL) { + err_repr = PyObject_Repr(arg); + PyErr_Format( + PyExc_ValueError, + "Couldn't parse time string '%s' - value is not a string.", + PyString_AsString(err_repr)); + Py_DECREF(err_repr); return NULL; + } /* microseconds are optional */ /* @@ -110,7 +131,12 @@ str_to_time(PyObject *self, PyObject *arg) */ if (sscanf(str, "%2u:%2u:%2u.%6u", &hour, &minute, &second, µsecond) < 3) { - PyErr_SetString(PyExc_ValueError, "Couldn't parse time string."); + err_repr = PyObject_Repr(arg); + PyErr_Format( + PyExc_ValueError, + "Couldn't parse time string: %s", + PyString_AsString(err_repr)); + Py_DECREF(err_repr); return NULL; } return PyTime_FromTime(hour, minute, second, microsecond); @@ -121,16 +147,29 @@ str_to_date(PyObject *self, PyObject *arg) { const char *str; unsigned int year, month, day; + PyObject *err_repr; if (arg == Py_None) Py_RETURN_NONE; str = PyString_AsString(arg); - if (str == NULL) + if (str == NULL) { + err_repr = PyObject_Repr(arg); + PyErr_Format( + PyExc_ValueError, + "Couldn't parse date string '%s' - value is not a string.", + PyString_AsString(err_repr)); + Py_DECREF(err_repr); return NULL; + } if (sscanf(str, "%4u-%2u-%2u", &year, &month, &day) != 3) { - PyErr_SetString(PyExc_ValueError, "Couldn't parse date string."); + err_repr = PyObject_Repr(arg); + PyErr_Format( + PyExc_ValueError, + "Couldn't parse date string: %s", + PyString_AsString(err_repr)); + Py_DECREF(err_repr); return NULL; } return PyDate_FromDate(year, month, day); diff --git a/lib/sqlalchemy/processors.py b/lib/sqlalchemy/processors.py index dd789a44eb..c4bac28347 100644 --- a/lib/sqlalchemy/processors.py +++ b/lib/sqlalchemy/processors.py @@ -24,9 +24,14 @@ def str_to_datetime_processor_factory(regexp, type_): if value is None: return None else: - m = rmatch(value) + try: + m = rmatch(value) + except TypeError: + raise ValueError("Couldn't parse %s string '%r' " + "- value is not a string." % (type_.__name__ , value)) if m is None: - raise ValueError("Couldn't parse %s string." % type_.__name__) + raise ValueError("Couldn't parse %s string: " + "'%s'" % (type_.__name__ , value)) return type_(*map(int, m.groups(0))) return process @@ -36,29 +41,7 @@ def boolean_to_int(value): else: return int(value) -try: - from sqlalchemy.cprocessors import UnicodeResultProcessor, \ - DecimalResultProcessor, \ - to_float, to_str, int_to_boolean, \ - str_to_datetime, str_to_time, \ - str_to_date - - def to_unicode_processor_factory(encoding, errors=None): - # this is cumbersome but it would be even more so on the C side - if errors is not None: - return UnicodeResultProcessor(encoding, errors).process - else: - return UnicodeResultProcessor(encoding).process - - def to_decimal_processor_factory(target_class, scale=10): - # Note that the scale argument is not taken into account for integer - # values in the C implementation while it is in the Python one. - # For example, the Python implementation might return - # Decimal('5.00000') whereas the C implementation will - # return Decimal('5'). These are equivalent of course. - return DecimalResultProcessor(target_class, "%%.%df" % scale).process - -except ImportError: +def py_fallback(): def to_unicode_processor_factory(encoding, errors=None): decoder = codecs.getdecoder(encoding) @@ -109,4 +92,30 @@ except ImportError: datetime.datetime) str_to_time = str_to_datetime_processor_factory(TIME_RE, datetime.time) str_to_date = str_to_datetime_processor_factory(DATE_RE, datetime.date) + return locals() + +try: + from sqlalchemy.cprocessors import UnicodeResultProcessor, \ + DecimalResultProcessor, \ + to_float, to_str, int_to_boolean, \ + str_to_datetime, str_to_time, \ + str_to_date + + def to_unicode_processor_factory(encoding, errors=None): + # this is cumbersome but it would be even more so on the C side + if errors is not None: + return UnicodeResultProcessor(encoding, errors).process + else: + return UnicodeResultProcessor(encoding).process + + def to_decimal_processor_factory(target_class, scale=10): + # Note that the scale argument is not taken into account for integer + # values in the C implementation while it is in the Python one. + # For example, the Python implementation might return + # Decimal('5.00000') whereas the C implementation will + # return Decimal('5'). These are equivalent of course. + return DecimalResultProcessor(target_class, "%%.%df" % scale).process + +except ImportError: + globals().update(py_fallback()) diff --git a/test/engine/test_processors.py b/test/engine/test_processors.py new file mode 100644 index 0000000000..48c0fb8f19 --- /dev/null +++ b/test/engine/test_processors.py @@ -0,0 +1,107 @@ +from test.lib.testing import eq_, assert_raises, assert_raises_message +from test.lib import testing, fixtures + +try: + from sqlalchemy.cprocessors import str_to_datetime as c_str_to_datetime, \ + str_to_date as c_str_to_date, \ + str_to_time as c_str_to_time + from sqlalchemy.processors import py_fallback + for key, value in py_fallback().items(): + globals()["py_%s" % key] = value +except: + from sqlalchemy.processors import str_to_datetime as py_str_to_datetime, \ + str_to_date as py_str_to_date, \ + str_to_time as py_str_to_time + +class DateProcessorTest(fixtures.TestBase): + @testing.requires.cextensions + def test_c_date_no_string(self): + assert_raises_message( + ValueError, + "Couldn't parse date string '2012' - value is not a string", + c_str_to_date, 2012 + ) + + def test_py_date_no_string(self): + assert_raises_message( + ValueError, + "Couldn't parse date string '2012' - value is not a string", + py_str_to_date, 2012 + ) + + @testing.requires.cextensions + def test_c_datetime_no_string(self): + assert_raises_message( + ValueError, + "Couldn't parse datetime string '2012' - value is not a string", + c_str_to_datetime, 2012 + ) + + def test_py_datetime_no_string(self): + assert_raises_message( + ValueError, + "Couldn't parse datetime string '2012' - value is not a string", + py_str_to_datetime, 2012 + ) + + @testing.requires.cextensions + def test_c_time_no_string(self): + assert_raises_message( + ValueError, + "Couldn't parse time string '2012' - value is not a string", + c_str_to_time, 2012 + ) + + def test_py_time_no_string(self): + assert_raises_message( + ValueError, + "Couldn't parse time string '2012' - value is not a string", + py_str_to_time, 2012 + ) + + @testing.requires.cextensions + def test_c_date_invalid_string(self): + assert_raises_message( + ValueError, + "Couldn't parse date string: '5:a'", + c_str_to_date, "5:a" + ) + + def test_py_date_invalid_string(self): + assert_raises_message( + ValueError, + "Couldn't parse date string: '5:a'", + py_str_to_date, "5:a" + ) + + @testing.requires.cextensions + def test_c_datetime_invalid_string(self): + assert_raises_message( + ValueError, + "Couldn't parse datetime string: '5:a'", + c_str_to_datetime, "5:a" + ) + + def test_py_datetime_invalid_string(self): + assert_raises_message( + ValueError, + "Couldn't parse datetime string: '5:a'", + py_str_to_datetime, "5:a" + ) + + @testing.requires.cextensions + def test_c_time_invalid_string(self): + assert_raises_message( + ValueError, + "Couldn't parse time string: '5:a'", + c_str_to_time, "5:a" + ) + + def test_py_time_invalid_string(self): + assert_raises_message( + ValueError, + "Couldn't parse time string: '5:a'", + py_str_to_time, "5:a" + ) + + -- 2.47.2