Skip to content

Commit 74e6c16

Browse files
committed
Merge pull request #460 from dhermes/fix-451-part4
Address fourth part of 451: Make Key constructor easier to use by taking positional args.
2 parents cfea8e9 + 676b20c commit 74e6c16

13 files changed

Lines changed: 255 additions & 255 deletions

gcloud/datastore/entity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def __init__(self, dataset=None, kind=None, exclude_from_indexes=()):
100100
# _implicit_environ._DatastoreBase to avoid split MRO.
101101
self._dataset = dataset or _implicit_environ.DATASET
102102
if kind:
103-
self._key = Key(path=[{'kind': kind}])
103+
self._key = Key(kind)
104104
else:
105105
self._key = None
106106
self._exclude_from_indexes = set(exclude_from_indexes)

gcloud/datastore/helpers.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,15 @@ def key_from_protobuf(pb):
6565
:rtype: :class:`gcloud.datastore.key.Key`
6666
:returns: a new `Key` instance
6767
"""
68-
path = []
68+
path_args = []
6969
for element in pb.path_element:
70-
element_dict = {'kind': element.kind}
71-
70+
path_args.append(element.kind)
7271
if element.HasField('id'):
73-
element_dict['id'] = element.id
74-
72+
path_args.append(element.id)
7573
# This is safe: we expect proto objects returned will only have
7674
# one of `name` or `id` set.
7775
if element.HasField('name'):
78-
element_dict['name'] = element.name
79-
80-
path.append(element_dict)
76+
path_args.append(element.name)
8177

8278
dataset_id = None
8379
if pb.partition_id.HasField('dataset_id'):
@@ -86,7 +82,7 @@ def key_from_protobuf(pb):
8682
if pb.partition_id.HasField('namespace'):
8783
namespace = pb.partition_id.namespace
8884

89-
return Key(path, namespace, dataset_id)
85+
return Key(*path_args, namespace=namespace, dataset_id=dataset_id)
9086

9187

9288
def _pb_attr_value(val):

gcloud/datastore/key.py

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,51 @@
1515
"""Create / interact with gcloud datastore keys."""
1616

1717
import copy
18+
from itertools import izip
19+
import six
1820

1921
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
2022

2123

2224
class Key(object):
2325
"""An immutable representation of a datastore Key.
2426
27+
To create a basic key:
28+
29+
>>> Key('EntityKind', 1234)
30+
<Key[{'kind': 'EntityKind', 'id': 1234}]>
31+
>>> Key('EntityKind', 'foo')
32+
<Key[{'kind': 'EntityKind', 'name': 'foo'}]>
33+
34+
To create a key with a parent:
35+
36+
>>> Key('Parent', 'foo', 'Child', 1234)
37+
<Key[{'kind': 'Parent', 'name': 'foo'}, {'kind': 'Child', 'id': 1234}]>
38+
39+
To create a paritial key:
40+
41+
>>> Key('Parent', 'foo', 'Child')
42+
<Key[{'kind': 'Parent', 'name': 'foo'}, {'kind': 'Child'}]>
43+
2544
.. automethod:: __init__
2645
"""
2746

28-
def __init__(self, path=None, namespace=None, dataset_id=None):
47+
def __init__(self, *path_args, **kwargs):
2948
"""Constructor / initializer for a key.
3049
31-
:type namespace: :class:`str`
32-
:param namespace: A namespace identifier for the key.
50+
:type path_args: tuple of strings and ints
51+
:param path_args: May represent a partial (odd length) or full (even
52+
length) key path.
3353
34-
:type path: sequence of dicts
35-
:param path: Each dict must have keys 'kind' (a string) and optionally
36-
'name' (a string) or 'id' (an integer).
54+
:type namespace: :class:`str`
55+
:param namespace: A namespace identifier for the key. Can only be
56+
passed as a keyword argument.
3757
3858
:type dataset_id: string
39-
:param dataset: The dataset ID assigned by back-end for the key.
59+
:param dataset_id: The dataset ID associated with the key. Can only be
60+
passed as a keyword argument.
61+
62+
# This note will be obsolete by the end of #451.
4063
4164
.. note::
4265
The key's ``_dataset_id`` field must be None for keys created
@@ -46,10 +69,51 @@ def __init__(self, path=None, namespace=None, dataset_id=None):
4669
returned from the datastore backend. The application
4770
**must** treat any value set by the back-end as opaque.
4871
"""
49-
self._path = path or [{'kind': ''}]
72+
self._path = self._parse_path(path_args)
73+
self._flat_path = path_args
5074
self._parent = None
51-
self._namespace = namespace
52-
self._dataset_id = dataset_id
75+
self._namespace = kwargs.get('namespace')
76+
self._dataset_id = kwargs.get('dataset_id')
77+
78+
@staticmethod
79+
def _parse_path(path_args):
80+
"""Parses positional arguments into key path with kinds and IDs.
81+
82+
:rtype: list of dict
83+
:returns: A list of key parts with kind and id or name set.
84+
:raises: `ValueError` if there are no `path_args`, if one of the
85+
kinds is not a string or if one of the IDs/names is not
86+
a string or an integer.
87+
"""
88+
if len(path_args) == 0:
89+
raise ValueError('Key path must not be empty.')
90+
91+
kind_list = path_args[::2]
92+
id_or_name_list = path_args[1::2]
93+
# Dummy sentinel value to pad incomplete key to even length path.
94+
partial_ending = object()
95+
if len(path_args) % 2 == 1:
96+
id_or_name_list += (partial_ending,)
97+
98+
result = []
99+
for kind, id_or_name in izip(kind_list, id_or_name_list):
100+
curr_key_part = {}
101+
if isinstance(kind, six.string_types):
102+
curr_key_part['kind'] = kind
103+
else:
104+
raise ValueError(kind, 'Kind was not a string.')
105+
106+
if isinstance(id_or_name, six.string_types):
107+
curr_key_part['name'] = id_or_name
108+
elif isinstance(id_or_name, six.integer_types):
109+
curr_key_part['id'] = id_or_name
110+
elif id_or_name is not partial_ending:
111+
raise ValueError(id_or_name,
112+
'ID/name was not a string or integer.')
113+
114+
result.append(curr_key_part)
115+
116+
return result
53117

54118
def _clone(self):
55119
"""Duplicates the Key.
@@ -74,8 +138,8 @@ def to_protobuf(self):
74138
if self.dataset_id is not None:
75139
key.partition_id.dataset_id = self.dataset_id
76140

