Skip to content
This repository was archived by the owner on May 6, 2026. It is now read-only.

Commit 2b65c04

Browse files
author
Chris Rossi
authored
fix: uniform handling of projection argument (#428)
The `projection` argument is now handled the same whether it is passed in to the constructor for `Query` or to one if it's methods, such as `Query.fetch`. Fixes #379
1 parent 4524542 commit 2b65c04

3 files changed

Lines changed: 63 additions & 50 deletions

File tree

google/cloud/ndb/query.py

Lines changed: 41 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,7 +1160,6 @@ def wrapper(self, *args, **kwargs):
11601160
# Avoid circular import in Python 2.7
11611161
from google.cloud.ndb import context as context_module
11621162
from google.cloud.ndb import _datastore_api
1163-
from google.cloud.ndb import model
11641163

11651164
# Maybe we already did this (in the case of X calling X_async)
11661165
if "_options" in kwargs:
@@ -1184,6 +1183,12 @@ def wrapper(self, *args, **kwargs):
11841183
"deprecated. Please pass arguments directly."
11851184
)
11861185

1186+
projection = kwargs.get("projection")
1187+
if projection:
1188+
projection = _to_property_names(projection)
1189+
_check_properties(self.kind, projection)
1190+
kwargs["projection"] = projection
1191+
11871192
if kwargs.get("keys_only"):
11881193
if kwargs.get("projection"):
11891194
raise TypeError(
@@ -1192,22 +1197,6 @@ def wrapper(self, *args, **kwargs):
11921197
kwargs["projection"] = ["__key__"]
11931198
del kwargs["keys_only"]
11941199

1195-
# When projection fields are passed as property objects, we need to
1196-
# convert them into property names. Fixes #295.
1197-
if kwargs.get("projection"):
1198-
property_names = []
1199-
for prop in kwargs["projection"]:
1200-
if isinstance(prop, six.string_types):
1201-
property_names.append(prop)
1202-
elif isinstance(prop, model.Property):
1203-
property_names.append(prop._name)
1204-
else:
1205-
raise TypeError(
1206-
"Unexpected projection value {}; "
1207-
"should be string or Property".format(prop)
1208-
)
1209-
kwargs["projection"] = property_names
1210-
12111200
if kwargs.get("transaction"):
12121201
read_consistency = kwargs.pop(
12131202
"read_consistency", kwargs.pop("read_policy", None)
@@ -1451,8 +1440,8 @@ def __init__(
14511440
"projection must be a tuple, list or None; "
14521441
"received {}".format(projection)
14531442
)
1454-
projection = self._to_property_names(projection)
1455-
self._check_properties(projection)
1443+
projection = _to_property_names(projection)
1444+
_check_properties(self.kind, projection)
14561445
self.projection = tuple(projection)
14571446

14581447
if distinct_on is not None and group_by is not None:
@@ -1472,8 +1461,8 @@ def __init__(
14721461
"distinct_on must be a tuple, list or None; "
14731462
"received {}".format(distinct_on)
14741463
)
1475-
distinct_on = self._to_property_names(distinct_on)
1476-
self._check_properties(distinct_on)
1464+
distinct_on = _to_property_names(distinct_on)
1465+
_check_properties(self.kind, distinct_on)
14771466
self.distinct_on = tuple(distinct_on)
14781467

14791468
def __repr__(self):
@@ -1492,11 +1481,11 @@ def __repr__(self):
14921481
args.append("order_by=%r" % self.order_by)
14931482
if self.projection:
14941483
args.append(
1495-
"projection=%r" % (self._to_property_names(self.projection))
1484+
"projection=%r" % (_to_property_names(self.projection))
14961485
)
14971486
if self.distinct_on:
14981487
args.append(
1499-
"distinct_on=%r" % (self._to_property_names(self.distinct_on))
1488+
"distinct_on=%r" % (_to_property_names(self.distinct_on))
15001489
)
15011490
if self.default_options is not None:
15021491
args.append("default_options=%r" % self.default_options)
@@ -1511,8 +1500,8 @@ def is_distinct(self):
15111500
"""
15121501
return bool(
15131502
self.distinct_on
1514-
and set(self._to_property_names(self.distinct_on))
1515-
<= set(self._to_property_names(self.projection))
1503+
and set(_to_property_names(self.distinct_on))
1504+
<= set(_to_property_names(self.projection))
15161505
)
15171506

15181507
def filter(self, *filters):
@@ -1662,23 +1651,6 @@ def bind(self, *positional, **keyword):
16621651
distinct_on=self.distinct_on,
16631652
)
16641653

1665-
def _to_property_names(self, properties):
1666-
# Avoid circular import in Python 2.7
1667-
from google.cloud.ndb import model
1668-
1669-
fixed = []
1670-
for prop in properties:
1671-
if isinstance(prop, six.string_types):
1672-
fixed.append(prop)
1673-
elif isinstance(prop, model.Property):
1674-
fixed.append(prop._name)
1675-
else:
1676-
raise TypeError(
1677-
"Unexpected property {}; "
1678-
"should be string or Property".format(prop)
1679-
)
1680-
return fixed
1681-
16821654
def _to_property_orders(self, order_by):
16831655
# Avoid circular import in Python 2.7
16841656
from google.cloud.ndb import model
@@ -1703,14 +1675,6 @@ def _to_property_orders(self, order_by):
17031675
raise TypeError("Order values must be properties or strings")
17041676
return orders
17051677

1706-
def _check_properties(self, fixed, **kwargs):
1707-
# Avoid circular import in Python 2.7
1708-
from google.cloud.ndb import model
1709-
1710-
modelclass = model.Model._kind_map.get(self.kind)
1711-
if modelclass is not None:
1712-
modelclass._check_properties(fixed, **kwargs)
1713-
17141678
@_query_options
17151679
@utils.keyword_only(
17161680
keys_only=None,
@@ -2400,3 +2364,30 @@ def gql(query_string, *args, **kwds):
24002364
if args or kwds:
24012365
query = query.bind(*args, **kwds)
24022366
return query
2367+
2368+
2369+
def _to_property_names(properties):
2370+
# Avoid circular import in Python 2.7
2371+
from google.cloud.ndb import model
2372+
2373+
fixed = []
2374+
for prop in properties:
2375+
if isinstance(prop, six.string_types):
2376+
fixed.append(prop)
2377+
elif isinstance(prop, model.Property):
2378+
fixed.append(prop._name)
2379+
else:
2380+
raise TypeError(
2381+
"Unexpected property {}; "
2382+
"should be string or Property".format(prop)
2383+
)
2384+
return fixed
2385+
2386+
2387+
def _check_properties(kind, fixed, **kwargs):
2388+
# Avoid circular import in Python 2.7
2389+
from google.cloud.ndb import model
2390+
2391+
modelclass = model.Model._kind_map.get(kind)
2392+
if modelclass is not None:
2393+
modelclass._check_properties(fixed, **kwargs)

tests/unit/test_model.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4713,6 +4713,14 @@ class XModel(model.Model):
47134713
with pytest.raises(TypeError):
47144714
XModel.query(distinct=True, group_by=("x",))
47154715

4716+
@staticmethod
4717+
def test_query_projection_of_unindexed_attribute():
4718+
class XModel(model.Model):
4719+
x = model.IntegerProperty(indexed=False)
4720+
4721+
with pytest.raises(model.InvalidPropertyError):
4722+
XModel.query(projection=["x"])
4723+
47164724
@staticmethod
47174725
@pytest.mark.usefixtures("in_context")
47184726
def test_gql():

tests/unit/test_query.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,20 @@ def test_fetch_with_limit_as_positional_arg(_datastore_query):
19581958
query_module.QueryOptions(project="testing", limit=20)
19591959
)
19601960

1961+
@staticmethod
1962+
@pytest.mark.usefixtures("in_context")
1963+
@mock.patch("google.cloud.ndb._datastore_query")
1964+
def test_fetch_projection_of_unindexed_property(_datastore_query):
1965+
class SomeKind(model.Model):
1966+
foo = model.IntegerProperty(indexed=False)
1967+
1968+
future = tasklets.Future("fetch")
1969+
future.set_result("foo")
1970+
_datastore_query.fetch.return_value = future
1971+
query = query_module.Query(kind="SomeKind")
1972+
with pytest.raises(model.InvalidPropertyError):
1973+
query.fetch(projection=["foo"])
1974+
19611975
@staticmethod
19621976
@pytest.mark.usefixtures("in_context")
19631977
def test_run_to_queue():

0 commit comments

Comments
 (0)