Skip to content

Commit d7e2f57

Browse files
authored
Improve typing in expressions module. (#851)
1 parent 1667a43 commit d7e2f57

File tree

8 files changed

+112
-82
lines changed

8 files changed

+112
-82
lines changed

pynamodb/connection/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,7 @@ def batch_get_item(
11451145
}
11461146
}
11471147

1148-
args_map = {}
1148+
args_map: Dict[str, Any] = {}
11491149
name_placeholders: Dict[str, str] = {}
11501150
if consistent_read:
11511151
args_map[CONSISTENT_READ] = consistent_read

pynamodb/constants.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,3 @@
201201
META_CLASS_NAME = "Meta"
202202
REGION = "region"
203203
HOST = "host"
204-
205-
AND = 'AND'
206-
OR = 'OR'
207-
BETWEEN = 'BETWEEN'
208-
IN = 'IN'

pynamodb/expressions/condition.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pynamodb.constants import AND, BETWEEN, IN, OR
1+
from typing import Dict
22

33

44
# match dynamo function syntax: size(path)
@@ -8,13 +8,13 @@ def size(path):
88

99

1010
class Condition(object):
11-
format_string = ''
11+
format_string: str = ''
1212

13-
def __init__(self, operator, *values):
13+
def __init__(self, operator: str, *values) -> None:
1414
self.operator = operator
1515
self.values = values
1616

17-
def serialize(self, placeholder_names, expression_attribute_values):
17+
def serialize(self, placeholder_names: Dict[str, str], expression_attribute_values: Dict[str, str]) -> str:
1818
values = [value.serialize(placeholder_names, expression_attribute_values) for value in self.values]
1919
return self.format_string.format(*values, operator=self.operator)
2020

@@ -40,14 +40,10 @@ def __or__(self, other):
4040
def __invert__(self):
4141
return Not(self)
4242

43-
def __repr__(self):
43+
def __repr__(self) -> str:
4444
values = [str(value) for value in self.values]
4545
return self.format_string.format(*values, operator=self.operator)
4646

47-
def __nonzero__(self):
48-
# Prevent users from accidentally comparing the condition object instead of the attribute instance
49-
raise TypeError("unsupported operand type(s) for bool: '{}'".format(self.__class__.__name__))
50-
5147
def __bool__(self):
5248
# Prevent users from accidentally comparing the condition object instead of the attribute instance
5349
raise TypeError("unsupported operand type(s) for bool: {}".format(self.__class__.__name__))
@@ -59,19 +55,19 @@ class Comparison(Condition):
5955
def __init__(self, operator, lhs, rhs):
6056
if operator not in ['=', '<>', '<', '<=', '>', '>=']:
6157
raise ValueError("{0} is not a valid comparison operator: {0}".format(operator))
62-
super(Comparison, self).__init__(operator, lhs, rhs)
58+
super().__init__(operator, lhs, rhs)
6359

6460

6561
class Between(Condition):
6662
format_string = '{0} {operator} {1} AND {2}'
6763

6864
def __init__(self, path, lower, upper):
69-
super(Between, self).__init__(BETWEEN, path, lower, upper)
65+
super().__init__('BETWEEN', path, lower, upper)
7066

7167

7268
class In(Condition):
7369
def __init__(self, path, *values):
74-
super(In, self).__init__(IN, path, *values)
70+
super().__init__('IN', path, *values)
7571
list_format = ', '.join('{' + str(i + 1) + '}' for i in range(len(values)))
7672
self.format_string = '{0} {operator} (' + list_format + ')'
7773

@@ -80,53 +76,53 @@ class Exists(Condition):
8076
format_string = '{operator} ({0})'
8177

8278
def __init__(self, path):
83-
super(Exists, self).__init__('attribute_exists', path)
79+
super().__init__('attribute_exists', path)
8480

8581

8682
class NotExists(Condition):
8783
format_string = '{operator} ({0})'
8884

8985
def __init__(self, path):
90-
super(NotExists, self).__init__('attribute_not_exists', path)
86+
super().__init__('attribute_not_exists', path)
9187

9288

9389
class IsType(Condition):
9490
format_string = '{operator} ({0}, {1})'
9591

9692
def __init__(self, path, attr_type):
97-
super(IsType, self).__init__('attribute_type', path, attr_type)
93+
super().__init__('attribute_type', path, attr_type)
9894

9995