77-
if self._namespace:
78-
key.partition_id.namespace = self._namespace
141+
if self.namespace:
142+
key.partition_id.namespace = self.namespace
79143

80144
for item in self.path:
81145
element = key.path_element.add()
@@ -118,15 +182,23 @@ def path(self):
118182
"""
119183
return copy.deepcopy(self._path)
120184

185+
@property
186+
def flat_path(self):
187+
"""Getter for the key path as a tuple.
188+
189+
:rtype: :class:`tuple` of string and int
190+
:returns: The tuple of elements in the path.
191+
"""
192+
return self._flat_path
193+
121194
@property
122195
def kind(self):
123196
"""Kind getter. Based on the last element of path.
124197
125198
:rtype: :class:`str`
126199
:returns: The kind of the current key.
127200
"""
128-
if self.path:
129-
return self.path[-1].get('kind')
201+
return self.path[-1]['kind']
130202

131203
@property
132204
def id(self):
@@ -135,8 +207,7 @@ def id(self):
135207
:rtype: :class:`int`
136208
:returns: The (integer) ID of the key.
137209
"""
138-
if self.path:
139-
return self.path[-1].get('id')
210+
return self.path[-1].get('id')
140211

141212
@property
142213
def name(self):
@@ -145,8 +216,7 @@ def name(self):
145216
:rtype: :class:`str`
146217
:returns: The (string) name of the key.
147218
"""
148-
if self.path:
149-
return self.path[-1].get('name')
219+
return self.path[-1].get('name')
150220

151221
@property
152222
def id_or_name(self):
@@ -178,14 +248,17 @@ def _make_parent(self):
178248
element of self's path. If self has only one path element,
179249
returns None.
180250
"""
181-
parent_path = self.path[:-1]
182-
if parent_path:
183-
return Key(path=parent_path, dataset_id=self.dataset_id,
251+
if self.is_partial:
252+
parent_args = self.flat_path[:-1]
253+
else:
254+
parent_args = self.flat_path[:-2]
255+
if parent_args:
256+
return Key(*parent_args, dataset_id=self.dataset_id,
184257
namespace=self.namespace)
185258

186259
@property
187260
def parent(self):
188-
"""Getter: return a new key for the next highest element in path.
261+
"""The parent of the current key.
189262
190263
:rtype: :class:`gcloud.datastore.key.Key` or `NoneType`
191264
:returns: a new `Key` instance, whose path consists of all but the last

gcloud/datastore/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def ancestor(self, ancestor):
172172
This will return a clone of the current :class:`Query` filtered
173173
by the ancestor provided. For example::
174174
175-
>>> parent_key = Key(path=[{'kind': 'Person', 'name': '1'}])
175+
>>> parent_key = Key('Person', '1')
176176
>>> query = dataset.query('Person')
177177
>>> filtered_query = query.ancestor(parent_key)
178178

gcloud/datastore/test___init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,9 @@ def test_allocate_ids(self):
167167
from gcloud._testing import _Monkey
168168

169169
CUSTOM_DATASET = _Dataset()
170-
INCOMPLETE_KEY = Key()
171170
NUM_IDS = 2
172171
with _Monkey(_implicit_environ, DATASET=CUSTOM_DATASET):
172+
INCOMPLETE_KEY = Key('KIND')
173173
result = gcloud.datastore.allocate_ids(INCOMPLETE_KEY, NUM_IDS)
174174

175175
# Check the IDs returned.

0 commit comments

Comments
 (0)