Skip to content

Commit ac995b9

Browse files
authored
Prepare for UpdateExpression API (#348)
1 parent ecc2af8 commit ac995b9

File tree

10 files changed

+230
-192
lines changed

10 files changed

+230
-192
lines changed

pynamodb/attributes.py

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
STRING, STRING_SHORT, NUMBER, BINARY, UTC, DATETIME_FORMAT, BINARY_SET, STRING_SET, NUMBER_SET,
1515
MAP, MAP_SHORT, LIST, LIST_SHORT, DEFAULT_ENCODING, BOOLEAN, ATTR_TYPE_MAP, NUMBER_SHORT, NULL, SHORT_ATTR_TYPES
1616
)
17-
from pynamodb.expressions.condition import Path
17+
from pynamodb.expressions.operand import AttributePath
1818
import collections
1919

2020

@@ -106,7 +106,7 @@ def __ge__(self, other):
106106
return AttributePath(self).__ge__(other)
107107

108108
def __getitem__(self, idx):
109-
return AttributePath(self)[idx]
109+
return AttributePath(self).__getitem__(idx)
110110

111111
def between(self, lower, upper):
112112
return AttributePath(self).between(lower, upper)
@@ -131,39 +131,6 @@ def contains(self, item):
131131
return AttributePath(self).contains(item)
132132

133133

134-
class AttributePath(Path):
135-
136-
def __init__(self, attribute):
137-
super(AttributePath, self).__init__(attribute.attr_path)
138-
self.attribute = attribute
139-
140-
def __getitem__(self, idx):
141-
if self.attribute.attr_type != LIST: # only list elements support the list dereference operator
142-
raise TypeError("'{0}' object has no attribute __getitem__".format(self.attribute.__class__.__name__))
143-
# The __getitem__ call returns a new Path instance, not an AttributePath instance.
144-
# This is intended since the list element is not the same attribute as the list itself.
145-
return super(AttributePath, self).__getitem__(idx)
146-
147-
def contains(self, item):
148-
if self.attribute.attr_type in [BINARY_SET, NUMBER_SET, STRING_SET]:
149-
# Set attributes assume the values to be serialized are sets.
150-
(attr_type, attr_value), = self._serialize([item]).items()
151-
item = {attr_type[0]: attr_value[0]}
152-
return super(AttributePath, self).contains(item)
153-
154-
def _serialize(self, value):
155-
# Check to see if value is already serialized
156-
if isinstance(value, dict) and len(value) == 1 and list(value.keys())[0] in SHORT_ATTR_TYPES:
157-
return value
158-
if self.attribute.attr_type == LIST and not isinstance(value, list):
159-
# List attributes assume the values to be serialized are lists.
160-
return self.attribute.serialize([value])[0]
161-
if self.attribute.attr_type == MAP and not isinstance(value, dict):
162-
# Map attributes assume the values to be serialized are maps.
163-
return super(AttributePath, self)._serialize(value)
164-
return {ATTR_TYPE_MAP[self.attribute.attr_type]: self.attribute.serialize(value)}
165-
166-
167134
class AttributeContainerMeta(type):
168135

169136
def __init__(cls, name, bases, attrs):

