Skip to content

Commit 800adfc

Browse files
authored
Merge pull request #153 from bluedynamics/fix/handle-keyword-non-iterable-value
fix(query): _handle_keyword coerces non-iterable values to single-element list (#152)
2 parents bae7c17 + c2857be commit 800adfc

3 files changed

Lines changed: 61 additions & 1 deletion

File tree

CHANGES.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## 1.0.0b62
4+
5+
### Fixed
6+
7+
- ``_handle_keyword`` no longer crashes with ``TypeError: 'DateTime'
8+
object is not iterable`` when a caller passes a non-str, non-iterable
9+
value (e.g. a Zope ``DateTime`` or a Python ``datetime``) as the
10+
query for a KeywordIndex. The old coercion assumed the value was
11+
either a ``str`` or iterable; anything else (``DateTime``, ``int``,
12+
…) hit ``list(value)`` and raised. Values are now coerced via
13+
``str()`` into a single-element list, matching the JSONB storage
14+
shape (keyword arrays always contain strings). Closes #152.
15+
316
## 1.0.0b61
417

518
### Fixed

src/plone/pgcatalog/query.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,16 @@ def _handle_keyword(self, name, idx_key, spec):
365365

366366
operator = spec.get("operator", "or")
367367

368-
query_val = [query_val] if isinstance(query_val, str) else list(query_val)
368+
# Coerce to a list of strings. The only "iterable" shape we want
369+
# to expand is a real list/tuple/set — everything else (str, int,
370+
# Python ``datetime``, Zope ``DateTime``, …) is a single-value
371+
# scalar. Without this, ``list(scalar)`` raises ``TypeError:
372+
# object is not iterable`` on Zope DateTime and friends; str()
373+
# coercion matches what JSONB keyword arrays store (#152).
374+
if isinstance(query_val, (list, tuple, set, frozenset)):
375+
query_val = [str(v) for v in query_val]
376+
else:
377+
query_val = [str(query_val)]
369378

370379
# Check for dedicated TEXT[] column (generic ExtraIdxColumn mechanism)
371380
from plone.pgcatalog.columns import get_extra_idx_column_for_key

tests/test_query.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,44 @@ def test_default_operator_is_or(self):
8686
qr = build_query({"Subject": {"query": ["Python", "Zope"]}})
8787
assert "?|" in qr["where"]
8888

89+
def test_non_iterable_non_str_coerced_to_single_value(self):
90+
"""Regression for #152: a caller accidentally passing a non-string,
91+
non-iterable value (e.g. a Zope ``DateTime``, a number, etc.) must
92+
not crash the query builder with ``TypeError: object is not
93+
iterable``. Observed on aaf-6 prod where an addon passed a
94+
``DateTime`` through a keyword criterion by mistake.
95+
"""
96+
97+
class _NotIterable:
98+
"""Mimics Zope ``DateTime`` — has ``__str__`` but no
99+
``__iter__`` and isn't a ``str``."""
100+
101+
def __str__(self):
102+
return "2026-04-20"
103+
104+
qr = build_query({"Subject": _NotIterable()})
105+
# Falls into the single-value branch — containment (`@>`).
106+
assert "idx @>" in qr["where"]
107+
# Value was coerced to its str form.
108+
param = _find_json_param(qr["params"])
109+
assert param.obj == {"Subject": ["2026-04-20"]}
110+
111+
def test_zope_datetime_as_keyword_does_not_crash(self):
112+
"""The specific shape that surfaced in production: a Zope
113+
``DateTime`` passed as a keyword-index query value. It should
114+
be coerced via ``str()`` into a single-element list, not crash.
115+
"""
116+
from datetime import datetime
117+
from datetime import UTC
118+
119+
# Python ``datetime`` is also not iterable — exactly the same
120+
# bug class.
121+
dt = datetime(2026, 4, 20, tzinfo=UTC)
122+
qr = build_query({"Subject": dt})
123+
assert "idx @>" in qr["where"]
124+
param = _find_json_param(qr["params"])
125+
assert len(param.obj["Subject"]) == 1
126+
89127

90128
# ---------------------------------------------------------------------------
91129
# BooleanIndex

0 commit comments

Comments
 (0)