Skip to content

Commit a5f2369

Browse files
authored
Support removing specific indexes from a ListAttribute (#754)
1 parent b28dcd6 commit a5f2369

File tree

8 files changed

+59
-3
lines changed

8 files changed

+59
-3
lines changed

docs/updates.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ Any value provided will be serialized using the serializer defined for that attr
4444
`attr_or_value_1` \- `attr_or_value_2`, `attr_or_value_1` \- `attr_or_value_2`, 5 - Thread.views
4545
"list_append( `attr` , `value` )", append( `value` ), Thread.notes.append(['my last note'])
4646
"list_append( `value` , `attr` )", prepend( `value` ), Thread.notes.prepend(['my first note'])
47+
"REMOVE list[index1], list[index2]", "remove_indexes(`index1`, `index2`)", "Thread.notes.remove_indexes(0, 1)"
4748
"if_not_exists( `attr`, `value` )", `attr` | `value`, Thread.forum_name | 'Default Forum Name'

pynamodb/attributes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,11 @@ def __init__(self, hash_key=False, range_key=False, null=None, default=None, att
961961
raise ValueError("'of' must be subclass of MapAttribute")
962962
self.element_type = of
963963

964+
def remove_indexes(self, *indexes):
965+
if not all([isinstance(i, int) for i in indexes]):
966+
raise ValueError("Method 'remove_indexes' arguments must be 'int'")
967+
return Path(self).remove_list_elements(*indexes)
968+
964969
def serialize(self, values):
965970
"""
966971
Encode the given list of objects into a list of AttributeValue types.

pynamodb/attributes.pyi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ from pynamodb.expressions.operand import (
99
_Decrement, _IfNotExists, _Increment, _ListAppend
1010
)
1111
from pynamodb.expressions.update import (
12-
AddAction, DeleteAction, RemoveAction, SetAction
12+
AddAction, DeleteAction, RemoveAction, SetAction, ListRemoveAction
1313
)
1414

1515

@@ -168,6 +168,8 @@ class ListAttribute(Generic[_T], Attribute[List[_T]]):
168168
def __get__(self: _A, instance: None, owner: Any) -> _A: ...
169169
@overload
170170
def __get__(self, instance: Any, owner: Any) -> List[_T]: ...
171+
def remove_indexes(self, *indexes: int) -> Union[ListRemoveAction]: ...
172+
171173

172174
DESERIALIZE_CLASS_MAP: Dict[Text, Attribute]
173175
SERIALIZE_CLASS_MAP: Dict[Type, Attribute]

pynamodb/expressions/operand.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
BeginsWith, Between, Comparison, Contains, Exists, In, IsType, NotExists
77
)
88
from pynamodb.expressions.update import (
9-
AddAction, DeleteAction, RemoveAction, SetAction
9+
AddAction, DeleteAction, RemoveAction, SetAction, ListRemoveAction
1010
)
1111
from pynamodb.expressions.util import get_path_segments, get_value_placeholder, substitute_names
1212
from six import string_types
@@ -277,6 +277,9 @@ def remove(self):
277277
# Returns an update action that removes this attribute from the item
278278
return RemoveAction(self)
279279

280+
def remove_list_elements(self, *indexes):
281+
return ListRemoveAction(self, *indexes)
282+
280283
def add(self, *values):
281284
# Returns an update action that appends the given values to a set or mathematically adds a value to a number
282285
value = values[0] if len(values) == 1 else values

pynamodb/expressions/operand.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ from pynamodb.expressions.condition import (
55
BeginsWith, Between, Comparison, Contains, Exists, In, IsType, NotExists
66
)
77
from pynamodb.expressions.update import (
8-
AddAction, DeleteAction, RemoveAction, SetAction
8+
AddAction, DeleteAction, RemoveAction, SetAction, ListRemoveAction
99
)
1010

1111

@@ -78,3 +78,4 @@ class Path(_NumericOperand, _ListAppendOperand, _ConditionOperand):
7878
def contains(self, item: Any) -> Contains: ...
7979
def set(self, value: Any) -> SetAction: ...
8080
def remove(self) -> RemoveAction: ...
81+
def remove_list_elements(self, *indexes: int) -> ListRemoveAction: ...

pynamodb/expressions/update.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ def __init__(self, path):
3636
super(RemoveAction, self).__init__(path)
3737

3838

39+
class ListRemoveAction(Action):
40+
"""
41+
The List REMOVE action deletes an element from a list item based on the index.
42+
"""
43+
format_string = None
44+
45+
def __init__(self, path, *indexes):
46+
self.format_string = ", ".join("{{0}}[{}]".format(index) for index in indexes)
47+
super(ListRemoveAction, self).__init__(path)
48+
49+
3950
class AddAction(Action):
4051
"""
4152
The ADD action appends elements to a set or mathematically adds to a number attribute.
@@ -65,6 +76,7 @@ def __init__(self, *actions):
6576
self.remove_actions = []
6677
self.add_actions = []
6778
self.delete_actions = []
79+
self.list_remove_actions = []
6880
for action in actions:
6981
self.add_action(action)
7082

@@ -73,6 +85,8 @@ def add_action(self, action):
7385
self.set_actions.append(action)
7486
elif isinstance(action, RemoveAction):
7587
self.remove_actions.append(action)
88+
elif isinstance(action, ListRemoveAction):
89+
self.list_remove_actions.append(action)
7690
elif isinstance(action, AddAction):
7791
self.add_actions.append(action)
7892
elif isinstance(action, DeleteAction):
@@ -84,6 +98,7 @@ def serialize(self, placeholder_names, expression_attribute_values):
8498
expression = None
8599
expression = self._add_clause(expression, 'SET', self.set_actions, placeholder_names, expression_attribute_values)
86100
expression = self._add_clause(expression, 'REMOVE', self.remove_actions, placeholder_names, expression_attribute_values)
101+
expression = self._add_clause(expression, 'REMOVE', self.list_remove_actions, placeholder_names, expression_attribute_values)
87102
expression = self._add_clause(expression, 'ADD', self.add_actions, placeholder_names, expression_attribute_values)
88103
expression = self._add_clause(expression, 'DELETE', self.delete_actions, placeholder_names, expression_attribute_values)
89104
return expression

pynamodb/expressions/update.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ class RemoveAction(Action):
1515
def __init__(self, path: Path) -> None: ...
1616

1717

18+
class ListRemoveAction(Action):
19+
def __init__(self, path: Path, *indexes: int) -> None: ...
20+
21+
1822
class AddAction(Action):
1923
def __init__(self, path: Path, subset: Path) -> None: ...
2024

@@ -26,6 +30,7 @@ class DeleteAction(Action):
2630
class Update(object):
2731
set_actions: List[SetAction]
2832
remove_actions: List[RemoveAction]
33+
list_remove_actions: List[ListRemoveAction]
2934
add_actions: List[AddAction]
3035
delete_actions: List[DeleteAction]
3136
def __init__(self, *actions: Action) -> None: ...

tests/test_expressions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ class UpdateExpressionTestCase(TestCase):
431431

432432
def setUp(self):
433433
self.attribute = UnicodeAttribute(attr_name='foo')
434+
self.list_attribute = ListAttribute(attr_name='foo_list', default=[])
434435

435436
def test_set_action(self):
436437
action = self.attribute.set('bar')
@@ -589,3 +590,26 @@ def test_update(self):
589590
':1': {'N': '0'},
590591
':2': {'NS': ['0']}
591592
}
593+
594+
def test_list_update_remove_by_index(self):
595+
update = Update(
596+
self.list_attribute.remove_indexes(0),
597+
)
598+
placeholder_names, expression_attribute_values = {}, {}
599+
expression = update.serialize(placeholder_names, expression_attribute_values)
600+
assert expression == "REMOVE #0[0]"
601+
assert placeholder_names == {'foo_list': '#0'}
602+
assert expression_attribute_values == {}
603+
604+
update = Update(
605+
self.list_attribute.remove_indexes(0, 10),
606+
)
607+
placeholder_names, expression_attribute_values = {}, {}
608+
expression = update.serialize(placeholder_names, expression_attribute_values)
609+
assert expression == "REMOVE #0[0], #0[10]"
610+
assert placeholder_names == {'foo_list': '#0'}
611+
assert expression_attribute_values == {}
612+
613+
with self.assertRaises(ValueError) as e:
614+
Update(self.list_attribute.remove_indexes(0, "a"))
615+
assert str(e.exception) == "Method 'remove_indexes' arguments must be 'int'"

0 commit comments

Comments
 (0)