10096
class BeginsWith(Condition):
10197
format_string = '{operator} ({0}, {1})'
10298

10399
def __init__(self, path, prefix):
104-
super(BeginsWith, self).__init__('begins_with', path, prefix)
100+
super().__init__('begins_with', path, prefix)
105101

106102

107103
class Contains(Condition):
108104
format_string = '{operator} ({0}, {1})'
109105

110106
def __init__(self, path, operand):
111-
super(Contains, self).__init__('contains', path, operand)
107+
super().__init__('contains', path, operand)
112108

113109

114110
class And(Condition):
115111
format_string = '({0} {operator} {1})'
116112

117-
def __init__(self, condition1, condition2):
118-
super(And, self).__init__(AND, condition1, condition2)
113+
def __init__(self, condition1: Condition, condition2: Condition) -> None:
114+
super().__init__('AND', condition1, condition2)
119115

120116

121117
class Or(Condition):
122118
format_string = '({0} {operator} {1})'
123119

124-
def __init__(self, condition1, condition2):
125-
super(Or, self).__init__(OR, condition1, condition2)
120+
def __init__(self, condition1: Condition, condition2: Condition) -> None:
121+
super().__init__('OR', condition1, condition2)
126122

127123

128124
class Not(Condition):
129125
format_string = '({operator} {0})'
130126

131-
def __init__(self, condition):
132-
super(Not, self).__init__('NOT', condition)
127+
def __init__(self, condition: Condition) -> None:
128+
super().__init__('NOT', condition)

pynamodb/expressions/operand.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@
1515
if TYPE_CHECKING:
1616
from pynamodb.attributes import Attribute
1717

18-
_PathOrAttribute = Union['Path', 'Attribute', List[str], str]
19-
2018

2119
class _Operand:
2220
"""
2321
Operand is the base class for objects that can be operands in Condition and Update Expressions.
2422
"""
2523
format_string = ''
26-
attr_type: Any = None
24+
attr_type: Optional[str] = None
2725

2826
def __init__(self, *values: Any) -> None:
2927
self.values = values
@@ -122,7 +120,7 @@ class _Size(_ConditionOperand):
122120
format_string = 'size ({0})'
123121
attr_type = NUMBER
124122

125-
def __init__(self, path: _PathOrAttribute) -> None:
123+
def __init__(self, path: Union['Path', 'Attribute', str, List[str]]) -> None:
126124
if not isinstance(path, Path):
127125
path = Path(path)
128126
super(_Size, self).__init__(path)
@@ -235,9 +233,9 @@ class Path(_NumericOperand, _ListAppendOperand, _ConditionOperand):
235233
"""
236234
format_string = '{0}'
237235

238-
def __init__(self, attribute_or_path: _PathOrAttribute) -> None:
236+
def __init__(self, attribute_or_path: Union['Attribute', str, List[str]]) -> None:
239237
from pynamodb.attributes import Attribute # prevent circular import -- Attribute imports Path
240-
path: _PathOrAttribute
238+
path: Union[str, List[str]]
241239
if isinstance(attribute_or_path, Attribute):
242240
self.attribute = attribute_or_path
243241
self.attr_type = attribute_or_path.attr_type
@@ -251,7 +249,7 @@ def __init__(self, attribute_or_path: _PathOrAttribute) -> None:
251249
super(Path, self).__init__(get_path_segments(path))
252250

253251
@property
254-
def path(self) -> Any:
252+
def path(self) -> List[str]:
255253
return self.values[0]
256254

257255
def __iter__(self):
@@ -338,6 +336,6 @@ def __repr__(self) -> str:
338336
return "Path({})".format(self.path)
339337

340338
@staticmethod
341-
def _quote_path(path):
339+
def _quote_path(path: str) -> str:
342340
path, sep, rem = path.partition('[')
343341
return repr(path) + sep + rem

pynamodb/expressions/projection.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1+
from typing import Dict
2+
from typing import List
3+
from typing import Union
4+
15
from pynamodb.attributes import Attribute
26
from pynamodb.expressions.operand import Path
37
from pynamodb.expressions.util import substitute_names
48

59

6-
def create_projection_expression(attributes_to_get, placeholders):
10+
def create_projection_expression(attributes_to_get, placeholders: Dict[str, str]) -> str:
711
if not isinstance(attributes_to_get, list):
812
attributes_to_get = [attributes_to_get]
913
expressions = [substitute_names(_get_document_path(attribute), placeholders) for attribute in attributes_to_get]
1014
return ', '.join(expressions)
1115

1216

13-
def _get_document_path(attribute):
17+
def _get_document_path(attribute: Union[Attribute, Path, str]) -> List[str]:
1418
if isinstance(attribute, Attribute):
1519
return [attribute.attr_name]
1620
if isinstance(attribute, Path):

pynamodb/expressions/update.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1+
from typing import Dict
12
from typing import List
3+
from typing import Optional
4+
from typing import Sequence
25
from typing import TYPE_CHECKING
36

4-
from pynamodb.constants import BINARY_SET, NUMBER, NUMBER_SET, STRING_SET
7+
from pynamodb.constants import BINARY_SET
8+
from pynamodb.constants import NUMBER
9+
from pynamodb.constants import NUMBER_SET
10+
from pynamodb.constants import STRING_SET
511

612
if TYPE_CHECKING:
13+
from pynamodb.expressions.operand import _Operand
714
from pynamodb.expressions.operand import Path
15+
from pynamodb.expressions.operand import Value
816

917

10-
class Action(object):
11-
format_string = ''
18+
class Action:
19+
format_string: str = ''
1220

13-
def __init__(self, *values: 'Path') -> None:
21+
def __init__(self, *values: '_Operand') -> None:
1422
self.values = values
1523

16-
def serialize(self, placeholder_names, expression_attribute_values):
24+
def serialize(self, placeholder_names: Dict[str, str], expression_attribute_values: Dict[str, str]) -> str:
1725
values = [value.serialize(placeholder_names, expression_attribute_values) for value in self.values]
1826
return self.format_string.format(*values)
1927

20-
def __repr__(self):
28+
def __repr__(self) -> str:
2129
values = [str(value) for value in self.values]
2230
return self.format_string.format(*values)
2331

@@ -28,7 +36,7 @@ class SetAction(Action):
2836
"""
2937
format_string = '{0} = {1}'
3038

