Repository: getmoto/moto

Issue:
[Feature][dynamodb2] AttributeUpdates ADD to List not implemented
Adding to a list via AttributeUpdates has not yet been implemented.

### Reproduce code:
```python
import boto3
import moto

@moto.mock_dynamodb2
def test_dynamodb2():
    resource = boto3.resource('dynamodb', region_name='us-west-1')
    resource.create_table(
        AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
        TableName="TestTable",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        BillingMode="PAY_PER_REQUEST",
    )
    table = resource.Table("TestTable")
    table.wait_until_exists()
    table.put_item(
        Item={
            "id": "list_add",
            "attr": ["a", "b", "c"],
        },
    )

    # Update item to add to 'attr' list
    table.update_item(
        TableName="TestTable",
        Key={"id": "list_add"},
        AttributeUpdates={ "attr": { 'Action': 'ADD', 'Value': ["d", "e"] } }
    )

    # Verify item has been added to 'attr' list
    result = table.get_item(
        Key={"id": "list_add"},
    )["Item"]
    assert {
        "id": "list_add",
        "attr": ["a", "b", "c", "d", "e"],
    } == result
```

### Error message:
```
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv\lib\site-packages\boto3\resources\factory.py:520: in do_action
    response = action(self, *args, **kwargs)
.venv\lib\site-packages\boto3\resources\action.py:83: in __call__
    response = getattr(parent.meta.client, operation_name)(*args, **params)
.venv\lib\site-packages\botocore\client.py:357: in _api_call
    return self._make_api_call(operation_name, kwargs)
.venv\lib\site-packages\botocore\client.py:662: in _make_api_call
    http, parsed_response = self._make_request(
.venv\lib\site-packages\botocore\client.py:682: in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
.venv\lib\site-packages\botocore\endpoint.py:102: in make_request
    return self._send_request(request_dict, operation_model)
.venv\lib\site-packages\botocore\endpoint.py:136: in _send_request
    while self._needs_retry(attempts, operation_model, request_dict,
.venv\lib\site-packages\botocore\endpoint.py:253: in _needs_retry
    responses = self._event_emitter.emit(
.venv\lib\site-packages\botocore\hooks.py:356: in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
.venv\lib\site-packages\botocore\hooks.py:228: in emit
    return self._emit(event_name, kwargs)
.venv\lib\site-packages\botocore\hooks.py:211: in _emit
    response = handler(**kwargs)
.venv\lib\site-packages\botocore\retryhandler.py:183: in __call__
    if self._checker(attempts, response, caught_exception):
.venv\lib\site-packages\botocore\retryhandler.py:250: in __call__
    should_retry = self._should_retry(attempt_number, response,
.venv\lib\site-packages\botocore\retryhandler.py:269: in _should_retry
    return self._checker(attempt_number, response, caught_exception)
.venv\lib\site-packages\botocore\retryhandler.py:316: in __call__
    checker_response = checker(attempt_number, response,
.venv\lib\site-packages\botocore\retryhandler.py:222: in __call__
    return self._check_caught_exception(
.venv\lib\site-packages\botocore\retryhandler.py:359: in _check_caught_exception
    raise caught_exception
.venv\lib\site-packages\botocore\endpoint.py:197: in _do_get_response
    responses = self._event_emitter.emit(event_name, request=request)
.venv\lib\site-packages\botocore\hooks.py:356: in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
.venv\lib\site-packages\botocore\hooks.py:228: in emit
    return self._emit(event_name, kwargs)
.venv\lib\site-packages\botocore\hooks.py:211: in _emit
    response = handler(**kwargs)
.venv\lib\site-packages\moto\core\models.py:322: in __call__
    status, headers, body = response_callback(
.venv\lib\site-packages\moto\core\responses.py:202: in dispatch
    return cls()._dispatch(*args, **kwargs)
.venv\lib\site-packages\moto\core\responses.py:312: in _dispatch
    return self.call_action()
.venv\lib\site-packages\moto\core\utils.py:277: in _wrapper
    response = f(*args, **kwargs)
.venv\lib\site-packages\moto\dynamodb2\responses.py:86: in call_action
    response = getattr(self, endpoint)()
.venv\lib\site-packages\moto\dynamodb2\responses.py:769: in update_item
    item = self.dynamodb_backend.update_item(
.venv\lib\site-packages\moto\dynamodb2\models\__init__.py:1287: in update_item
    item.update_with_attribute_updates(attribute_updates)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = Item: {'Attributes': {'id': {'S': 'list_add'}, 'attr': {'L': [DynamoType: {'S': 'a'}, DynamoType: {'S': 'b'}, DynamoType: {'S': 'c'}]}}}
attribute_updates = {'attr': {'Action': 'ADD', 'Value': {'L': [{'S': 'd'}, {'S': 'e'}]}}}

                elif set(update_action["Value"].keys()) == set(["SS"]):
                    existing = self.attrs.get(attribute_name, DynamoType({"SS": {}}))
                    new_set = set(existing.value).union(set(new_value))
                    self.attrs[attribute_name] = DynamoType({"SS": list(new_set)})
                else:
                    # TODO: implement other data types
>                   raise NotImplementedError(
                        "ADD not supported for %s"
                        % ", ".join(update_action["Value"].keys())
E                       NotImplementedError: ADD not supported for L

```

Thanks!


Discussion:
Thanks - marking it as an enhancement

Resolving patch:
diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py
--- a/moto/dynamodb2/models/__init__.py
+++ b/moto/dynamodb2/models/__init__.py
@@ -156,6 +156,10 @@ def update_with_attribute_updates(self, attribute_updates):
                     existing = self.attrs.get(attribute_name, DynamoType({"SS": {}}))
                     new_set = set(existing.value).union(set(new_value))
                     self.attrs[attribute_name] = DynamoType({"SS": list(new_set)})
+                elif set(update_action["Value"].keys()) == {"L"}:
+                    existing = self.attrs.get(attribute_name, DynamoType({"L": []}))
+                    new_list = existing.value + new_value
+                    self.attrs[attribute_name] = DynamoType({"L": new_list})
                 else:
                     # TODO: implement other data types
                     raise NotImplementedError(


Test patch:
diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py
--- a/tests/test_dynamodb2/test_dynamodb.py
+++ b/tests/test_dynamodb2/test_dynamodb.py
@@ -5704,3 +5704,26 @@ def test_dynamodb_update_item_fails_on_string_sets():
         Key={"record_id": {"S": "testrecord"}},
         AttributeUpdates=attribute,
     )
+
+
+@moto.mock_dynamodb2
+def test_update_item_add_to_list_using_legacy_attribute_updates():
+    resource = boto3.resource("dynamodb", region_name="us-west-2")
+    resource.create_table(
+        AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
+        TableName="TestTable",
+        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
+        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
+    )
+    table = resource.Table("TestTable")
+    table.wait_until_exists()
+    table.put_item(Item={"id": "list_add", "attr": ["a", "b", "c"]},)
+
+    table.update_item(
+        TableName="TestTable",
+        Key={"id": "list_add"},
+        AttributeUpdates={"attr": {"Action": "ADD", "Value": ["d", "e"]}},
+    )
+
+    resp = table.get_item(Key={"id": "list_add"})
+    resp["Item"]["attr"].should.equal(["a", "b", "c", "d", "e"])