pynamodb/connection/base.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@
4343
TableError, QueryError, PutError, DeleteError, UpdateError, GetError, ScanError, TableDoesNotExist,
4444
VerboseClientError
4545
)
46-
from pynamodb.expressions.condition import Condition, Path
46+
from pynamodb.expressions.condition import Condition
47+
from pynamodb.expressions.operand import Path
4748
from pynamodb.expressions.projection import create_projection_expression
48-
from pynamodb.expressions.update import AddAction, DeleteAction, RemoveAction, SetAction, Update
49+
from pynamodb.expressions.update import Update
4950
from pynamodb.settings import get_settings_value
5051
from pynamodb.signals import pre_dynamodb_send, post_dynamodb_send
5152
from pynamodb.types import HASH, RANGE
@@ -870,6 +871,7 @@ def update_item(self,
870871
update_expression = Update()
871872
# We sort the keys here for determinism. This is mostly done to simplify testing.
872873
for key in sorted(attribute_updates.keys()):
874+
path = Path([key])
873875
update = attribute_updates[key]
874876
action = update.get(ACTION)
875877
if action not in ATTR_UPDATE_ACTIONS:
@@ -880,11 +882,12 @@ def update_item(self,
880882
attr_type = self.get_attribute_type(table_name, key, value)
881883
value = {attr_type: value}
882884
if action == DELETE:
883-
action = RemoveAction(key) if attr_type is None else DeleteAction(key, value)
885+
action = path.remove() if attr_type is None else path.difference_update(value)
884886
elif action == PUT:
885-
action = SetAction(key, value)
887+
action = path.set(value)
886888
else:
887-
action = AddAction(key, value)
889+
# right now update() returns an AddAction
890+
action = path.update(value)
888891
update_expression.add_action(action)
889892
operation_kwargs[UPDATE_EXPRESSION] = update_expression.serialize(name_placeholders, expression_attribute_values)
890893

pynamodb/expressions/condition.py

Lines changed: 3 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,16 @@
11
from pynamodb.constants import (
2-
AND, ATTR_TYPE_MAP, BETWEEN, BINARY_SHORT, IN, NUMBER_SHORT, OR, SHORT_ATTR_TYPES, STRING_SHORT
2+
AND, BETWEEN, BINARY_SHORT, IN, NUMBER_SHORT, OR, SHORT_ATTR_TYPES, STRING_SHORT
33
)
44
from pynamodb.expressions.util import get_value_placeholder, substitute_names
5-
from six import string_types
65
from six.moves import range
76

87

9-
class Operand(object):
10-
"""
11-
Operand is the base class for objects that support creating conditions from comparators.
12-
"""
13-
14-
def __eq__(self, other):
15-
return self._compare('=', other)
16-
17-
def __ne__(self, other):
18-
return self._compare('<>', other)
19-
20-
def __lt__(self, other):
21-
return self._compare('<', other)
22-
23-
def __le__(self, other):
24-
return self._compare('<=', other)
25-
26-
def __gt__(self, other):
27-
return self._compare('>', other)
28-
29-
def __ge__(self, other):
30-
return self._compare('>=', other)
31-
32-
def _compare(self, operator, other):
33-
return Condition(self, operator, self._serialize(other))
34-
35-
def between(self, lower, upper):
36-
# This seemed preferable to other options such as merging value1 <= attribute & attribute <= value2
37-
# into one condition expression. DynamoDB only allows a single sort key comparison and having this
38-
# work but similar expressions like value1 <= attribute & attribute < value2 fail seems too brittle.
39-
return Between(self, self._serialize(lower), self._serialize(upper))
40-
41-
def is_in(self, *values):
42-
values = [self._serialize(value) for value in values]
43-
return In(self, *values)
44-
45-
def _serialize(self, value):
46-
# Check to see if value is already serialized
47-
if isinstance(value, dict) and len(value) == 1 and list(value.keys())[0] in SHORT_ATTR_TYPES:
48-
return value
49-
# Serialize value based on its type
50-
from pynamodb.attributes import _get_class_for_serialize
51-
attr_class = _get_class_for_serialize(value)
52-
return {ATTR_TYPE_MAP[attr_class.attr_type]: attr_class.serialize(value)}
53-
54-
55-
class Size(Operand):
56-
"""
57-
Size is a special operand that represents the result of calling the 'size' function on a Path operand.
58-
"""
59-
60-
def __init__(self, path):
61-
# prevent circular import -- AttributePath imports Path
62-
from pynamodb.attributes import Attribute, AttributePath
63-
if isinstance(path, Path):
64-
self.path = Path
65-
elif isinstance(path, Attribute):
66-
self.path = AttributePath(path)
67-
else:
68-
self.path = Path(path)
69-
70-
def _serialize(self, value):
71-
if not isinstance(value, int):
72-
raise TypeError("size must be compared to an integer, not {0}".format(type(value).__name__))
73-
return {NUMBER_SHORT: str(value)}
74-
75-
def __str__(self):
76-
return "size({0})".format(self.path)
77-
78-
def __repr__(self):
79-
return "Size({0})".format(repr(self.path))
80-
81-
828
# match dynamo function syntax: size(path)
839
def size(path):
10+
from pynamodb.expressions.operand import Size
8411
return Size(path)
8512

8613

87-
class Path(Operand):
88-
"""
89-
Path is an operand that represents either an attribute name or document path.
90-
In addition to supporting comparisons, Path also supports creating conditions from functions.
91-
"""
92-
93-
def __init__(self, path):
94-
if not path:
95-
raise ValueError("path cannot be empty")
96-
self.path = path.split('.') if isinstance(path, string_types) else list(path)
97-
98-
def __getitem__(self, idx):
99-
# list dereference operator
100-
if not isinstance(idx, int):
101-
raise TypeError("list indices must be integers, not {0}".format(type(idx).__name__))
102-
element_path = Path(self.path) # copy the document path before indexing last element
103-
element_path.path[-1] = '{0}[{1}]'.format(self.path[-1], idx)
104-
return element_path
105-
106-
def exists(self):
107-
return Exists(self)
108-
109-
def does_not_exist(self):
110-
return NotExists(self)
111-
112-
def is_type(self, attr_type):
113-
return IsType(self, attr_type)
114-
115-
def startswith(self, prefix):
116-
# A 'pythonic' replacement for begins_with to match string behavior (e.g. "foo".startswith("f"))
117-
return BeginsWith(self, self._serialize(prefix))
118-
119-
def contains(self, item):
120-
return Contains(self, self._serialize(item))
121-
122-
def __str__(self):
123-
# Quote the path to illustrate that any dot characters are not dereference operators.
124-
quoted_path = [self._quote_path(segment) if '.' in segment else segment for segment in self.path]
125-
return '.'.join(quoted_path)
126-
127-
def __repr__(self):
128-
return "Path({0})".format(self.path)
129-
130-
@staticmethod
131-
def _quote_path(path):
132-
path, sep, rem = path.partition('[')
133-
return repr(path) + sep + rem
134-
135-
13614
class Condition(object):
13715
format_string = '{path} {operator} {0}'
13816

@@ -151,6 +29,7 @@ def serialize(self, placeholder_names, expression_attribute_values):
15129
return self.format_string.format(*values, path=path, operator=self.operator)
15230

15331
def _get_path(self, path, placeholder_names):
32+
from pynamodb.expressions.operand import Path, Size
15433
if isinstance(path, Path):
15534
return substitute_names(path.path, placeholder_names)
15635
elif isinstance(path, Size):

0 commit comments

Comments
 (0)