31-
def __init__(self, path: 'Path', value: 'Path') -> None:
39+
def __init__(self, path: 'Path', value: '_Operand') -> None:
3240
super(SetAction, self).__init__(path, value)
3341

3442

@@ -48,7 +56,8 @@ class AddAction(Action):
4856
"""
4957
format_string = '{0} {1}'
5058

51-
def __init__(self, path: 'Path', subset: 'Path') -> None:
59+
def __init__(self, path: 'Path', subset: 'Value') -> None:
60+
path._type_check(BINARY_SET, NUMBER, NUMBER_SET, STRING_SET)
5261
subset._type_check(BINARY_SET, NUMBER, NUMBER_SET, STRING_SET)
5362
super(AddAction, self).__init__(path, subset)
5463

@@ -59,12 +68,13 @@ class DeleteAction(Action):
5968
"""
6069
format_string = '{0} {1}'
6170

62-
def __init__(self, path: 'Path', subset: 'Path') -> None:
71+
def __init__(self, path: 'Path', subset: 'Value') -> None:
72+
path._type_check(BINARY_SET, NUMBER_SET, STRING_SET)
6373
subset._type_check(BINARY_SET, NUMBER_SET, STRING_SET)
6474
super(DeleteAction, self).__init__(path, subset)
6575

6676

67-
class Update(object):
77+
class Update:
6878

6979
def __init__(self, *actions: Action) -> None:
7080
self.set_actions: List[SetAction] = []
@@ -86,22 +96,24 @@ def add_action(self, action: Action) -> None:
8696
else:
8797
raise ValueError("unsupported action type: '{}'".format(action.__class__.__name__))
8898

89-
def serialize(self, placeholder_names, expression_attribute_values):
90-
expression = None
91-
expression = self._add_clause(expression, 'SET', self.set_actions, placeholder_names, expression_attribute_values)
92-
expression = self._add_clause(expression, 'REMOVE', self.remove_actions, placeholder_names, expression_attribute_values)
93-
expression = self._add_clause(expression, 'ADD', self.add_actions, placeholder_names, expression_attribute_values)
94-
expression = self._add_clause(expression, 'DELETE', self.delete_actions, placeholder_names, expression_attribute_values)
95-
return expression
99+
def serialize(self, placeholder_names: Dict[str, str], expression_attribute_values: Dict[str, str]) -> Optional[str]:
100+
clauses = [
101+
self._get_clause('SET', self.set_actions, placeholder_names, expression_attribute_values),
102+
self._get_clause('REMOVE', self.remove_actions, placeholder_names, expression_attribute_values),
103+
self._get_clause('ADD', self.add_actions, placeholder_names, expression_attribute_values),
104+
self._get_clause('DELETE', self.delete_actions, placeholder_names, expression_attribute_values),
105+
]
106+
expression = ' '.join(clause for clause in clauses if clause is not None)
107+
return expression or None
96108

