Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dict() method to ctds.Row #79

Merged
merged 1 commit into from
Nov 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Offical support for Python 3.9.
- Expose `ctds.Row` type so that it can be used as a type annotation.
- Add `description` attribute to rowlists and rows.
- Add `ctds.Row.dict()` method.

### Fixed
- Cache `cursor.description` rather than creating a new instance every time.
Expand Down
21 changes: 15 additions & 6 deletions doc/results.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ therefore supports indexing. For example,

Reading Columns
^^^^^^^^^^^^^^^
*cTDS* rows support referencing column values multiple ways: index, attribute
or mapping.
*cTDS* rows support referencing column values multiple ways: you can index
a row by either a column number or a column name, use a column name as an
attribute of the row, or build a dictionary mapping column names to values.

.. code-block:: python

Expand All @@ -76,23 +77,31 @@ or mapping.
cursor.execute(
'''
SELECT
1 AS Column1,
'2' AS Column2,
'unnamed',
2 AS Column2,
'Three' AS Column3
'''
)
rows = cursor.fetchall()

for row in rows:
# index
assert row[1] == '2'
assert row[0] == 'unnamed'

# attribute
assert row.Column1 == 1
assert row.Column2 == 2

# mapping
assert row['Column3'] == 'Three'

# dict - note that the column number is used as the key
# for any unnamed columns
assert row.dict() == {
0: 'unnamed',
'Column1': 1,
'Column2': '2',
'Column3': 'Three',
}

Advancing the Result Set
------------------------
Expand Down
74 changes: 73 additions & 1 deletion src/ctds/cursor.c
Original file line number Diff line number Diff line change
Expand Up @@ -387,13 +387,15 @@ static int Description_init(void)
# endif /* if PY_VERSION_HEX >= 0x03040000 */

# define Description_New() PyStructSequence_New(&DescriptionType)
# define Description_GET_ITEM PyStructSequence_GET_ITEM
# define Description_SET_ITEM PyStructSequence_SET_ITEM

}
#else /* if PY_MAJOR_VERSION < 3 */

# define Description_init() 0
# define Description_New() PyTuple_New(7)
# define Description_GET_ITEM PyTuple_GET_ITEM
# define Description_SET_ITEM PyTuple_SET_ITEM

#endif /* else if PY_MAJOR_VERSION >= 3 */
Expand Down Expand Up @@ -2858,6 +2860,76 @@ static PyGetSetDef Row_getset[] = {
{ NULL, NULL, NULL, NULL, NULL }
};

static const char s_Row_dict_doc[] =
"dict()\n"
"\n"
"Get a dict representing the contents of the row. The keys will be column\n"
"names for columns that are named, and integer column numbers for those that\n"
"do not.\n"
"\n"
":return: A dict representation of the row.\n";

static PyObject* Row_dict(PyObject* self, PyObject* args)
{
struct Row* row = (struct Row*)self;
PyObject* dict = PyDict_New();

if (dict)
{
PyObject* description = ResultSetDescription_get_object(row->description);
if (description)
{
size_t ncolumns = row->description->ncolumns;
size_t ix;
assert((Py_ssize_t)ncolumns == PyTuple_GET_SIZE(description));
for (ix = 0; ix < ncolumns; ++ix)
{
PyObject* value = row->values[ix];
PyObject* colname = Description_GET_ITEM(PyTuple_GET_ITEM(description, ix), 0);
if (0 == PyObject_IsTrue(colname))
{
/* Use the column number as the key for unnamed columns. */
PyObject* colnum = PyLong_FromSize_t(ix);
if (!colnum)
{
break;
}
if (0 != PyDict_SetItem(dict, colnum, value))
{
Py_DECREF(colnum);
break;
}
Py_DECREF(colnum);
}
else
{
/* Use the column name as the key. */
if (0 != PyDict_SetItem(dict, colname, value))
{
break;
}
}
}
Py_DECREF(description);
}
}

if (PyErr_Occurred())
{
Py_XDECREF(dict);
dict = NULL;
}
return dict;

UNUSED(args);
}

static PyMethodDef Row_methods[] = {
/* ml_name, ml_meth, ml_flags, ml_doc */
{ "dict", Row_dict, METH_NOARGS, s_Row_dict_doc },
{ NULL, NULL, 0, NULL }
};

PyTypeObject RowType = {
PyVarObject_HEAD_INIT(NULL, 0)
"ctds.Row", /* tp_name */
Expand Down Expand Up @@ -2890,7 +2962,7 @@ PyTypeObject RowType = {
0, /* tp_weaklistoffset */
NULL, /* tp_iter */
NULL, /* tp_iternext */
NULL, /* tp_methods */
Row_methods, /* tp_methods */
NULL, /* tp_members */
Row_getset, /* tp_getset */
NULL, /* tp_base */
Expand Down
28 changes: 28 additions & 0 deletions tests/test_cursor_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,31 @@ def test_description(self):
row = cursor.fetchone()

self.assertTrue(row.description is description)

def test_dict(self):
with self.connect() as connection:
with connection.cursor() as cursor:
args = (1, 'two', 'three', 4)
cursor.execute(
'''
SELECT
:0 AS Col1,
'unnamed',
:1 AS Col2,
:2 AS Col3,
'another unnamed',
:3 AS Col4
''',
args
)
rows = cursor.fetchall()

row = rows[0]
self.assertEqual(row.dict(), {
'Col1': 1,
1: 'unnamed',
'Col2': 'two',
'Col3': 'three',
4: 'another unnamed',
'Col4': 4,
})