Repository: python-gitlab/python-gitlab

Issue:
project-variable update fails
## Description of the problem, including code/CLI snippet
$ gitlab project-variable create --project-id xxxxxxxx --key junk --value car
key: junk
gitlab project-variable update --project-id xxxxxxxx --key junk --value boat
Impossible to update object (Missing attributes: key)

## Expected Behavior
Update the value for the key

## Actual Behavior
Error for missing key.

## Specifications
- API version v4
- gitlab.com

In gitlab.v4.cli.py
```python
    def do_update(self):
        id = None
        if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.mgr_cls):
            id = self.args.pop(self.cls._id_attr)
        try:
            return self.mgr.update(id, self.args, path_arg=self.cls._id_attr)
        except Exception as e:
            cli.die("Impossible to update object", e)
```
self.cls._id_attr='key' and gets popped from the list of arguments so that it can get added to the path in the http request.  

In gitlab.mixins:update()
```python
...
        if id is None:
            path = self.path
        else:
            path = '%s/%s' % (self.path, id)

        self._check_missing_update_attrs(new_data)
        files = {}
...
```
and gitlab.mixins:
```python
    def _check_missing_update_attrs(self, data):
        required, optional = self.get_update_attrs()
        missing = []
        for attr in required:
            if attr not in data:
                missing.append(attr)
                continue
        if missing:
            raise AttributeError("Missing attributes: %s" % ", ".join(missing))
```
required=('key', 'value')

This causes the call to fail because the 'key' value is no longer in the args list.  Removing 'key' from the required list solves the problem.

```python
    def _check_missing_update_attrs(self, data):
        required, optional = self.get_update_attrs()

        # Remove the id field from the required list as it has been moved to the http path.
        required = tuple(filter(lambda k: k != self._obj_cls._id_attr, required))

        missing = []
```

I'll submit a pull request with this update and some additions to the test cases.  The problem may exist in other update requests on different objects, but I didn't explore those at this time.

Thanks for the comprehensive library.  It has save us a bunch of time.



Discussion:

Resolving patch:
diff --git a/gitlab/mixins.py b/gitlab/mixins.py
--- a/gitlab/mixins.py
+++ b/gitlab/mixins.py
@@ -208,6 +208,8 @@ def create(self, data, **kwargs):
 class UpdateMixin(object):
     def _check_missing_update_attrs(self, data):
         required, optional = self.get_update_attrs()
+        # Remove the id field from the required list as it was previously moved to the http path.
+        required = tuple(filter(lambda k: k != self._obj_cls._id_attr, required))
         missing = []
         for attr in required:
             if attr not in data:


Test patch:
diff --git a/tools/cli_test_v4.sh b/tools/cli_test_v4.sh
old mode 100644
new mode 100755
--- a/tools/cli_test_v4.sh
+++ b/tools/cli_test_v4.sh
@@ -89,6 +89,31 @@ testcase "merge request validation" '
         --iid "$MR_ID" >/dev/null 2>&1
 '
 
+# Test project variables
+testcase "create project variable" '
+    OUTPUT=$(GITLAB -v project-variable create --project-id $PROJECT_ID \
+        --key junk --value car)
+'
+
+testcase "get project variable" '
+    OUTPUT=$(GITLAB -v project-variable get --project-id $PROJECT_ID \
+        --key junk)
+'
+
+testcase "update project variable" '
+    OUTPUT=$(GITLAB -v project-variable update --project-id $PROJECT_ID \
+        --key junk --value bus)
+'
+
+testcase "list project variable" '
+    OUTPUT=$(GITLAB -v project-variable list --project-id $PROJECT_ID)
+'
+
+testcase "delete project variable" '
+    OUTPUT=$(GITLAB -v project-variable delete --project-id $PROJECT_ID \
+        --key junk)
+'
+
 testcase "branch deletion" '
     GITLAB project-branch delete --project-id "$PROJECT_ID" \
         --name branch1 >/dev/null 2>&1