Repository: python-gitlab/python-gitlab

Issue:
RefreshMixin.refresh() doesn't remove removed attributes
## Description of the problem, including code/CLI snippet

When attributes disappear from an object on the server `RefreshMixin.refresh()` doesn't remove them.

For instance if a job that has artifacts will have an `artifacts_file` attribute.  If you call `.delete_artifacts()` on it, then call `.refresh()` the `artifacts_file` attribute will still be there.

```bash
# get a job with artifacts
job = project.jobs.get(job_id)
# will succeed
assert hasattr(job, "artifacts_file")
# now delete the artifacts from the server
job.delete_artifacts()

# This will fail because the artifacts_file is still there; refresh() didn't remove it
job.refresh()
assert not hasattr(job, "artifacts_file")

# If you get the job again from the project it'll be fine
job = project.jobs.get(job_id)
assert not hasattr(job, "artifacts_file")
```

```python

```


## Expected Behavior

I would expect that the attributes dict on any object should be exactly the same between a freshly retrieved object and an old object after calling `.refresh()`

```python
o.refresh()
# After a refresh this should always be true
o.attributes == o.manager.get(o.id).attributes
```

## Actual Behavior

They're not equal

## Specifications

  - python-gitlab version: `v2.4.0`
  - API version you are using (v3/v4): `v4`
  - Gitlab server version (or gitlab.com): `13.2.3`



Discussion:
Before creating a merge request I'd like to know if we feel this is indeed a bug.

Doing a `git grep "\._update_attrs(" "*.py"` I see that this method is only ever called with `server_data` as the parameter.
I'd suspect that we could simply modify the `_update_attrs` method to make it exactly equal.
I'm guessing this because it is only ever called with `server_data` so likely never called with any partial updates.

## Current implementation

```python
    def _update_attrs(self, new_attrs):
        self.__dict__["_updated_attrs"] = {}
        self.__dict__["_attrs"].update(new_attrs)
```

The `.update(new_attrs)` could be replaced with ` = new_attrs`.

Resolving patch:
diff --git a/gitlab/base.py b/gitlab/base.py
--- a/gitlab/base.py
+++ b/gitlab/base.py
@@ -131,7 +131,7 @@ def _create_managers(self):
 
     def _update_attrs(self, new_attrs):
         self.__dict__["_updated_attrs"] = {}
-        self.__dict__["_attrs"].update(new_attrs)
+        self.__dict__["_attrs"] = new_attrs
 
     def get_id(self):
         """Returns the id of the resource."""


Test patch:
diff --git a/gitlab/tests/test_base.py b/gitlab/tests/test_base.py
--- a/gitlab/tests/test_base.py
+++ b/gitlab/tests/test_base.py
@@ -128,6 +128,13 @@ def test_update_attrs(self, fake_manager):
         assert {"foo": "foo", "bar": "bar"} == obj._attrs
         assert {} == obj._updated_attrs
 
+    def test_update_attrs_deleted(self, fake_manager):
+        obj = FakeObject(fake_manager, {"foo": "foo", "bar": "bar"})
+        obj.bar = "baz"
+        obj._update_attrs({"foo": "foo"})
+        assert {"foo": "foo"} == obj._attrs
+        assert {} == obj._updated_attrs
+
     def test_create_managers(self, fake_gitlab, fake_manager):
         class ObjectWithManager(FakeObject):
             _managers = (("fakes", "FakeManager"),)