Skip to content

Commit 43e85ac

Browse files
authored
Merge pull request datastax#951 from datastax/python-new-row_factory
PYTHON-893: fallback implementation for namedtuple row factory
2 parents c45743a + 16ae36b commit 43e85ac

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Bug Fixes
44
---------
55
* Tokenmap.get_replicas returns the wrong value if token coincides with the end of the range (PYTHON-978)
6+
* Python Driver fails with "more than 255 arguments" python exception when > 255 columns specified in query response (PYTHON-893)
67

78
Other
89
-----

cassandra/query.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import time
2626
import six
2727
from six.moves import range, zip
28+
import warnings
2829

2930
from cassandra import ConsistencyLevel, OperationTimedOut
3031
from cassandra.util import unix_time_from_uuid1
@@ -83,6 +84,39 @@ def tuple_factory(colnames, rows):
8384
"""
8485
return rows
8586

87+
class PseudoNamedTupleRow(object):
88+
"""
89+
Helper class for pseudo_named_tuple_factory. These objects provide an
90+
__iter__ interface, as well as index- and attribute-based access to values,
91+
but otherwise do not attempt to implement the full namedtuple or iterable
92+
interface.
93+
"""
94+
def __init__(self, ordered_dict):
95+
self._dict = ordered_dict
96+
self._tuple = tuple(ordered_dict.values())
97+
98+
def __getattr__(self, name):
99+
return self._dict[name]
100+
101+
def __getitem__(self, idx):
102+
return self._tuple[idx]
103+
104+
def __iter__(self):
105+
return iter(self._tuple)
106+
107+
def __repr__(self):
108+
return '{t}({od})'.format(t=self.__class__.__name__,
109+
od=self._dict)
110+
111+
112+
def pseudo_namedtuple_factory(colnames, rows):
113+
"""
114+
Returns each row as a :class:`.PseudoNamedTupleRow`. This is the fallback
115+
factory for cases where :meth:`.named_tuple_factory` fails to create rows.
116+
"""
117+
return [PseudoNamedTupleRow(od)
118+
for od in ordered_dict_factory(colnames, rows)]
119+
86120

87121
def named_tuple_factory(colnames, rows):
88122
"""
@@ -116,6 +150,20 @@ def named_tuple_factory(colnames, rows):
116150
clean_column_names = map(_clean_column_name, colnames)
117151
try:
118152
Row = namedtuple('Row', clean_column_names)
153+
except SyntaxError:
154+
warnings.warn(
155+
"Failed creating namedtuple for a result because there were too "
156+
"many columns. This is due to a Python limitation that affects "
157+
"namedtuple in Python 3.0-3.6 (see issue18896). The row will be "
158+
"created with {substitute_factory_name}, which lacks some namedtuple "
159+
"features and is slower. To avoid slower performance accessing "
160+
"values on row objects, Upgrade to Python 3.7, or use a different "
161+
"row factory. (column names: {colnames})".format(
162+
substitute_factory_name=pseudo_namedtuple_factory.__name__,
163+
colnames=colnames
164+
)
165+
)
166+
return pseudo_namedtuple_factory(colnames, rows)
119167
except Exception:
120168
clean_column_names = list(map(_clean_column_name, colnames)) # create list because py3 map object will be consumed by first attempt
121169
log.warning("Failed creating named tuple for results with column names %s (cleaned: %s) "

tests/unit/test_row_factories.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright DataStax, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
from cassandra.query import named_tuple_factory
17+
18+
import logging
19+
import warnings
20+
21+
import sys
22+
23+
try:
24+
from unittest import TestCase
25+
except ImportError:
26+
from unittest2 import TestCase
27+
28+
29+
log = logging.getLogger(__name__)
30+
31+
32+
NAMEDTUPLE_CREATION_BUG = sys.version_info >= (3,) and sys.version_info < (3, 7)
33+
34+
class TestNamedTupleFactory(TestCase):
35+
36+
long_colnames, long_rows = (
37+
['col{}'.format(x) for x in range(300)],
38+
[
39+
['value{}'.format(x) for x in range(300)]
40+
for _ in range(100)
41+
]
42+
)
43+
short_colnames, short_rows = (
44+
['col{}'.format(x) for x in range(200)],
45+
[
46+
['value{}'.format(x) for x in range(200)]
47+
for _ in range(100)
48+
]
49+
)
50+
51+
def test_creation_warning_on_long_column_list(self):
52+
"""
53+
Reproduces the failure described in PYTHON-893
54+
55+
@since 3.15
56+
@jira_ticket PYTHON-893
57+
@expected_result creation fails on Python > 3 and < 3.7
58+
59+
@test_category row_factory
60+
"""
61+
if not NAMEDTUPLE_CREATION_BUG:
62+
named_tuple_factory(self.long_colnames, self.long_rows)
63+
return
64+
65+
with warnings.catch_warnings(record=True) as w:
66+
rows = named_tuple_factory(self.long_colnames, self.long_rows)
67+
self.assertEqual(len(w), 1)
68+
warning = w[0]
69+
self.assertIn('pseudo_namedtuple_factory', str(warning))
70+
self.assertIn('3.7', str(warning))
71+
72+
for r in rows:
73+
self.assertEqual(r.col0, self.long_rows[0][0])
74+
75+
def test_creation_no_warning_on_short_column_list(self):
76+
"""
77+
Tests that normal namedtuple row creation still works after PYTHON-893 fix
78+
79+
@since 3.15
80+
@jira_ticket PYTHON-893
81+
@expected_result creates namedtuple-based Rows
82+
83+
@test_category row_factory
84+
"""
85+
with warnings.catch_warnings(record=True) as w:
86+
rows = named_tuple_factory(self.short_colnames, self.short_rows)
87+
self.assertEqual(len(w), 0)
88+
# check that this is a real namedtuple
89+
self.assertTrue(hasattr(rows[0], '_fields'))
90+
self.assertIsInstance(rows[0], tuple)

0 commit comments

Comments
 (0)