Skip to content

Commit 5fd2ee0

Browse files
authored
Reduce the surface area of the expressions api. (#351)
1 parent e5d5b3e commit 5fd2ee0

File tree

4 files changed

+113
-134
lines changed

4 files changed

+113
-134
lines changed

pynamodb/attributes.py

Lines changed: 15 additions & 15 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.operand import AttributePath
17+
from pynamodb.expressions.operand import Path
1818
import collections
1919

2020

@@ -86,49 +86,49 @@ def get_value(self, value):
8686
def __eq__(self, other):
8787
if other is None or isinstance(other, Attribute): # handle object identity comparison
8888
return self is other
89-
return AttributePath(self).__eq__(other)
89+
return Path(self).__eq__(other)
9090

9191
def __ne__(self, other):
9292
if other is None or isinstance(other, Attribute): # handle object identity comparison
9393
return self is not other
94-
return AttributePath(self).__ne__(other)
94+
return Path(self).__ne__(other)
9595

9696
def __lt__(self, other):
97-
return AttributePath(self).__lt__(other)
97+
return Path(self).__lt__(other)
9898

9999
def __le__(self, other):
100-
return AttributePath(self).__le__(other)
100+
return Path(self).__le__(other)
101101

102102
def __gt__(self, other):
103-
return AttributePath(self).__gt__(other)
103+
return Path(self).__gt__(other)
104104

105105
def __ge__(self, other):
106-
return AttributePath(self).__ge__(other)
106+
return Path(self).__ge__(other)
107107

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

111111
def between(self, lower, upper):
112-
return AttributePath(self).between(lower, upper)
112+
return Path(self).between(lower, upper)
113113

114114
def is_in(self, *values):
115-
return AttributePath(self).is_in(*values)
115+
return Path(self).is_in(*values)
116116

117117
def exists(self):
118-
return AttributePath(self).exists()
118+
return Path(self).exists()
119119

120120
def does_not_exist(self):
121-
return AttributePath(self).does_not_exist()
121+
return Path(self).does_not_exist()
122122

123123
def is_type(self):
124124
# What makes sense here? Are we using this to check if deserialization will be successful?
125-
return AttributePath(self).is_type(ATTR_TYPE_MAP[self.attr_type])
125+
return Path(self).is_type(ATTR_TYPE_MAP[self.attr_type])
126126

127127
def startswith(self, prefix):
128-
return AttributePath(self).startswith(prefix)
128+
return Path(self).startswith(prefix)
129129

130130
def contains(self, item):
131-
return AttributePath(self).contains(item)
131+
return Path(self).contains(item)
132132

133133

134134
class AttributeContainerMeta(type):

pynamodb/expressions/condition.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
# match dynamo function syntax: size(path)
66
def size(path):
7-
from pynamodb.expressions.operand import Size
8-
return Size(path)
7+
from pynamodb.expressions.operand import _Size
8+
return _Size(path)
99

1010

1111
class Condition(object):

pynamodb/expressions/operand.py

Lines changed: 94 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,44 @@
1010
from pynamodb.expressions.util import get_path_segments, get_value_placeholder, substitute_names
1111

1212

13-
class Operand(object):
13+
class _Operand(object):
1414
"""
1515
Operand is the base class for objects that can be operands in Condition and Update Expressions.
1616
"""
17+
format_string = ''
18+
short_attr_type = None
19+
20+
def __init__(self, *values):
21+
self.values = values
22+
23+
def __repr__(self):
24+
return self.format_string.format(*self.values)
25+
26+
def serialize(self, placeholder_names, expression_attribute_values):
27+
values = [self._serialize_value(value, placeholder_names, expression_attribute_values) for value in self.values]
28+
return self.format_string.format(*values)
29+
30+
def _serialize_value(self, value, placeholder_names, expression_attribute_values):
31+
return value.serialize(placeholder_names, expression_attribute_values)
32+
33+
def _to_operand(self, value):
34+
if isinstance(value, _Operand):
35+
return value
36+
from pynamodb.attributes import Attribute # prevent circular import -- Attribute imports Path
37+
return Path(value) if isinstance(value, Attribute) else self._to_value(value)
38+
39+
def _to_value(self, value):
40+
return Value(value)
41+
42+
def _type_check(self, *types):
43+
if self.short_attr_type and self.short_attr_type not in types:
44+
raise ValueError("The data type of '{0}' must be one of {1}".format(self, list(types)))
45+
46+
47+
class _ConditionOperand(_Operand):
48+
"""
49+
A base class for Operands that can be used in Condition Expression comparisons.
50+
"""
1751

1852
def __eq__(self, other):
1953
return Comparison('=', self, self._to_operand(other))
@@ -40,86 +74,94 @@ def is_in(self, *values):
4074
values = [self._to_operand(value) for value in values]
4175
return In(self, *values)
4276

43-
def serialize(self, placeholder_names, expression_attribute_values):
44-
raise NotImplementedError('serialize has not been implemented for {0}'.format(self.__class__.__name__))
4577

46-
def _has_type(self, short_type):
47-
raise NotImplementedError('_has_type has not been implemented for {0}'.format(self.__class__.__name__))
78+
class _Size(_ConditionOperand):
79+
"""
80+
Size is a special operand that represents the result of calling the 'size' function on a Path operand.
81+
"""
82+
format_string = 'size ({0})'
83+
short_attr_type = NUMBER_SHORT
84+
85+
def __init__(self, path):
86+
if not isinstance(path, Path):
87+
path = Path(path)
88+
super(_Size, self).__init__(path)
4889

4990
def _to_operand(self, value):
50-
from pynamodb.attributes import Attribute # prevent circular import -- Attribute imports AttributePath
51-
if isinstance(value, Attribute):
52-
return AttributePath(value)
53-
return value if isinstance(value, Operand) else self._to_value(value)
54-
55-
def _to_value(self, value):
56-
return Value(value)
91+
operand = super(_Size, self)._to_operand(value)
92+
operand._type_check(NUMBER_SHORT)
93+
return operand
5794

5895

59-
class Value(Operand):
96+
class Value(_ConditionOperand):
6097
"""
6198
Value is an operand that represents an attribute value.
6299
"""
100+
format_string = '{0}'
63101

64102
def __init__(self, value, attribute=None):
65103
# Check to see if value is already serialized
66104
if isinstance(value, dict) and len(value) == 1 and list(value.keys())[0] in SHORT_ATTR_TYPES:
67-
self.value = value
105+
(self.short_attr_type, value), = value.items()
68106
else:
69-
self.value = Value._serialize_value(value, attribute)
107+
(self.short_attr_type, value) = Value.__serialize(value, attribute)
108+
super(Value, self).__init__({self.short_attr_type: value})
70109

71-
def serialize(self, placeholder_names, expression_attribute_values):
72-
return get_value_placeholder(self.value, expression_attribute_values)
110+
@property
111+
def value(self):
112+
return self.values[0]
73113

74-
def _has_type(self, short_type):
75-
(attr_type, value), = self.value.items()
76-
return short_type == attr_type
77-
78-
def __str__(self):
79-
(attr_type, value), = self.value.items()
80-
try:
81-
from pynamodb.attributes import _get_class_for_deserialize
82-
attr_class = _get_class_for_deserialize(self.value)
83-
return str(attr_class.deserialize(value))
84-
except ValueError:
85-
return str(value)
86-
87-
def __repr__(self):
88-
return "Value({0})".format(self.value)
114+
def _serialize_value(self, value, placeholder_names, expression_attribute_values):
115+
return get_value_placeholder(value, expression_attribute_values)
89116

90117
@staticmethod
91-
def _serialize_value(value, attribute=None):
118+
def __serialize(value, attribute=None):
92119
if attribute is None:
93-
return Value._serialize_value_based_on_type(value)
120+
return Value.__serialize_based_on_type(value)
94121
if attribute.attr_type == LIST and not isinstance(value, list):
95122
# List attributes assume the values to be serialized are lists.
96-
return attribute.serialize([value])[0]
123+
(attr_type, attr_value), = attribute.serialize([value])[0].items()
124+
return attr_type, attr_value
97125
if attribute.attr_type == MAP and not isinstance(value, dict):
98126
# Map attributes assume the values to be serialized are maps.
99-
return Value._serialize_value_based_on_type(value)
100-
return {ATTR_TYPE_MAP[attribute.attr_type]: attribute.serialize(value)}
127+
return Value.__serialize_based_on_type(value)
128+
return ATTR_TYPE_MAP[attribute.attr_type], attribute.serialize(value)
101129

102130
@staticmethod
103-
def _serialize_value_based_on_type(value):
131+
def __serialize_based_on_type(value):
104132
from pynamodb.attributes import _get_class_for_serialize
105133
attr_class = _get_class_for_serialize(value)
106-
return {ATTR_TYPE_MAP[attr_class.attr_type]: attr_class.serialize(value)}
134+
return ATTR_TYPE_MAP[attr_class.attr_type], attr_class.serialize(value)
107135

108136

109-
class Path(Operand):
137+
class Path(_ConditionOperand):
110138
"""
111139
Path is an operand that represents either an attribute name or document path.
112140
"""
113-
114-
def __init__(self, path):
141+
format_string = '{0}'
142+
143+
def __init__(self, attribute_or_path):
144+
from pynamodb.attributes import Attribute # prevent circular import -- Attribute imports Path
145+
is_attribute = isinstance(attribute_or_path, Attribute)
146+
self.attribute = attribute_or_path if is_attribute else None
147+
self.short_attr_type = ATTR_TYPE_MAP[attribute_or_path.attr_type] if is_attribute else None
148+
path = attribute_or_path.attr_path if is_attribute else attribute_or_path
115149
if not path:
116150
raise ValueError("path cannot be empty")
117-
self.path = get_path_segments(path)
151+
super(Path, self).__init__(get_path_segments(path))
152+
153+
@property
154+
def path(self):
155+
return self.values[0]
118156

119157
def __getitem__(self, idx):
120158
# list dereference operator
159+
if self.attribute and self.attribute.attr_type != LIST:
160+
raise TypeError("'{0}' object has no attribute __getitem__".format(self.attribute.__class__.__name__))
121161
if not isinstance(idx, int):
122162
raise TypeError("list indices must be integers, not {0}".format(type(idx).__name__))
163+
# The __getitem__ call returns a new Path instance without any attribute set.
164+
# This is intended since the list element is not the same attribute as the list itself.
123165
element_path = Path(self.path) # copy the document path before indexing last element
124166
element_path.path[-1] = '{0}[{1}]'.format(self.path[-1], idx)
125167
return element_path
@@ -155,19 +197,21 @@ def is_type(self, attr_type):
155197
def startswith(self, prefix):
156198
# A 'pythonic' replacement for begins_with to match string behavior (e.g. "foo".startswith("f"))
157199
operand = self._to_operand(prefix)
158-
if not operand._has_type(STRING_SHORT):
159-
raise ValueError("{0} must be a string operand".format(operand))
200+
operand._type_check(STRING_SHORT)
160201
return BeginsWith(self, operand)
161202

162203
def contains(self, item):
204+
if self.attribute and self.attribute.attr_type in [BINARY_SET, NUMBER_SET, STRING_SET]:
205+
# Set attributes assume the values to be serialized are sets.
206+
(attr_type, attr_value), = self._to_value([item]).value.items()
207+
item = {attr_type[0]: attr_value[0]}
163208
return Contains(self, self._to_operand(item))
164209

165-
def serialize(self, placeholder_names, expression_attribute_values):
166-
return substitute_names(self.path, placeholder_names)
210+
def _serialize_value(self, value, placeholder_names, expression_attribute_values):
211+
return substitute_names(value, placeholder_names)
167212

168-
def _has_type(self, short_type):
169-
# Assume the attribute has the correct type
170-
return True
213+
def _to_value(self, value):
214+
return Value(value, attribute=self.attribute)
171215

172216
def __str__(self):
173217
# Quote the path to illustrate that any dot characters are not dereference operators.
@@ -181,64 +225,3 @@ def __repr__(self):
181225
def _quote_path(path):
182226
path, sep, rem = path.partition('[')
183227
return repr(path) + sep + rem
184-
185-
186-
class AttributePath(Path):
187-
188-
def __init__(self, attribute):
189-
super(AttributePath, self).__init__(attribute.attr_path)
190-
self.attribute = attribute
191-
192-
def __getitem__(self, idx):
193-
if self.attribute.attr_type != LIST: # only list elements support the list dereference operator
194-
raise TypeError("'{0}' object has no attribute __getitem__".format(self.attribute.__class__.__name__))
195-
# The __getitem__ call returns a new Path instance, not an AttributePath instance.
196-
# This is intended since the list element is not the same attribute as the list itself.
197-
return super(AttributePath, self).__getitem__(idx)
198-
199-
def contains(self, item):
200-
if self.attribute.attr_type in [BINARY_SET, NUMBER_SET, STRING_SET]:
201-
# Set attributes assume the values to be serialized are sets.
202-
(attr_type, attr_value), = self._to_value([item]).value.items()
203-
item = {attr_type[0]: attr_value[0]}
204-
return super(AttributePath, self).contains(item)
205-
206-
def _has_type(self, short_type):
207-
return ATTR_TYPE_MAP[self.attribute.attr_type] == short_type
208-
209-
def _to_value(self, value):
210-
return Value(value, attribute=self.attribute)
211-
212-
213-
class Size(Operand):
214-
"""
215-
Size is a special operand that represents the result of calling the 'size' function on a Path operand.
216-
"""
217-
218-
def __init__(self, path):
219-
# prevent circular import -- Attribute imports AttributePath
220-
from pynamodb.attributes import Attribute
221-
if isinstance(path, Path):
222-
self.path = path
223-
elif isinstance(path, Attribute):
224-
self.path = AttributePath(path)
225-
else:
226-
self.path = Path(path)
227-
228-
def _to_operand(self, value):
229-
operand = super(Size, self)._to_operand(value)
230-
if not operand._has_type(NUMBER_SHORT):
231-
raise ValueError("size must be compared to a number, not {0}".format(operand))
232-
return operand
233-
234-
def serialize(self, placeholder_names, expression_attribute_values):
235-
return "size ({0})".format(substitute_names(self.path.path, placeholder_names))
236-
237-
def _has_type(self, short_type):
238-
return short_type == NUMBER_SHORT
239-
240-
def __str__(self):
241-
return "size({0})".format(self.path)
242-
243-
def __repr__(self):
244-
return "Size({0})".format(repr(self.path))

pynamodb/expressions/update.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ class AddAction(Action):
3939
format_string = '{0} {1}'
4040

4141
def __init__(self, path, subset):
42-
(attr_type, value), = subset.value.items()
43-
if attr_type not in [BINARY_SET_SHORT, NUMBER_SET_SHORT, NUMBER_SHORT, STRING_SET_SHORT]:
44-
raise ValueError("{0} must be a number or set".format(value))
42+
subset._type_check(BINARY_SET_SHORT, NUMBER_SET_SHORT, NUMBER_SHORT, STRING_SET_SHORT)
4543
super(AddAction, self).__init__(path, subset)
4644

4745

@@ -52,9 +50,7 @@ class DeleteAction(Action):
5250
format_string = '{0} {1}'
5351

5452
def __init__(self, path, subset):
55-
(attr_type, value), = subset.value.items()
56-
if attr_type not in [BINARY_SET_SHORT, NUMBER_SET_SHORT, STRING_SET_SHORT]:
57-
raise ValueError("{0} must be a set".format(value))
53+
subset._type_check(BINARY_SET_SHORT, NUMBER_SET_SHORT, STRING_SET_SHORT)
5854
super(DeleteAction, self).__init__(path, subset)
5955

6056

0 commit comments

Comments
 (0)