97109
@staticmethod
98-
def _add_clause(expression, keyword, actions, placeholder_names, expression_attribute_values):
99-
clause = Update._get_clause(keyword, actions, placeholder_names, expression_attribute_values)
100-
if clause is None:
101-
return expression
102-
return clause if expression is None else expression + " " + clause
103-
104-
@staticmethod
105-
def _get_clause(keyword, actions, placeholder_names, expression_attribute_values):
106-
actions = ", ".join([action.serialize(placeholder_names, expression_attribute_values) for action in actions])
107-
return keyword + " " + actions if actions else None
110+
def _get_clause(
111+
keyword: str,
112+
actions: Sequence[Action],
113+
placeholder_names: Dict[str, str],
114+
expression_attribute_values: Dict[str, str]
115+
) -> Optional[str]:
116+
actions_string = ', '.join(
117+
action.serialize(placeholder_names, expression_attribute_values) for action in actions
118+
)
119+
return keyword + ' ' + actions_string if actions_string else None

pynamodb/expressions/util.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
11
import re
2-
from typing import Dict, List, Union
3-
from typing import TYPE_CHECKING
4-
5-
if TYPE_CHECKING:
6-
from pynamodb.expressions.operands import Path
2+
from typing import Any
3+
from typing import Dict
4+
from typing import List
5+
from typing import Union
76

87

98
PATH_SEGMENT_REGEX = re.compile(r'([^\[\]]+)((?:\[\d+\])*)$')
109

1110

12-
def get_path_segments(document_path: Union[str, 'Path', List[str]]) -> Union[List[str], List['Path']]:
11+
def get_path_segments(document_path: Union[str, List[str]]) -> List[str]:
12+
"""
13+
Splits a document path into nested elements using the map dereference operator (.)
14+
and returns the list of path segments (an attribute name and optional list dereference operators ([n]).
15+
If the document path is already a list of path segments, a new copy is returned.
16+
17+
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html
18+
19+
Note: callers depend upon the returned list being a copy so that it may be safely mutated
20+
"""
1321
return document_path.split('.') if isinstance(document_path, str) else list(document_path)
1422

1523

16-
def substitute_names(document_path: Union[str, 'Path'], placeholders: Dict[str, str]) -> str:
24+
def substitute_names(document_path: Union[str, List[str]], placeholders: Dict[str, str]) -> str:
1725
"""
1826
Replaces all attribute names in the given document path with placeholders.
1927
Stores the placeholders in the given dictionary.
2028
21-
:param document_path: list of path segments (an attribute name and optional list dereference)
29+
:param document_path: list of path segments (an attribute name and optional list dereference operators)
2230
:param placeholders: a dictionary to store mappings from attribute names to expression attribute name placeholders
2331
2432
For example: given the document_path for some attribute "baz", that is the first element of a list attribute "bar",
25-
that itself is a map element of "foo" (i.e. ['foo', 'bar[0], 'baz']) and an empty placeholders dictionary,
33+
that itself is a map element of "foo" (i.e. ['foo', 'bar[0]', 'baz']) and an empty placeholders dictionary,
2634
`substitute_names` will return "#0.#1[0].#2" and placeholders will contain {"foo": "#0", "bar": "#1", "baz": "#2}
2735
"""
2836
path_segments = get_path_segments(document_path)
@@ -40,7 +48,7 @@ def substitute_names(document_path: Union[str, 'Path'], placeholders: Dict[str,
4048
return '.'.join(path_segments)
4149

4250

43-
def get_value_placeholder(value: 'Path', expression_attribute_values: Dict[str, str]) -> str:
51+
def get_value_placeholder(value: Any, expression_attribute_values: Dict[str, str]) -> str:
4452
placeholder = ':' + str(len(expression_attribute_values))
4553
expression_attribute_values[placeholder] = value
4654
return placeholder

0 commit comments

Comments
 (0)